Detect, replace, and lint-ban direct `useEffect` in React components and hooks. Prefer declarative replacements for derived state, fetching, event reactions, resets, and external sync; add ESLint or agent rules for a no-direct-useEffect policy. Use when writing, refactoring, reviewing, or migrating React code that imports or calls `useEffect`, or when an agent reaches for an effect by default. Do not use for ordinary React work with no effect smell, non-React code, or legitimate effect architecture outside React.
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Default stance: do not import or call useEffect directly in React components.
Treat effects as an escape hatch for synchronizing with external systems.
useEffect imports and calls.react-doctor
CLI diff scan when available, plus the smallest runtime check for the changed
path. Fix failures and rerun before completion.Highest applicable layer wins. Intent → layer:
| Intent | Prefer |
|---|---|
| derive from props/state | render-time calculation |
| fetch / server state | server, loader, or TanStack Query / SWR / Relay / Apollo |
| user action | event handler, form action, or mutation |
| reset on identity change | keyed component boundary |
| read external store | useSyncExternalStore |
| other external sync | reviewed domain-specific hook |
useSyncExternalStore or a reviewed domain-specific external-system hookCommon keyed reset:
function ProfilePage({ userId }: { userId: string }) {
return <Profile key={userId} userId={userId} />;
}If none fit, stop and explain why the code truly needs an effect instead of
adding direct useEffect.
Prefer an existing repo integration hook. Otherwise add a domain-specific hook that names the external system, owns setup/cleanup, and lists every reactive input in the dependency array. See the external-system replacement.
Do not expose a generic effect callback or dependency list to callers.
Do not suppress react-hooks/exhaustive-deps.
A mount-only empty dependency list is allowed only when setup reads no reactive
props, state, or closure values.
Prefer useSyncExternalStore for stores or browser values that change over time.
Never use an exception hook to fetch server state, copy props into state, or
relay user actions.
Prefer the repo's existing ESLint shape. Usual rules: no-restricted-imports
against useEffect from react, plus no-restricted-syntax (or a custom rule)
for React.useEffect(...). Allow only named, reviewed external-integration hook
files as exceptions. Preserve any existing local lint convention.
{
"no-restricted-imports": ["error", {
name: "react",
importNames: ["useEffect"],
message: "Use a declarative replacement or reviewed external-integration hook.",
}],
"no-restricted-syntax": ["error", {
selector: "CallExpression[callee.object.name='React'][callee.property.name='useEffect']",
message: "Do not call React.useEffect directly.",
}],
}In reviews, treat new direct useEffect as a finding unless the diff also adds
a clear, reviewed exception. Ask for a replacement plan, not dependency-array
tuning. Provenance notes: references/upstream.md.
useLayoutEffect, framework lifecycle APIs, and non-React effect systems alone unless requested.Core sources and the broader alternatives bibliography live in references/alternatives.md.