# No Use Effect

> Prevent unnecessary React `useEffect` usage by steering code toward derived state, event handlers, memoization, `key`-based resets, `useSyncExternalStore`, and framework or query-library data APIs. Use when writing or reviewing React/Next.js components, refactoring effect-heavy code, or when a change introduces `useEffect`, `useLayoutEffect`, or dependency-array churn. Triggers on: "avoid useEffect", "remove useEffect", "you might not need an effect", "effect dependencies", "derived state", "React side effects".

- Skill: `caidanw/no-use-effect` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add caidanw/no-use-effect`
- Raw SKILL.md: https://api.skillmd.com/api/skills/caidanw/no-use-effect/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: caidanw (https://skillmd.com/u/caidanw)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/caidanw/no-use-effect

---


# No useEffect

Treat raw `useEffect` as a last resort, not a default tool. Most component logic belongs in
render, event handlers, framework data APIs, or narrower React primitives.

This skill is based primarily on the React docs (`You Might Not Need an Effect`,
`Synchronizing with Effects`, `Removing Effect Dependencies`), with supporting guidance from
Dan Abramov, Kent C. Dodds, TkDodo, and team practices that ban or heavily restrict raw
`useEffect`.

## Core Rule

Before adding an Effect, answer this question:

"What external system am I synchronizing with, and why can't this be done during render,
in an event handler, or with a more specific primitive?"

If you cannot name the external system, do not write the Effect.

External systems include:
- browser APIs and DOM subscriptions
- timers, sockets, observers, media APIs
- third-party widgets and imperative libraries
- network synchronization that truly happens because the component is visible

Not external systems:
- derived values from props or state
- event-driven actions like submit, save, buy, delete, notify
- resetting local state because props changed
- chaining state updates to trigger more state updates
- passing data upward just because a child rendered

## Replacement Patterns

### 1. Derive during render

Do not mirror props or state into more state.

```tsx
// Bad
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// Good
const fullName = `${firstName} ${lastName}`;
```

Use this for filtered lists, labels, counts, booleans, selected objects, and other values that
can be computed from current inputs.

### 2. Memoize expensive pure work

If the calculation is slow, cache it with `useMemo`. Do not use an Effect that immediately
calls `setState` with a computed value.

```tsx
const visibleTodos = useMemo(() => getVisibleTodos(todos, filter), [todos, filter]);
```

First ask whether the work is actually expensive. If not, calculate it directly during render.

### 3. Put user actions in event handlers

If the code runs because the user clicked, submitted, selected, dragged, or typed, keep it in
the corresponding handler.

```tsx
// Bad
useEffect(() => {
  if (shouldSubmit) {
    void saveForm(form);
  }
}, [shouldSubmit, form]);

// Good
async function handleSubmit() {
  await saveForm(form);
}
```

Never build "set flag -> Effect notices flag -> Effect does the real work" flows unless the
flag is synchronizing with a real external system.

### 4. Reset identity with `key` when you want a fresh instance

If a component should become a brand-new instance when some identity changes, give it a
different `key`. React will unmount the old subtree and mount a new one, which resets all local
state below that point without an Effect.

```tsx
// Bad
function Profile({ userId }: { userId: string }) {
  const [comment, setComment] = useState("");

  useEffect(() => {
    setComment("");
  }, [userId]);

  return <input value={comment} onChange={(e) => setComment(e.target.value)} />;
}

// Good
function ProfilePage({ userId }: { userId: string }) {
  return <Profile key={userId} userId={userId} />;
}

function Profile({ userId }: { userId: string }) {
  const [comment, setComment] = useState("");
  return <input value={comment} onChange={(e) => setComment(e.target.value)} />;
}
```

Use this when the identity really changed: user, document, chat room, wizard step, or form
instance. Do not use it when you only want to tweak one field, because remounting resets the
entire subtree.

### 5. Lift state or control the component

If an Effect exists only to notify a parent after local state changed, the data model is usually
wrong. Prefer lifting state up or updating both sides in the same event.

```tsx
// Bad
function Toggle({ onChange }: { onChange: (next: boolean) => void }) {
  const [isOn, setIsOn] = useState(false);

  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange]);

  return <button onClick={() => setIsOn(!isOn)}>{isOn ? "On" : "Off"}</button>;
}

// Good
function Toggle({ isOn, onChange }: { isOn: boolean; onChange: (next: boolean) => void }) {
  return <button onClick={() => onChange(!isOn)}>{isOn ? "On" : "Off"}</button>;
}
```

### 6. Use `useSyncExternalStore` for external subscriptions

Use `useSyncExternalStore` when the source of truth lives outside React and changes over time.
This is React's built-in primitive for subscribing to external stores.

Good fits:
- browser APIs with a current value plus subscription events
- third-party or app-level stores outside React state
- reusable hooks like `useOnlineStatus()` or `useMediaQuery()`

The shape is:

```tsx
const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
```

Where:
- `subscribe` tells React how to listen for changes and return cleanup
- `getSnapshot` reads the current value from the external store
- `getServerSnapshot` provides the initial server and hydration value when SSR is involved

```tsx
// Bad
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function updateStatus() {
      setIsOnline(navigator.onLine);
    }

    updateStatus();
    window.addEventListener("online", updateStatus);
    window.addEventListener("offline", updateStatus);

    return () => {
      window.removeEventListener("online", updateStatus);
      window.removeEventListener("offline", updateStatus);
    };
  }, []);

  return isOnline;
}

// Good
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
```

Prefer this over hand-rolled subscription Effects in components.

Important caveats from the React docs:
- `getSnapshot` must return a stable cached value unless the store actually changed
- if `subscribe` changes identity every render, React will resubscribe every render
- if you support SSR, `getServerSnapshot` must match between server render and hydration

Do not use this for:
- normal component state that should live in `useState`, `useReducer`, or context
- derived values you can calculate during render
- user actions handled by event handlers
- one-time fetching
- general side effects that are not subscriptions

### 7. Fetch through the framework or a data library

Avoid raw fetch-in-Effect code in app components when a framework loader or query library is
available.

Preferred order:
1. framework data APIs (`Next.js`, `React Router`, Remix, server components, route loaders)
2. data libraries (`TanStack Query`, `SWR`)
3. a focused custom hook
4. raw `useEffect` only if none of the above fit

If you must fetch in an Effect, handle cleanup so stale responses are ignored.

```tsx
// Bad
useEffect(() => {
  void fetchResults(query).then(setResults);
}, [query]);

// Good
useEffect(() => {
  let ignore = false;

  void fetchResults(query).then((next) => {
    if (!ignore) {
      setResults(next);
    }
  });

  return () => {
    ignore = true;
  };
}, [query]);
```

### 8. Encapsulate unavoidable imperative synchronization

Sometimes an Effect is correct: sockets, observers, widgets, media controls, focus management,
layout measurement, or DOM APIs that must stay in sync with rendered output.

When that happens:
- keep the Effect tiny and specific
- name the external system in a comment or hook name
- include full cleanup
- prefer a custom hook over open-coded Effect logic in many components
- consider `useLayoutEffect` only for pre-paint measurement or visual correctness

```tsx
// Bad
function ChatRoom({ roomId }: { roomId: string }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
  }, [roomId]);
}

// Good
function useChatConnection(roomId: string) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    return () => {
      connection.disconnect();
    };
  }, [roomId]);
}
```

If your team bans raw `useEffect`, put these escape hatches behind approved hooks instead of
sprinkling Effects through feature code.

## Anti-Patterns

Treat these as red flags during authoring or review:
- `useEffect(() => setX(...), [...])` for derived state
- Effect only exists to call a function after a button click
- effect chains where one state update triggers another Effect and another render
- dependency arrays fought with `eslint-disable`, refs-as-flags, or intentionally stale closures
- object or function dependencies recreated every render without need
- child component fetching data and pushing it upward in an Effect
- mount-only initialization that should live in module scope, entrypoints, or framework boot code

## Review Checklist

Before accepting a new Effect, verify all of these:
- [ ] The code synchronizes with a real external system
- [ ] Derived render data, memoization, events, `key`, lifted state, and `useSyncExternalStore`
      were all considered first
- [ ] The dependency list is honest and complete
- [ ] Setup and cleanup are symmetrical
- [ ] The Effect is idempotent under Strict Mode re-mounting
- [ ] Data fetching is not bypassing an existing framework or query abstraction
- [ ] The logic is small enough that a custom hook or helper name explains its purpose

## Team Policy

For teams trying to ban or heavily restrict raw `useEffect`, use this policy:

1. Do not introduce `useEffect` for derived state, events, or resets.
2. Prefer render-time derivation, event handlers, `key`, `useMemo`, and `useSyncExternalStore`.
3. Route data loading through the framework or an approved query library.
4. If an Effect is still necessary, hide it behind a narrowly named custom hook or document the
   external system and cleanup explicitly in review.
5. Never silence `react-hooks/exhaustive-deps` to "make it work".

## References

For source material and further reading, see `references/sources.md`.

