Technology · React

React Lifecycle

Understand React component lifecycle phases and run effects at the right time.

TL;DR
  1. 01Every component goes through mount, update, and unmount phases.
  2. 02useEffect with an empty array runs only once on mount.
  3. 03Return a cleanup function to run code on unmount or before re-run.

Component Lifecycle Phases

  • Mount: component is created and inserted into the DOM.
  • Update: component re-renders due to state or prop changes.
  • Unmount: component is removed from the DOM.
    function Component() {
      // Mount: run once
      useEffect(() => {
        console.log("Mounted");
      }, []);
      
      // Update: run on every render
      useEffect(() => {
        console.log("Rendered");
      });
      
      // Unmount: cleanup
      useEffect(() => {
        return () => console.log("Unmounting");
      }, []);
    }
  • React 18 Strict Mode mounts, unmounts, then remounts in development.
    // In development with Strict Mode, effects run twice
    // This is intentional — effects must be cleanup-safe
  • The same component instance updates without dismounting on re-renders.
    // Changing props or state updates the component
    // — it does NOT unmount and remount

Running on Mount

  • Run effect once when component mounts with an empty dependency array.
    useEffect(() => {
      fetch('/api/data').then(r => r.json()).then(setData);
    }, []);
  • Empty dependency array means run once on mount.
  • Set up subscriptions and listeners on mount.
    useEffect(() => {
      const handler = () => setOnline(navigator.onLine);
      window.addEventListener("online", handler);
      window.addEventListener("offline", handler);
      return () => {
        window.removeEventListener("online", handler);
        window.removeEventListener("offline", handler);
      };
    }, []);
  • Log analytics events when a page or component first appears.
    useEffect(() => {
      analytics.track("page_view", { page: "home" });
    }, []);
  • Initialize third-party libraries like maps or charts on mount.
    useEffect(() => {
      const chart = new Chart(canvasRef.current, config);
      return () => chart.destroy();
    }, []);

Running on Update

  • Run effect when specific dependencies change.
    useEffect(() => {
      console.log("Count changed:", count);
    }, [count]);
  • Runs after the first render and on every dependency change.
  • Re-fetch data when a filter or ID changes.
    useEffect(() => {
      setLoading(true);
      fetchUser(userId).then(setUser).finally(() => setLoading(false));
    }, [userId]);
  • Sync an external system when state changes.
    useEffect(() => {
      document.title = `${unreadCount} new messages`;
    }, [unreadCount]);
  • Include all variables used inside the effect in the dependency array.
    useEffect(() => {
      // uses both userId and token — both must be in deps
      loadProfile(userId, token);
    }, [userId, token]);

Running on Every Render

  • Run effect on every render by omitting the dependency array.
    useEffect(() => {
      console.log("Rendered");
    });
  • No dependency array means run after every render.
  • Can cause performance issues if the effect is expensive.
  • Useful for syncing to an external store every time.
    useEffect(() => {
      // Runs every render — only use when every render matters
      logger.record({ component: "App", renderCount: ++count });
    });
  • Prefer a specific dependency array to limit when the effect runs.
    // Instead of no deps, list what actually needs to trigger the effect
    useEffect(() => {
      syncToExternalStore(value);
    }, [value]); // only syncs when value changes

Cleanup and Unmount

  • Return a cleanup function to run before unmount or re-run.
    useEffect(() => {
      const subscription = subscribe();
      
      // Cleanup function runs before unmount or before effect re-runs
      return () => {
        subscription.unsubscribe();
      };
    }, []);
  • Cleanup runs before unmount or before the effect re-runs.
  • Prevents memory leaks and duplicate subscriptions.
  • Cancel in-progress fetch requests on cleanup.
    useEffect(() => {
      const controller = new AbortController();
      fetch(url, { signal: controller.signal }).then(setData);
      return () => controller.abort();
    }, [url]);
  • Clear timers in cleanup to prevent stale updates.
    useEffect(() => {
      const id = setTimeout(() => {
        setDebouncedValue(value);
      }, 300);
      return () => clearTimeout(id); // cancel if value changes quickly
    }, [value]);

Tip: Understand the dependency array — it controls when effects run and is critical for correct behavior.

Warning: Forgetting cleanup functions causes memory leaks — always unsubscribe from subscriptions, remove event listeners, and clear timers.

React Keys and ListsReact Memo and Lazy Loading