No Direct useEffect

SkillDev tools

Enforce a strict ban on direct useEffect calls in React code. Replace with derived state, event handlers, data-fetching libraries, useMountEffect, or key-based remounting. Use when: (1) Writing new React components, (2) Reviewing React code for effect usage, (3) Refactoring existing useEffect calls, (4) AI agent is about to add useEffect. Triggers on: useEffect, effect hook, side effect, sync state, dependency array, infinite loop, race condition, effect cleanup.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the No Direct useEffect skill

What this skill tells your AI

The instructions your AI receives, as published by ahonn/dotfiles in .claude/skills/no-useeffect/SKILL.md and read by ahel’s review.

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 update chains
  • Race conditions: effect-based fetching without cancellation
  • Debugging pain: "why did this run?" has no clear entrypoint like a handler does
  • useMountEffect failures are binary and loud; useEffect failures degrade gradually and show up as flaky behavior

Signals

GitHub stars
62
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
no-useeffect
Source
github.com/ahonn/dotfiles