no-use-effect
ALWAYS ACTIVE. Enforces a strict no-direct-useEffect rule in React and React Native code. Never write useEffect directly — use derived state, event handlers, useMemo, useSyncExternalStore, data-fetching libraries, key-based resets, or useMountEffect instead. Triggers on all React component code: writing new components, editing existing ones, reviewing code, fixing bugs, or refactoring. Also triggers when user mentions useEffect, side effects, data fetching, state sync, or component lifecycle.
Savant verdict: Pending analysis
Imported from the source hub; Savant's analysis is still running.
Live evaluation
Not evaluated yet. Workspaces can request a live evaluation.
Security assessment
Scan pending.
Structure
- No license declaredConfirm you may reuse this skill before importing it into your repository.
SKILL.md
---
name: no-use-effect
description: "ALWAYS ACTIVE. Enforces a strict no-direct-useEffect rule in React and React Native code. Never write useEffect directly — use derived state, event handlers, useMemo, useSyncExternalStore, data-fetching libraries, key-based resets, or useMountEffect instead. Triggers on all React component code: writing new components, editing existing ones, reviewing code, fixing bugs, or refactoring. Also triggers when user mentions useEffect, side effects, data fetching, state sync, or component lifecycle."
---
# No Direct useEffect
Never call `useEffect` directly. For the rare case of syncing with an external system on mount, use `useMountEffect()`.
```tsx
export function useMountEffect(effect: () => void | (() => void)) {
/* eslint-disable react-hooks/exhaustive-deps, no-restricted-syntax */
useEffect(effect, []);
}
```
The only place `useEffect` may appear directly is inside reusable custom hooks (like `useMountEffect` itself, or a `useData` hook when no fetching library is available). Components must never import or call `useEffect`.
## The 6 Rules
### Rule 1: Derive state, do not sync it
If you can calculate it from existing state/props, compute it inline.
```tsx
// BAD: two render cycles
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
setFilteredProducts(products.filter((p) => p.inStock));
}, [products]);
// GOOD: one render
const filteredProducts = products.filter((p) => p.inStock);
```
For expensive calculations, use `useMemo`:
```tsx
const visibleTodos = useMemo(
() => getFilteredTodos(todos, filter),
[todos, filter]
);
```
**Smell test:** You're about to write `useEffect(() => setX(f(y)), [y])` or state that only mirrors other state/props.
### Rule 2: Use data-fetching libraries
Effect-based fetching creates race conditions and reinvents caching.
```tsx
// BAD: race condition
useEffect(() => {
fetchProduct(productId).then(setProduct);
}, [productId]);
// GOOD: library handles cancellation/caching/staleness
const { data: product } = useQuery(
['product', productId],
() => fetchProduct(productId)
);
```
In React 19+, use the `use()` hook with `<Suspense>` to unwrap promises without `useEffect` or `useState`:
```tsx
// GOOD: use() + Suspense (React 19+)
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}
```
**Smell test:** Your effect does `fetch()` then `setState()`, or you're reimplementing caching/retries/cancellation.
### Rule 3: Event handlers, not effects
If a user action triggers it, do the work in the handler. Procedural logic (validate → transform → submit) belongs in handlers. Reconstructing procedural flow through ref+effect chains is a sign the logic was event-driven from the start.
```tsx
// BAD: flag-relay through effect
const [liked, setLiked] = useState(false);
useEffect(() => {
if (liked) { postLike(); setLiked(false); }
}, [liked]);
// GOOD: direct
<button onClick={() => postLike()}>Like</button>
```
For shared logic between handlers, extract a function — not an effect:
```tsx
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name}`);
}
```
**Smell test:** State used as a flag so an effect can do the real action, or "set flag -> effect runs -> reset flag" mechanics.
### Rule 4: useMountEffect for one-time external sync
Only for true mount-time external system setup: DOM integration, third-party widgets, browser API subscriptions.
```tsx
// BAD: guard inside effect
useEffect(() => {
if (!isLoading) playVideo();
}, [isLoading]);
// GOOD: conditional mounting
function VideoPlayerWrapper({ isLoading }) {
if (isLoading) return <LoadingScreen />;
return <VideoPlayer />;
}
function VideoPlayer() {
useMountEffect(() => playVideo());
}
```
**Smell test:** Behavior is naturally "setup on mount, cleanup on unmount" with an external system.
### Rule 5: Reset with key, not dependency choreography
If you need "start fresh when ID changes," use React's remount semantics.
```tsx
// BAD: effect resets on ID change
useEffect(() => { loadVideo(videoId); }, [videoId]);
// GOOD: key forces clean remount
<VideoPlayer key={videoId} videoId={videoId} />
function VideoPlayer({ videoId }) {
useMountEffect(() => { loadVideo(videoId); });
}
```
This also applies to resetting form state, clearing selections, etc. Use `key` on the component instead of an effect that sets state to initial values.
**Smell test:** Effect's only job is to reset local state when an ID/prop changes.
### Rule 6: Never patch a broken effect with a ref
If you need a `useRef` to stop an effect from double-firing, looping, or accessing stale state, the effect itself is the problem. Eliminate the root effect — don't bandage it with a ref.
```tsx
// BAD: ref guard hides the real problem
const hasRun = useRef(false);
useEffect(() => {
if (hasRun.current) return;
hasRun.current = true;
showWelcomeToast(userId);
}, [userId]);
// GOOD: useMountEffect for true one-time setup
useMountEffect(() => {
showWelcomeToast(userId);
});
```
```tsx
// BAD: ref to capture latest callback, dodging the dependency array
const onMessageRef = useRef(onMessage);
onMessageRef.current = onMessage;
useEffect(() => {
const conn = createConnection(roomId);
conn.on('message', (msg) => onMessageRef.current(msg));
return () => conn.disconnect();
}, [roomId]);
// GOOD: useEffectEvent (experimental) captures latest values automatically
const onMsg = useEffectEvent((msg) => {
onMessage(msg);
});
useEffect(() => {
const conn = createConnection(roomId);
conn.on('message', onMsg);
return () => conn.disconnect();
}, [roomId]);
```
The React docs explicitly warn: *"The right question isn't 'how to run an Effect once', but 'how to fix my Effect so that it works after remounting'."*
**Smell test:** You're adding `hasRun.current`, `isMounted.current`, or a ref whose sole purpose is controlling when/if an effect runs. Or you're storing a callback in a ref to avoid listing it as a dependency.
## Additional Patterns
### Notifying parents about state changes
Do not use an effect to call `onChange` — update both in the event handler, or lift state up:
```tsx
// BAD
useEffect(() => { onChange(isOn); }, [isOn, onChange]);
// GOOD: update both during the event
function updateToggle(nextIsOn) {
setIsOn(nextIsOn);
onChange(nextIsOn);
}
```
### useEffectEvent for latest values (experimental)
When an effect needs to read the latest value of a prop/state without re-running when it changes, use `useEffectEvent` instead of a ref workaround:
```tsx
// BAD: ref to read latest theme without adding it as dependency
const themeRef = useRef(theme);
themeRef.current = theme;
useEffect(() => {
logVisit(url, themeRef.current);
}, [url]);
// GOOD: useEffectEvent reads latest values automatically
const onVisit = useEffectEvent(() => {
logVisit(url, theme);
});
useEffect(() => {
onVisit();
}, [url]);
```
Until `useEffectEvent` is stable, prefer moving logic to event handlers or `useMountEffect`. If neither fits, isolate the ref workaround in a custom hook — never in a component.
### Callback refs for DOM side effects
For DOM measurement or setup, a callback ref is more reliable than `useRef` + `useMountEffect` — it fires exactly when the node attaches or detaches:
```tsx
// BAD: ref.current may be null on first render
const ref = useRef(null);
useMountEffect(() => {
setHeight(ref.current.getBoundingClientRect().height);
});
return <div ref={ref}>Content</div>;
// GOOD: callback ref fires when node is attached
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return <div ref={measuredRef}>Content</div>;
```
In React 19+, callback refs support cleanup return values. For earlier versions, store cleanup logic in a ref inside a custom hook.
### Subscribing to external stores
Use `useSyncExternalStore` instead of manual subscription effects:
```tsx
const isOnline = useSyncExternalStore(
subscribe,
() => navigator.onLine,
() => true
);
```
### App initialization
Run once at module level, not in an effect:
```tsx
if (typeof window !== 'undefined') {
checkAuthToken();
loadDataFromLocalStorage();
}
```
### Chains of computations
Never chain effects that trigger each other. Derive values inline and batch state updates in the event handler:
```tsx
// BAD: 3 effects chaining state -> 3 extra renders
useEffect(() => { if (card?.gold) setGoldCardCount(c => c + 1); }, [card]);
useEffect(() => { if (goldCardCount > 3) { setRound(r => r + 1); setGoldCardCount(0); } }, [goldCardCount]);
useEffect(() => { if (round > 5) setIsGameOver(true); }, [round]);
// GOOD: derive + handle in event
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount < 3) setGoldCardCount(goldCardCount + 1);
else { setGoldCardCount(0); setRound(round + 1); }
}
}
```
## Decision Checklist
Before writing any effect, answer these questions:
1. **Can I compute it during render?** -> Derive it inline or `useMemo`
2. **Is it triggered by a user action?** -> Event handler
3. **Am I fetching data?** -> Data-fetching library (React Query, SWR, etc.) or `use()` + Suspense in React 19+
4. **Am I subscribing to an external store?** -> `useSyncExternalStore`
5. **Do I need to reset state when a prop changes?** -> `key` prop
6. **Is it true mount-time external system sync?** -> `useMountEffect`
7. **Am I using refs to control when/if an effect runs?** -> Remove the refs; move logic to an event handler, derive it, or use `useEffectEvent`
8. **Do I need a DOM side effect when a node mounts?** -> Callback ref
If none of the above apply and you still think you need `useEffect`, see [references/patterns.md](references/patterns.md) for detailed examples.
## Failure Modes
- **useMountEffect failures** are binary and loud: it ran once, or not at all
- **Direct useEffect failures** degrade gradually as flaky behavior, perf issues, or loops before a hard crash
Choose the bug that's easy to find.