Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/webapp/app/routes/resources.incidents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function IncidentStatusPanel({ isCollapsed = false }: { isCollapsed?: boo
if (fetcher.state === "idle") {
fetcher.load("/resources/incidents");
}
}, [fetcher]);
}, []);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Stale closure causes fetcher.state check to always read 'idle'

By removing fetcher from the useCallback dependency array, the fetchIncidents callback captures the fetcher object from the initial render. Since Remix's useFetcher returns a new object on each render, the captured fetcher.state will always be "idle" (the initial state), making the guard if (fetcher.state === "idle") ineffective.

Click to expand

How the bug is triggered

The callback is created once with an empty dependency array:

const fetchIncidents = useCallback(() => {
  if (fetcher.state === "idle") {  // Always sees "idle" from first render
    fetcher.load("/resources/incidents");
  }
}, []);  // fetcher not in deps

When the interval fires every 60 seconds, fetcher.state in the closure always reads "idle" regardless of whether a request is currently in progress.

Actual vs Expected

  • Expected: Skip calling fetcher.load() if a request is already in progress
  • Actual: The guard is bypassed because fetcher.state always reads as "idle", so fetcher.load() is called even if a request is pending

Impact

This could cause unnecessary duplicate network requests if the previous request hasn't completed within 60 seconds. In practice, the impact is limited because:

  1. The 60-second interval makes overlapping requests unlikely
  2. fetcher.load cancels previous pending requests

A proper fix would use a ref to track the fetcher state or use fetcher.state directly outside the callback.

Recommendation: Use a ref to track loading state, or check fetcher.state outside the callback and pass it as a parameter. Alternative: Since fetcher.load is stable and cancels previous requests, the guard could be removed entirely if duplicate requests are acceptable.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


useEffect(() => {
if (!isManagedCloud) return;
Expand Down
Loading