no-useeffect
No Direct useEffect
Rule: Never call useEffect directly. All 5 replacement patterns below cover every legitimate use case.
useMountEffect — The Only Allowed Effect Wrapper
export function useMountEffect(effect: () => void | (() => void)) {
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(effect, []);
}
Use useMountEffect only for one-time external system sync (DOM, third-party widgets, browser APIs). For anything else, use Rules 1-3 or 5.
Rule 1: Derive State, Don't Sync It
Smell: useEffect(() => setX(deriveFromY(y)), [y]) or state that mirrors other state/props.
// BAD: two render cycles
const [filtered, setFiltered] = useState([]);
useEffect(() => {
setFiltered(products.filter((p) => p.inStock));
}, [products]);
// GOOD: compute inline
const filtered = products.filter((p) => p.inStock);
// BAD: effect chain creates loop risk
useEffect(() => { setTax(subtotal * 0.1); }, [subtotal]);
useEffect(() => { setTotal(subtotal + tax); }, [subtotal, tax]);
// GOOD: plain derivation
const tax = subtotal * 0.1;
const total = subtotal + tax;
Rule 2: Use Data-Fetching Libraries
Smell: useEffect with fetch() + setState(), or re-implementing caching/cancellation/retries.
// BAD: race condition when productId changes fast
useEffect(() => {
fetchProduct(productId).then(setProduct);
}, [productId]);
// GOOD: library handles cancellation, caching, staleness
const { data: product } = useQuery(
['product', productId],
() => fetchProduct(productId)
);
Acceptable libraries: TanStack Query, SWR, Apollo, RTK Query, tRPC, Relay, or framework loaders (Next.js/Remix/Expo Router).
Rule 3: Event Handlers, Not Effects
Smell: state used as a flag so an effect can do the real work ("set flag → effect runs → reset flag").
// BAD: effect as action relay
const [liked, setLiked] = useState(false);
useEffect(() => {
if (liked) { postLike(); setLiked(false); }
}, [liked]);
// GOOD: direct handler
<button onClick={() => postLike()}>Like</button>
If a user action triggers the work, do it in the handler.
Rule 4: useMountEffect for One-Time External Sync
Valid uses: DOM focus/scroll, third-party widget init, browser API subscriptions.
// BAD: guard inside effect
useEffect(() => {
if (!isLoading) playVideo();
}, [isLoading]);
// GOOD: conditional mounting — mount only when preconditions met
function VideoPlayerWrapper({ isLoading }) {
if (isLoading) return <LoadingScreen />;
return <VideoPlayer />;
}
function VideoPlayer() {
useMountEffect(() => playVideo());
}
Parent owns orchestration; child assumes preconditions are met.
Rule 5: Reset with key, Not Dependency Choreography
Smell: effect whose only job is to reset local state when an ID/prop changes.
// BAD: manual reset on ID change
useEffect(() => { loadVideo(videoId); }, [videoId]);
// GOOD: key forces clean remount
<VideoPlayer key={videoId} videoId={videoId} />
function VideoPlayer({ videoId }) {
useMountEffect(() => loadVideo(videoId));
}
If the requirement is "start fresh when X changes", use React's remount semantics via key.
Quick Decision Tree
Need to compute a value from state/props?
→ Rule 1: Derive inline
Need to fetch data?
→ Rule 2: Use a query library
Responding to a user action?
→ Rule 3: Event handler
One-time setup on mount (DOM, external system)?
→ Rule 4: useMountEffect
Need to reset when an ID/entity changes?
→ Rule 5: key prop on parent
None of the above?
→ Rethink. The answer is almost never useEffect.
Why This Rule Exists
- Brittleness: dependency arrays hide coupling; unrelated refactors break effects silently
- Infinite loops:
state update → render → effect → state updatechains - Race conditions: effect-based fetching without cancellation
- Debugging pain: "why did this run?" has no clear entrypoint like a handler does
useMountEffectfailures are binary and loud;useEffectfailures degrade gradually and show up as flaky behavior