# Mastering useEffect in React: a practical guide



useEffect is one of React’s most important hooks for handling side effects in function components: data fetching, subscriptions, timers, DOM mutations after render, and more. This article explains how it works, common patterns, pitfalls, and best practices, with practical examples and TypeScript notes.

---

## What is useEffect?

`useEffect` lets you perform side effects after a component renders. Unlike render logic, effects run after React has updated the DOM. The hook signature:

```js
useEffect(effect: () => void | (() => void | undefined), deps?: any[])
```

- `effect` is a function that optionally returns a cleanup function.
- `deps` is an optional dependency array that controls when the effect runs.

---

## Basic behavior

- No dependency array: `useEffect(() => { ... })` runs after every render.
- Empty array: `useEffect(() => { ... }, [])` runs once after the initial mount.
- With dependencies: `useEffect(() => { ... }, [a, b])` runs after mount and whenever `a` or `b` changes (by reference equality).
- Cleanup: If `effect` returns a function, React calls it before the next effect run and on unmount.

Example — run once on mount:

```jsx
useEffect(() => {
  console.log('mounted');
}, []);
```

Example — cleanup on unmount:

```jsx
useEffect(() => {
  const id = setInterval(() => console.log('tick'), 1000);
  return () => clearInterval(id); // cleanup
}, []);
```

---

## Common patterns

1. Data fetching (with cancellation)
```jsx
useEffect(() => {
  const controller = new AbortController();
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(setData)
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err);
    });
  return () => controller.abort(); // cancel fetch on unmount or dep change
}, [/* dependencies */]);
```

2. Avoid `async` directly on effect:
```jsx
useEffect(() => {
  let canceled = false;
  async function load() {
    const data = await fetchData();
    if (!canceled) setData(data);
  }
  load();
  return () => { canceled = true; };
}, [/* deps */]);
```

3. Event listeners
```jsx
useEffect(() => {
  function onResize() { /* ... */ }
  window.addEventListener('resize', onResize);
  return () => window.removeEventListener('resize', onResize);
}, []);
```

4. Subscriptions (WebSocket, RxJS)
```jsx
useEffect(() => {
  const sub = observable.subscribe(value => setValue(value));
  return () => sub.unsubscribe();
}, [observable]);
```

5. Timers
```jsx
useEffect(() => {
  const id = setTimeout(() => /* ... */, 500);
  return () => clearTimeout(id);
}, [/* deps */]);
```

---

## Dependency array: rules and gotchas

- Always include all external values referenced inside the effect (props, state, functions) in the dependency array.
- Values are compared by reference. Objects, arrays, functions that are re-created each render will re-trigger the effect.
- To avoid unnecessary re-runs:
  - Use `useCallback` for stable function references.
  - Use `useMemo` for expensive objects/arrays.
  - Move logic outside component if it doesn’t depend on props/state.
- ESLint plugin react-hooks (`exhaustive-deps`) will warn when dependencies are missing. Prefer following it; if you intentionally omit a dependency, document why with a comment.

Example pitfall — inline object triggers:
```jsx
useEffect(() => {
  // This runs every render because options is recreated each render
  doSomething({ page: 1 });
}, [{ page: 1 }]);
```
Fix by memoizing options:
```jsx
const options = useMemo(() => ({ page: 1 }), []);
useEffect(() => doSomething(options), [options]);
```

---

## Cleanup semantics

- Cleanup runs:
  - Before the next invocation of the effect (except the first run).
  - When the component unmounts.
- This makes effects perfect for managing subscriptions, timers, and external resources.

Edge behavior: if an effect schedules side-effects later (e.g., setting state in a callback), ensure cleanup prevents acting on an unmounted component.

---

## Async effects and cancellation

- You cannot make the effect callback `async` (it would return a Promise, which React treats as a non-cleanup value). Use an inner async function or handle promises manually.
- For fetches: prefer `AbortController` or a mounted flag pattern to ignore stale responses.

Example with AbortController:
```jsx
useEffect(() => {
  const ac = new AbortController();
  async function load() {
    try {
      const res = await fetch(url, { signal: ac.signal });
      const json = await res.json();
      setData(json);
    } catch (err) {
      if (err.name !== 'AbortError') console.error(err);
    }
  }
  load();
  return () => ac.abort();
}, [url]);
```

---

## Multiple effects vs single effect

- Prefer multiple focused effects rather than one giant effect handling many unrelated concerns. Separation improves readability and reduces accidental re-runs.

```jsx
useEffect(() => {
  // effect A
}, [a]);

useEffect(() => {
  // effect B
}, [b]);
```

---

## When to use useLayoutEffect

- `useLayoutEffect` runs synchronously after DOM mutations but before browser paint. Use it when you must measure DOM (e.g., reading layout and synchronously applying changes).
- On server-side rendering (SSR), `useLayoutEffect` emits a React warning. For universal apps, use `useEffect` unless you need synchronous layout reads or mute the warning conditionally.

---

## Common mistakes and how to avoid them

- Forgetting dependencies → stale closures and bugs. Use ESLint rule `react-hooks/exhaustive-deps`.
- Putting expensive logic in effects that run too often → cause performance issues. Memoize dependencies and move heavy work elsewhere.
- Not cleaning up subscriptions/timers → memory leaks or duplicate listeners.
- Setting state in an effect without proper dependency control → infinite loops.
- Relying on `[]` to run effects only once when they depend on props. If effect should react to prop changes, include those props in deps.

Checklist when writing useEffect:
- Which values does the effect read? Add them to deps.
- Does the effect create a subscription/timer/fetch? Return a cleanup that cancels it.
- Can any dependency be stabilized using useMemo/useCallback?
- Can logic be split into smaller effects?

---

## Testing effects

- With React Testing Library, use `waitFor`, `findBy*`, or `act` to assert changes caused by effects.
- Mock network requests (fetch/xhr) and timers (jest.useFakeTimers()) when testing timers or time-dependent logic.
- Example: wait for elements that rely on fetched data using `await findByText(...)`.

---

## TypeScript tips

Effect signature in TS:
```ts
useEffect(() => {
  // ...
  return () => { /* cleanup */ };
}, [dep1, dep2]);
```
No special typing is normally needed. If you use refs with mutable values, annotate them:
```ts
const mountedRef = useRef<boolean>(false);
useEffect(() => {
  mountedRef.current = true;
  return () => { mountedRef.current = false };
}, []);
```

---

## Useful custom hooks (patterns)

1. useInterval
```jsx
function useInterval(callback, delay) {
  const savedCallback = useRef();
  useEffect(() => { savedCallback.current = callback; }, [callback]);
  useEffect(() => {
    if (delay == null) return;
    const id = setInterval(() => savedCallback.current(), delay);
    return () => clearInterval(id);
  }, [delay]);
}
```

2. useEventListener
```jsx
function useEventListener(target, type, handler) {
  const saved = useRef(handler);
  useEffect(() => { saved.current = handler; }, [handler]);
  useEffect(() => {
    const t = target && 'addEventListener' in target ? target : window;
    const listener = e => saved.current(e);
    t.addEventListener(type, listener);
    return () => t.removeEventListener(type, listener);
  }, [target, type]);
}
```

3. useFetch (simple)
```jsx
function useFetch(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    const ac = new AbortController();
    fetch(url, { signal: ac.signal })
      .then(r => r.json())
      .then(setData)
      .catch(() => {});
    return () => ac.abort();
  }, [url]);
  return data;
}
```

---

## Migrating from class lifecycle

- componentDidMount -> useEffect(..., [])
- componentDidUpdate -> useEffect(..., [deps])
- componentWillUnmount -> cleanup function returned by useEffect

Example:
```jsx
// class
componentDidMount() { this.timer = setInterval(...); }
componentWillUnmount() { clearInterval(this.timer); }

// function
useEffect(() => {
  const t = setInterval(...);
  return () => clearInterval(t);
}, []);
```

---

## Server-side rendering (SSR) considerations

- Effects (useEffect) do not run during SSR — they only run on the client — which is good for client-only side effects (e.g., subscriptions, fetches that should only run in browser).
- If behavior must run on the server as well, perform the work during rendering or in data fetching logic outside `useEffect`.
- `useLayoutEffect` can warn under SSR; detect environment or use `useIsomorphicLayoutEffect` pattern that maps to `useEffect` on server.

---

## Best practices summary

- Keep effects focused and small.
- Always clean up subscriptions and timers.
- Follow exhaustive-deps; stabilize dependencies with memoization when appropriate.
- Avoid async directly on the effect callback; use inner async functions and cancellation.
- Use multiple effects for different concerns.
- When reading layout or performing synchronous DOM updates, prefer `useLayoutEffect`.
- Test effects with appropriate asynchronous utilities.

---

## Quick reference: common examples

1. Fetch with cancel
```jsx
useEffect(() => {
  const ac = new AbortController();
  (async () => {
    try {
      const res = await fetch(url, { signal: ac.signal });
      setData(await res.json());
    } catch (e) {
      if (e.name !== 'AbortError') console.error(e);
    }
  })();
  return () => ac.abort();
}, [url]);
```

2. Debounced effect
```jsx
useEffect(() => {
  const id = setTimeout(() => doSearch(query), 300);
  return () => clearTimeout(id);
}, [query]);
```

3. Subscribe to event
```jsx
useEffect(() => {
  window.addEventListener('mousemove', onMove);
  return () => window.removeEventListener('mousemove', onMove);
}, []);
```

---

useEffect gives functional components the full power to interact with external systems and the DOM, but it requires careful dependency management and cleanup. With the patterns above you can write predictable, efficient, and bug-resistant effects.
