React Cheatsheet

useEffect

Use this React reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Signature

import { useEffect } from 'react';

useEffect(
  () => {            // setup function
    // side effect here
    return () => {   // optional cleanup function
      // runs before next effect and on unmount
    };
  },
  [dep1, dep2]       // dependency array
);

Dependency Array Variants

// 1. No dependency array — runs after EVERY render
useEffect(() => {
  console.log('rendered');
});

// 2. Empty array — runs once after initial mount
useEffect(() => {
  console.log('mounted');
  return () => console.log('unmounted');
}, []);

// 3. With dependencies — runs after mount AND whenever deps change
useEffect(() => {
  console.log('userId changed:', userId);
}, [userId]);

// 4. Multiple dependencies
useEffect(() => {
  fetchData(userId, filter);
}, [userId, filter]);

ESLint rule react-hooks/exhaustive-deps warns if you forget a dependency. Every reactive value used inside the effect (props, state, functions defined in the component) must be in the array — or you'll get stale closures.

Common Use Cases

Data Fetching

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;  // prevent setting state after unmount

    setLoading(true);
    setError(null);

    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => { if (!cancelled) setUser(data); })
      .catch(err => { if (!cancelled) setError(err.message); })
      .finally(() => { if (!cancelled) setLoading(false); });

    return () => { cancelled = true; };
  }, [userId]);

  if (loading) return <p>Loading…</p>;
  if (error)   return <p>Error: {error}</p>;
  return <div>{user?.name}</div>;
}

AbortController (preferred over flag)

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/data/${id}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setData)
    .catch(err => {
      if (err.name !== 'AbortError') setError(err.message);
    });

  return () => controller.abort();
}, [id]);

Subscriptions and Event Listeners

// Window event listener
useEffect(() => {
  const handler = (e) => setSize({ w: window.innerWidth, h: window.innerHeight });
  window.addEventListener('resize', handler);
  handler(); // call immediately to set initial value
  return () => window.removeEventListener('resize', handler);
}, []);

// Custom event bus / observable
useEffect(() => {
  const sub = store.subscribe(setState);
  return () => sub.unsubscribe();
}, []);

Timers

// Interval
useEffect(() => {
  const id = setInterval(() => setTick(t => t + 1), 1000);
  return () => clearInterval(id);
}, []);

// Timeout
useEffect(() => {
  const id = setTimeout(() => setVisible(false), 3000);
  return () => clearTimeout(id);
}, []);

DOM Manipulation and Third-party Libraries

function MapWidget({ center }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  // Initialize once on mount
  useEffect(() => {
    mapRef.current = new ThirdPartyMap(containerRef.current);
    return () => mapRef.current.destroy();
  }, []);

  // Update on center change
  useEffect(() => {
    mapRef.current?.setCenter(center);
  }, [center]);

  return <div ref={containerRef} className="map" />;
}

Synchronizing with External Store

useEffect(() => {
  document.title = `${notifications} notifications`;
}, [notifications]);

useEffect(() => {
  const mql = window.matchMedia('(prefers-color-scheme: dark)');
  const handler = (e) => setDark(e.matches);
  mql.addEventListener('change', handler);
  setDark(mql.matches);
  return () => mql.removeEventListener('change', handler);
}, []);

Cleanup — Why It Matters

The cleanup function runs: 1. Before the effect re-runs (when deps change). 2. When the component unmounts.

Without cleanup, you risk: - Memory leaks (lingering listeners, subscriptions) - State updates on unmounted components (log warnings in React 17, silent in 18) - Race conditions (stale fetch results writing over fresh ones)

useEffect(() => {
  let active = true;
  fetchUser(id).then(u => { if (active) setUser(u); });
  return () => { active = false; };  // cancel stale update
}, [id]);

Effect vs Event Handler — Which to Use?

TriggerUse
User interaction (click, submit)Event handler
Synchronizing with an external systemuseEffect
Computation derived from state/propsNeither — compute during render
Side effect triggered by state change you causedOften better in the event handler
// ANTI-PATTERN — using effect for user-triggered work
function BadForm() {
  const [submitted, setSubmitted] = useState(false);
  useEffect(() => {
    if (submitted) sendToServer(); // delayed, hard to reason about
  }, [submitted]);
  return <button onClick={() => setSubmitted(true)}>Submit</button>;
}

// CORRECT — do it in the event handler
function GoodForm() {
  const handleSubmit = () => sendToServer(); // synchronous, obvious
  return <button onClick={handleSubmit}>Submit</button>;
}

Stale Closures and Fixes

// PROBLEM — stale closure: count is captured from the initial render
useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1); // count is always 0 — stale!
  }, 1000);
  return () => clearInterval(id);
}, []); // empty array = setup never re-runs

// FIX A — functional update (doesn't need count in deps)
useEffect(() => {
  const id = setInterval(() => setCount(c => c + 1), 1000);
  return () => clearInterval(id);
}, []);

// FIX B — add count to deps (interval re-created every tick)
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, [count]);

// FIX C — useRef to hold latest value
const countRef = useRef(count);
useEffect(() => { countRef.current = count; });
useEffect(() => {
  const id = setInterval(() => setCount(countRef.current + 1), 1000);
  return () => clearInterval(id);
}, []);

useEffect with async/await

useEffect cannot be async. Wrap instead:

// WRONG
useEffect(async () => {  // returns a Promise, not a cleanup fn
  const data = await fetch('/api');
}, []);

// CORRECT — async IIFE inside
useEffect(() => {
  (async () => {
    const res = await fetch('/api/data');
    const data = await res.json();
    setData(data);
  })();
}, []);

// CORRECT — named async function inside
useEffect(() => {
  async function load() {
    const res = await fetch('/api/data');
    const data = await res.json();
    setData(data);
  }
  load();
}, []);

useLayoutEffect

Same signature as useEffect but fires synchronously after DOM mutations, before the browser paints. Use for measuring DOM or preventing visual flicker.

import { useLayoutEffect, useRef, useState } from 'react';

function Tooltip({ target }) {
  const ref = useRef(null);
  const [pos, setPos] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    // Measure before paint — no flicker
    const rect = ref.current.getBoundingClientRect();
    setPos({ top: rect.bottom, left: rect.left });
  }, [target]);

  return <div ref={ref} style={pos}>Tooltip</div>;
}

Prefer useEffect by default. Use useLayoutEffect only when you observe flicker or need a DOM measurement that must happen before paint.