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

- 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:
2026-08-08 04:15:55 +08:00
parent 36d39e4a47
commit 8ef0ff8507
+32 -33
View File
@@ -20,15 +20,6 @@ export function NodeSwitcher() {
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
setMounted(true)
// 在客户端初始化时从 localStorage 读取选中的节点
setSelected(activeInterconnectNodeId || 'local')
}, [])
// SSR 时返回 null,避免 hydration 错误
if (!mounted) return null
const fetchNodes = useCallback(async () => { const fetchNodes = useCallback(async () => {
try { try {
const res = await getNodes() const res = await getNodes()
@@ -48,15 +39,23 @@ export function NodeSwitcher() {
} else { } else {
setNodes([]) setNodes([])
setSelected('local') setSelected('local')
if (typeof document !== 'undefined') {
const traveling = !!document.cookie.match(/(?:^| )active_interconnect_node_id=([^;]*)/) const traveling = !!document.cookie.match(/(?:^| )active_interconnect_node_id=([^;]*)/)
if (activeInterconnectNodeId && !traveling) { if (activeInterconnectNodeId && !traveling) {
setActiveInterconnectNodeId('') setActiveInterconnectNodeId('')
} }
} }
}
}, },
[fetchNodes], [fetchNodes],
) )
useEffect(() => {
setMounted(true)
// 在客户端初始化时从 localStorage 读取选中的节点
setSelected(activeInterconnectNodeId || 'local')
}, [])
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
try { try {
@@ -85,27 +84,8 @@ export function NodeSwitcher() {
} }
}, [isMaster, fetchNodes]) }, [isMaster, fetchNodes])
if (!isMaster) return null useEffect(() => {
if (!open) return
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 updateMenuPosition = () => {
const el = buttonRef.current const el = buttonRef.current
if (!el) return if (!el) return
@@ -123,9 +103,6 @@ export function NodeSwitcher() {
: { top: rect.bottom + 6, bottom: 'auto' }), : { top: rect.bottom + 6, bottom: 'auto' }),
}) })
} }
useEffect(() => {
if (!open) return
updateMenuPosition() updateMenuPosition()
const onDoc = (e: MouseEvent) => { const onDoc = (e: MouseEvent) => {
const t = e.target as Node const t = e.target as Node
@@ -148,6 +125,28 @@ export function NodeSwitcher() {
} }
}, [open]) }, [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 ( return (
<div className="relative"> <div className="relative">
<button <button