skills/ahonn/dotfiles/no-useeffect

no-useeffect

Installation
SKILL.md

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 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
Weekly Installs
2
Repository
ahonn/dotfiles
GitHub Stars
61
First Seen
Mar 27, 2026