Require Effect Cleanup and Race-Safe Data Fetching in React
Flags React effects that leave listeners, timers, or sockets running after unmount, and fetches that let a stale response overwrite a newer one.
What it flags
import { useEffect, useState } from 'react';
import { fetchJson } from '@/lib/fetch-json';
import { ResultList } from './ResultList';
import type { SearchResult } from './types';
export function SearchResults({ query }: { query: string }) {
const [results, setResults] = useState<SearchResult[]>([]);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
// An older, slower response can overwrite results for the current query
fetchJson<SearchResult[]>(`/api/search?q=${encodeURIComponent(query)}`)
.then((data) => {
setResults(data);
setError(null);
})
.catch((err: Error) => setError(err));
}, [query]);
return <ResultList results={results} error={error} />;
}import { useEffect, useState } from 'react';
import { fetchJson } from '@/lib/fetch-json';
import { ResultList } from './ResultList';
import type { SearchResult } from './types';
export function SearchResults({ query }: { query: string }) {
const [results, setResults] = useState<SearchResult[]>([]);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
const url = `/api/search?q=${encodeURIComponent(query)}`;
fetchJson<SearchResult[]>(url, { signal: controller.signal })
.then((data) => {
setResults(data);
setError(null);
})
.catch((err: Error) => {
if (!controller.signal.aborted) setError(err);
});
return () => controller.abort();
}, [query]);
return <ResultList results={results} error={error} />;
}Why it matters
An effect without cleanup keeps working after its component unmounts or its dependencies change: intervals keep firing, sockets stay open, and window listeners stack up each time a user returns to the page. Requests started by an effect can resolve out of order, so a slow response for an earlier search term or route replaces the result for the current one. Strict Mode runs an extra setup and cleanup cycle in development to expose missing cleanup, but race conditions depend on network timing and rarely show up in a quick manual test.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Require Effect Cleanup and Race-Safe Data Fetching in React Applies to `**/*.tsx`, `**/*.jsx`, `**/use*.ts`, `**/use*.js`. Review React effects for missing cleanup and for async results that can arrive after they are stale. Flag changes that: - Call addEventListener, setInterval, setTimeout, a store or socket subscribe, or an observer such as ResizeObserver inside useEffect or useLayoutEffect without returning a cleanup that removes, clears, unsubscribes, or disconnects it. - Open a WebSocket, EventSource, or BroadcastChannel in an effect without closing it in cleanup. - Remove a listener with a different function reference than the one added, such as a new inline arrow function, so the original listener stays attached. - Fetch in an effect and set state from the response without an AbortController or an ignore flag, when the effect depends on values that change, such as an ID, search term, or route param. - Set error state when a request fails because the cleanup aborted it, which shows a failure message for a request the user replaced. - Await a request in an effect and then navigate, show a toast, or call a parent callback without checking that the effect is still current, which can redirect a user who already left the view. Do not flag: - Effects that leave nothing running, such as setting document.title or focusing an element. - Data fetching through a library or framework that handles cancellation and stale responses, such as TanStack Query, SWR, or a route loader. - Fetches in effects with an empty dependency array that only set state, where no newer request can race the first one.
Checks files matching **/*.tsx, **/*.jsx, **/use*.ts, **/use*.js.
More React agents
See all React code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.