fix(NodeSwitcher): move all hooks before conditional return to comply with React hooks rules
Build and Deploy / Build and Push Docker Image (push) Successful in 2m1s
Build and Deploy / Build and Push Docker Image (push) Successful in 2m1s
- All hooks (useState, useCallback, useEffect) must be called before any conditional return - Add document check in updateRoleState - Merge SSR and isMaster checks into single return at the end
This commit is contained in:
@@ -20,15 +20,6 @@ export function NodeSwitcher() {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
// 在客户端初始化时从 localStorage 读取选中的节点
|
||||
setSelected(activeInterconnectNodeId || 'local')
|
||||
}, [])
|
||||
|
||||
// SSR 时返回 null,避免 hydration 错误
|
||||
if (!mounted) return null
|
||||
|
||||
const fetchNodes = useCallback(async () => {
|
||||
try {
|
||||
const res = await getNodes()
|
||||
@@ -48,15 +39,23 @@ export function NodeSwitcher() {
|
||||
} else {
|
||||
setNodes([])
|
||||
setSelected('local')
|
||||
const traveling = !!document.cookie.match(/(?:^| )active_interconnect_node_id=([^;]*)/)
|
||||
if (activeInterconnectNodeId && !traveling) {
|
||||
setActiveInterconnectNodeId('')
|
||||
if (typeof document !== 'undefined') {
|
||||
const traveling = !!document.cookie.match(/(?:^| )active_interconnect_node_id=([^;]*)/)
|
||||
if (activeInterconnectNodeId && !traveling) {
|
||||
setActiveInterconnectNodeId('')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetchNodes],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
// 在客户端初始化时从 localStorage 读取选中的节点
|
||||
setSelected(activeInterconnectNodeId || 'local')
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
try {
|
||||
@@ -85,47 +84,25 @@ export function NodeSwitcher() {
|
||||
}
|
||||
}, [isMaster, fetchNodes])
|
||||
|
||||
if (!isMaster) return null
|
||||
|
||||
const handleSelect = (val: string) => {
|
||||
setSelected(val)
|
||||
setOpen(false)
|
||||
if (val === 'local') {
|
||||
setActiveInterconnectNodeId('')
|
||||
} else {
|
||||
const name = nodes.find((n) => n.id === val)?.name || ''
|
||||
setActiveInterconnectNodeId(val, name)
|
||||
}
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const allOptions = [
|
||||
{ value: 'local', label: '本机节点' },
|
||||
...nodes.map((n) => ({ value: n.id, label: n.name })),
|
||||
]
|
||||
|
||||
const currentLabel = allOptions.find((o) => o.value === selected)?.label || '本机节点'
|
||||
|
||||
const updateMenuPosition = () => {
|
||||
const el = buttonRef.current
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
const viewportH = window.innerHeight
|
||||
const spaceBelow = viewportH - rect.bottom
|
||||
const openUp = spaceBelow < 220 && rect.top > spaceBelow
|
||||
setMenuStyle({
|
||||
position: 'fixed',
|
||||
left: rect.left,
|
||||
width: Math.max(rect.width, 140),
|
||||
zIndex: 80,
|
||||
...(openUp
|
||||
? { bottom: viewportH - rect.top + 6, top: 'auto' }
|
||||
: { top: rect.bottom + 6, bottom: 'auto' }),
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const updateMenuPosition = () => {
|
||||
const el = buttonRef.current
|
||||
if (!el) return
|
||||
const rect = el.getBoundingClientRect()
|
||||
const viewportH = window.innerHeight
|
||||
const spaceBelow = viewportH - rect.bottom
|
||||
const openUp = spaceBelow < 220 && rect.top > spaceBelow
|
||||
setMenuStyle({
|
||||
position: 'fixed',
|
||||
left: rect.left,
|
||||
width: Math.max(rect.width, 140),
|
||||
zIndex: 80,
|
||||
...(openUp
|
||||
? { bottom: viewportH - rect.top + 6, top: 'auto' }
|
||||
: { top: rect.bottom + 6, bottom: 'auto' }),
|
||||
})
|
||||
}
|
||||
updateMenuPosition()
|
||||
const onDoc = (e: MouseEvent) => {
|
||||
const t = e.target as Node
|
||||
@@ -148,6 +125,28 @@ export function NodeSwitcher() {
|
||||
}
|
||||
}, [open])
|
||||
|
||||
// SSR 时或非 master 节点时返回 null
|
||||
if (!mounted || !isMaster) return null
|
||||
|
||||
const handleSelect = (val: string) => {
|
||||
setSelected(val)
|
||||
setOpen(false)
|
||||
if (val === 'local') {
|
||||
setActiveInterconnectNodeId('')
|
||||
} else {
|
||||
const name = nodes.find((n) => n.id === val)?.name || ''
|
||||
setActiveInterconnectNodeId(val, name)
|
||||
}
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const allOptions = [
|
||||
{ value: 'local', label: '本机节点' },
|
||||
...nodes.map((n) => ({ value: n.id, label: n.name })),
|
||||
]
|
||||
|
||||
const currentLabel = allOptions.find((o) => o.value === selected)?.label || '本机节点'
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user