fix(react): remove four setState-in-effect cascades flagged by react-hooks v5
The new react-hooks lint rule "Calling setState synchronously within an effect can trigger cascading renders" flagged real anti-patterns in four spots. Refactored each per the rule's intent (derive during render, or use useSyncExternalStore for external subscriptions). 1. hooks/useMediaQuery.ts — replaced the useState + useEffect pair with useSyncExternalStore. That's the canonical React hook for subscribing to external stores (matchMedia in this case) without mirroring into local state via an effect. Snapshot/getServerSnapshot pair preserves the SSR-safe behaviour. 2. components/network/nodes/DeviceNode.tsx — the prop-sync useEffect that copied nodeData.label into labelValue was redundant. labelValue is the EDIT BUFFER; while not editing, the displayed span now reads nodeData.label directly. The buffer is initialized only when an edit session starts (onDoubleClick). 3. components/network/nodes/GroupNode.tsx — same pattern, same fix. 4. components/dashboard/TicketQueue.tsx — the setTickets([]) + setLoading(true) + fetchTickets() chain in the effect was the cascade. Pushed those writes inside fetchTickets (after the function boundary, so they batch with the eventual setTickets(result)). Added a request-id ref so a slow first response can't overwrite a fast second one. Frontend lint: 20 errors → 0 errors. tsc -b clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -62,10 +62,9 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) {
|
||||
}
|
||||
}, [editing])
|
||||
|
||||
// Sync if data.label changes externally (e.g. undo/redo)
|
||||
useEffect(() => {
|
||||
if (!editing) setLabelValue(nodeData.label ?? '')
|
||||
}, [nodeData.label, editing])
|
||||
// While not editing, the displayed label is derived directly from
|
||||
// nodeData.label — no effect-driven sync needed. labelValue holds the
|
||||
// edit buffer only and is reset when an edit session starts.
|
||||
|
||||
const hasTooltipContent = props.hostname || props.ip || props.vendor || props.model || props.role || props.notes
|
||||
|
||||
@@ -127,10 +126,11 @@ function DeviceNodeComponent({ id, data, selected, width, height }: NodeProps) {
|
||||
className="max-w-[88%] cursor-default text-center font-medium leading-tight text-primary line-clamp-2"
|
||||
onDoubleClick={e => {
|
||||
e.stopPropagation()
|
||||
setLabelValue(nodeData.label ?? '')
|
||||
setEditing(true)
|
||||
}}
|
||||
>
|
||||
{labelValue}
|
||||
{nodeData.label ?? ''}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
|
||||
@@ -22,10 +22,9 @@ const GroupNodeComponent = ({ data, selected, id }: NodeProps) => {
|
||||
if (editing) inputRef.current?.focus()
|
||||
}, [editing])
|
||||
|
||||
// Sync if external data.label changes
|
||||
useEffect(() => {
|
||||
if (!editing) setLabelValue(groupData.label ?? '')
|
||||
}, [groupData.label, editing])
|
||||
// While not editing, the displayed label is derived directly from
|
||||
// groupData.label — no effect-driven sync needed. labelValue holds the
|
||||
// edit buffer only and is reset when an edit session starts.
|
||||
|
||||
const handleLabelCommit = () => {
|
||||
setEditing(false)
|
||||
@@ -69,9 +68,12 @@ const GroupNodeComponent = ({ data, selected, id }: NodeProps) => {
|
||||
<span
|
||||
className="inline-block rounded-sm bg-card/90 px-1.5 py-0.5 text-[11px] font-semibold cursor-text select-none tracking-wide"
|
||||
style={{ color }}
|
||||
onDoubleClick={() => setEditing(true)}
|
||||
onDoubleClick={() => {
|
||||
setLabelValue(groupData.label ?? '')
|
||||
setEditing(true)
|
||||
}}
|
||||
>
|
||||
{labelValue || groupData.groupType}
|
||||
{(groupData.label ?? '') || groupData.groupType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user