Effects and Strict Mode

Introduction

Side effects—fetching data, subscriptions, timers, syncing with document.title—belong outside the render path. useEffect runs code after render and can clean up when dependencies change or the component unmounts. Strict Mode helps catch unsafe effects during development.

Prerequisites

useEffect Basics

tsx
import { useEffect, useState } from "react";
 
function PageTitle({ title }: { title: string }) {
  useEffect(() => {
    document.title = title;
  }, [title]);
 
  return <h1>{title}</h1>;
}

Code explanation:

  • useEffect(fn, deps) runs fn after paint
  • [title] — re-run when title changes
  • [] — run once after mount (and clean up on unmount)
  • Omitting deps — runs after every render (rarely what you want)

Cleanup Functions

tsx
useEffect(() => {
  const id = window.setInterval(() => {
    console.log("tick");
  }, 1000);
 
  return () => {
    window.clearInterval(id);
  };
}, []);

Code explanation:

  • return () => { ... } runs before the effect re-runs and on unmount
  • Cancel timers, abort fetches, remove event listeners here

Event listener example:

tsx
useEffect(() => {
  function onResize() {
    setWidth(window.innerWidth);
  }
  window.addEventListener("resize", onResize);
  return () => window.removeEventListener("resize", onResize);
}, []);

Fetching Data (Simple Pattern)

Code explanation:

  • cancelled flag ignores stale responses when userId changes quickly
  • Later you will wrap this in useUser custom hook—Custom Hooks

Common Mistakes

Infinite loop

tsx
// Wrong — setState in effect with no proper deps
useEffect(() => {
  setCount(count + 1);
});

Fix: add correct dependencies or use functional updates only when intended.

Missing dependency

ESLint react-hooks/exhaustive-deps warns when values used inside the effect are omitted from the dependency array.

Object/array deps

tsx
// New object every render — effect runs every time
useEffect(() => {}, [{ id: userId }]);

Depend on primitives: [userId].

Mapping to Class Lifecycle (Legacy)

ClassHooks
componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {}, [dep])
componentWillUnmountcleanup in useEffect return

Vue mapping: onMounted / watchVue lifecycle.

Strict Mode

main.tsx wraps the app:

tsx
createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

In development, Strict Mode:

  • Double-invokes some functions to surface impure render logic
  • Mounts, unmounts, and remounts components to test effect cleanup

You may see fetch run twice in dev—that is intentional. Production builds do not double effects.

Tip

Idempotent Effects

Write effects that tolerate re-run: use cleanup, abort controllers, and ignore stale async results.

useEffect vs Event Handlers

Use effectUse event handler
Sync with external system after renderRespond to user click/submit
Subscribe to browser/window eventsUpdate state directly on interaction
Fetch when id prop changesPOST form on submit

Do not put every state update in an effect—handle user input in handlers.

FAQ

Can I make useEffect async?

tsx
// Wrong
useEffect(async () => { ... }, []);
 
// Right
useEffect(() => {
  async function load() { ... }
  load();
}, []);

useLayoutEffect?

Runs before browser paint—measure DOM layout. Rare; default to useEffect.

No cleanup needed?

Omit return—effect simply runs and finishes.

Strict Mode in production?

Wrapped app is fine—extra checks are dev-only.

Replace useEffect with React Query?

Libraries like TanStack Query handle caching and retries—optional after you understand raw fetch in chapter 16.

Next chapter?

Forms and Controlled Components.