React Tutorial 0/120 lessons ~6 min read Lesson 33

    useEffect

    useEffect in React explains synchronizing components with external systems, dependency arrays, cleanup, Strict Mode, and avoiding Effects for ordinary calculations or event responses.

    Course progress0%
    Focus
    51 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    useEffect is a React Hook used to synchronize a component with an external system.

    It is commonly used when a component needs to perform work that is caused by rendering but is not itself part of calculating the JSX.

    Examples include:

    • Fetching data from an API
    • Updating the document title
    • Connecting to a WebSocket
    • Subscribing to an event
    • Starting and cleaning up a timer
    • Listening to browser events
    • Synchronizing with browser APIs
    • Connecting to third-party libraries

    The most important idea is:

    useEffect is primarily about synchronization with systems outside React.

    Why Do We Need useEffect?

    A React component should ideally describe:

    text
    Props + State
    ↓
    Render
    ↓
    UI

    But applications also need to interact with systems outside React:

    text
    React component
    │
    ├── Browser API
    ├── Network
    ├── Timer
    ├── WebSocket
    ├── Event listener
    └── Third-party library

    These interactions are called side effects.

    For example:

    javascript
    document.title = "Dashboard";

    This changes something outside the component's rendered JSX.

    Or:

    javascript
    window.addEventListener("resize", handleResize);

    This creates an external subscription.

    useEffect provides a structured way to synchronize these external systems with the component.

    Importing useEffect

    Import it from React:

    javascript
    import { useEffect } from "react";

    It can be used alongside other Hooks:

    javascript
    import { useState, useEffect } from "react";

    Basic Syntax

    The basic structure is:

    javascript
    useEffect(() => {
    // Effect logic
    });

    A dependency array can be provided:

    javascript
    useEffect(() => {
    // Effect logic
    }, []);

    Or:

    javascript
    useEffect(() => {
    // Effect logic
    }, [dependency]);

    The dependency array tells React which reactive values the effect depends on.

    The Three Common Forms

    There are three important forms to understand.

    1. No Dependency Array

    javascript
    useEffect(() => {
    console.log("Effect ran");
    });

    The effect is associated with every completed render.

    This is usually not what you want for most effects because it can run more often than necessary.

    2. Empty Dependency Array

    javascript
    useEffect(() => {
    console.log("Effect ran");
    }, []);

    The effect has no reactive dependencies.

    It runs after the component's initial mount.

    In development, React Strict Mode may intentionally run setup and cleanup an additional time to help reveal bugs. Therefore, do not rely on an effect running exactly once as a universal guarantee.

    3. Dependency Array With Values

    javascript
    useEffect(() => {
    console.log("User changed:", userId);
    }, [userId]);

    The effect is synchronized when userId changes between renders.

    The mental model is:

    text
    Render
    ↓
    Compare dependencies
    ↓
    Dependency changed?
    │
    ├── No → Skip effect
    │
    └── Yes → Run effect

    A Simple Example: Document Title

    javascript
    import { useEffect, useState } from "react";
    function Counter() {
    const [count, setCount] = useState(0);
    useEffect(() => {
    document.title = `Count: ${count}`;
    }, [count]);
    return (
    <button onClick={() => setCount(previous => previous + 1)}>
    Count: {count}
    </button>
    );
    }
    export default Counter;

    The relationship is:

    text
    count changes
    ↓
    component renders
    ↓
    effect runs
    ↓
    document.title is synchronized

    Here, document.title belongs to the browser environment, not the React component tree.

    Rendering vs Effects

    One of the most important concepts is separating rendering from effects.

    Rendering should calculate the UI:

    javascript
    function User({ name }) {
    return <h1>Hello, {name}</h1>;
    }

    An effect performs synchronization:

    javascript
    useEffect(() => {
    document.title = name;
    }, [name]);

    Think:

    text
    Render
    ↓
    Calculate JSX
    Effect
    ↓
    Synchronize external system

    An effect is not another place to calculate ordinary values that could have been calculated during rendering.

    What Is a Side Effect?

    A side effect is an operation that interacts with something outside the component's rendering calculation.

    Examples:

    javascript
    document.title = "Hello";
    javascript
    localStorage.setItem("theme", "dark");
    javascript
    window.addEventListener("resize", handleResize);
    javascript
    fetch("/api/users");
    javascript
    connection.connect();

    These operations interact with external systems.

    By contrast:

    javascript
    const fullName = `${firstName} ${lastName}`;

    is just a calculation.

    It does not need an effect.

    The Synchronization Mental Model

    Instead of thinking:

    Run this code after rendering.

    prefer:

    Keep this external system synchronized with the current React state and props.

    For example:

    javascript
    useEffect(() => {
    document.title = `Cart (${cartItems.length})`;
    }, [cartItems.length]);

    The effect is responsible for keeping the browser tab title synchronized with the cart state.

    This mental model leads to better effect design.

    Dependency Arrays

    Dependencies are one of the most important parts of useEffect.

    Consider:

    javascript
    useEffect(() => {
    console.log(userId);
    }, [userId]);

    The effect reads userId, so userId belongs in the dependency array.

    If another reactive value is read by the effect:

    javascript
    useEffect(() => {
    console.log(userId, theme);
    }, [userId, theme]);

    both values are dependencies.

    A useful rule is:

    If an effect uses a reactive value from the component, that value generally needs to be represented in the dependency list.

    React's Hooks linting tools can help identify missing dependencies.

    What Counts as a Reactive Value?

    Values declared inside a component can be reactive:

    javascript
    function Profile({ userId }) {
    const [name, setName] = useState("");
    useEffect(() => {
    console.log(userId, name);
    }, [userId, name]);
    // ...
    }

    Here:

    text
    userId → prop
    name → state

    Both can change between renders.

    They therefore affect when the synchronization should be recalculated.

    Dependency Comparison

    React compares dependency values between renders.

    For example:

    javascript
    useEffect(() => {
    // ...
    }, [userId]);

    If:

    text
    Previous userId = 10
    Current userId = 10

    the dependency has not changed.

    If:

    text
    Previous userId = 10
    Current userId = 20

    the dependency changed.

    The effect can then run again.

    React uses Object.is semantics when comparing dependency values.

    Object and Array Dependencies

    Be careful when using objects or arrays as dependencies.

    Consider:

    javascript
    function Component({ user }) {
    useEffect(() => {
    console.log(user);
    }, [user]);
    // ...
    }

    If the user object is recreated with a different reference, React can consider it changed even if the properties contain the same values.

    For example:

    javascript
    const user = {
    name: "John"
    };

    Creating a new object produces a new reference:

    javascript
    const anotherUser = {
    name: "John"
    };

    Even though the contents look the same, these are different object references.

    This is why dependency design matters.

    Cleanup Functions

    Some effects create subscriptions or resources that need to be removed.

    Examples:

    • Event listeners
    • Timers
    • WebSocket connections
    • Subscriptions
    • Third-party resources

    An effect can return a cleanup function:

    javascript
    useEffect(() => {
    // Setup
    return () => {
    // Cleanup
    };
    }, []);

    Event Listener Example

    javascript
    import { useEffect } from "react";
    function WindowWidth() {
    useEffect(() => {
    function handleResize() {
    console.log(window.innerWidth);
    }
    window.addEventListener("resize", handleResize);
    return () => {
    window.removeEventListener("resize", handleResize);
    };
    }, []);
    return <p>Resize the browser window.</p>;
    }

    The lifecycle is:

    text
    Component mounts
    ↓
    Effect setup
    ↓
    Add event listener
    ↓
    Component remains active
    ↓
    Component unmounts
    ↓
    Cleanup
    ↓
    Remove event listener

    The cleanup prevents the component from leaving behind an unnecessary subscription.

    Cleanup Before Re-Synchronization

    Cleanup is not only for unmounting.

    When dependencies change, React can:

    text
    Previous effect
    ↓
    Cleanup previous synchronization
    ↓
    Render with new values
    ↓
    Run new effect

    For example:

    javascript
    useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    return () => {
    connection.disconnect();
    };
    }, [roomId]);

    When roomId changes, the old connection should be cleaned up before synchronizing the new one.

    This is one of the most important reasons to return cleanup functions.

    Timer Example

    javascript
    import { useEffect } from "react";
    function Timer() {
    useEffect(() => {
    const intervalId = setInterval(() => {
    console.log("Tick");
    }, 1000);
    return () => {
    clearInterval(intervalId);
    };
    }, []);
    return <p>Timer is running.</p>;
    }

    Without cleanup:

    text
    Component
    ↓
    Creates interval
    ↓
    Component disappears
    ↓
    Interval may continue running

    With cleanup:

    text
    Component
    ↓
    Creates interval
    ↓
    Component disappears
    ↓
    clearInterval()
    ↓
    Timer stopped

    Effect Lifecycle

    A useful conceptual lifecycle is:

    text
    Component renders
    ↓
    React commits the UI
    ↓
    Effect setup runs
    ↓
    External system synchronized
    ↓
    Dependencies change?
    │
    ├── No
    │
    └── Yes
    ↓
    Cleanup previous effect
    ↓
    Run new effect setup

    When the component is removed:

    text
    Component unmounts
    ↓
    Cleanup runs

    This is why an effect can be thought of as a synchronization process with a corresponding cleanup process.

    Fetching Data With useEffect

    A common beginner example is fetching data:

    javascript
    import { useEffect, useState } from "react";
    function Users() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
    async function loadUsers() {
    const response = await fetch("/api/users");
    const data = await response.json();
    setUsers(data);
    setLoading(false);
    }
    loadUsers();
    }, []);
    if (loading) {
    return <p>Loading...</p>;
    }
    return (
    <ul>
    {users.map(user => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    );
    }

    The flow is:

    text
    Component renders
    ↓
    Effect starts request
    ↓
    API responds
    ↓
    setUsers(...)
    ↓
    setLoading(...)
    ↓
    Component renders again
    ↓
    UI displays data

    Handling Fetch Errors

    Production code should generally handle failures.

    javascript
    import { useEffect, useState } from "react";
    function Users() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    useEffect(() => {
    async function loadUsers() {
    try {
    const response = await fetch("/api/users");
    if (!response.ok) {
    throw new Error("Failed to load users");
    }
    const data = await response.json();
    setUsers(data);
    } catch (error) {
    setError(error);
    } finally {
    setLoading(false);
    }
    }
    loadUsers();
    }, []);
    if (loading) {
    return <p>Loading...</p>;
    }
    if (error) {
    return <p>Unable to load users.</p>;
    }
    return (
    <ul>
    {users.map(user => (
    <li key={user.id}>{user.name}</li>
    ))}
    </ul>
    );
    }

    This creates three useful UI states:

    text
    Loading
    ↓
    Success
    OR
    Error

    Fetching Data When a Dependency Changes

    Suppose a page displays a selected user:

    javascript
    function UserProfile({ userId }) {
    const [user, setUser] = useState(null);
    useEffect(() => {
    async function loadUser() {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    setUser(data);
    }
    loadUser();
    }, [userId]);
    if (!user) {
    return <p>Loading...</p>;
    }
    return <h2>{user.name}</h2>;
    }

    The dependency:

    javascript
    [userId]

    means the effect should synchronize the displayed data whenever the selected user changes.

    text
    userId = 10
    ↓
    fetch user 10
    userId = 20
    ↓
    fetch user 20

    Race Conditions in Data Fetching

    When fetching data based on changing dependencies, an earlier request may finish after a newer request.

    Example:

    text
    Request A → user 10
    Request B → user 20
    B finishes first
    ↓
    UI shows user 20
    A finishes later
    ↓
    UI could incorrectly show user 10

    One approach is to ignore stale results using an ignore flag:

    javascript
    useEffect(() => {
    let ignore = false;
    async function loadUser() {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    if (!ignore) {
    setUser(data);
    }
    }
    loadUser();
    return () => {
    ignore = true;
    };
    }, [userId]);

    Another approach is to use AbortController when the underlying API supports cancellation.

    For larger applications, dedicated data-fetching libraries or framework-level data APIs may provide better caching, cancellation, deduplication, and loading-state management.

    AbortController Example

    javascript
    useEffect(() => {
    const controller = new AbortController();
    async function loadUser() {
    try {
    const response = await fetch(
    `/api/users/${userId}`,
    {
    signal: controller.signal
    }
    );
    const data = await response.json();
    setUser(data);
    } catch (error) {
    if (error.name !== "AbortError") {
    setError(error);
    }
    }
    }
    loadUser();
    return () => {
    controller.abort();
    };
    }, [userId]);

    The cleanup cancels the previous request when the synchronization is replaced or the component is removed.

    Effects Are Not Event Handlers

    An event handler responds to a specific interaction:

    javascript
    function handleSubmit() {
    // User submitted the form
    }

    An effect responds to rendering and dependency changes:

    javascript
    useEffect(() => {
    // Synchronize external system
    }, [value]);

    Do not automatically move event-specific logic into an effect.

    For example, if clicking a button should submit an order:

    javascript
    function handleOrder() {
    submitOrder();
    }

    is generally clearer than:

    javascript
    useEffect(() => {
    if (shouldSubmit) {
    submitOrder();
    }
    }, [shouldSubmit]);

    When something happens specifically because the user interacted with the UI, an event handler is often the appropriate place.

    Effects Are Not for Derived Data

    Suppose:

    javascript
    const [firstName, setFirstName] = useState("John");
    const [lastName, setLastName] = useState("Doe");

    Avoid:

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

    This introduces unnecessary state and an unnecessary render cycle.

    Prefer:

    javascript
    const fullName = `${firstName} ${lastName}`;

    The value can be calculated directly during rendering.

    This is a very important principle:

    Do not use an effect to calculate something that can be calculated during rendering.

    Effects Are Not for State-to-State Synchronization

    Avoid patterns like:

    javascript
    const [firstName, setFirstName] = useState("");
    const [lastName, setLastName] = useState("");
    const [fullName, setFullName] = useState("");
    useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
    }, [firstName, lastName]);

    Instead:

    javascript
    const fullName = `${firstName} ${lastName}`;

    There is one source of truth rather than three pieces of related state.

    When You Might Not Need an Effect

    Before writing:

    javascript
    useEffect(() => {
    // ...
    }, []);

    ask:

    What external system am I synchronizing with?

    If the answer is nothing, the effect may not be necessary.

    Common situations where an effect is often unnecessary:

    Transforming data

    javascript
    const sortedUsers = [...users].sort(...);

    Calculating a value

    javascript
    const total = price * quantity;

    Handling a click

    javascript
    function handleClick() {
    // ...
    }

    Rendering conditionally

    javascript
    return isLoggedIn ? <Dashboard /> : <Login />;

    Resetting state unnecessarily

    Sometimes component structure or a key can provide a cleaner reset mechanism.

    Synchronizing With Browser APIs

    Effects can synchronize React state with browser APIs.

    For example:

    javascript
    function PageTitle({ title }) {
    useEffect(() => {
    document.title = title;
    }, [title]);
    return <h1>{title}</h1>;
    }

    The React UI is:

    javascript
    <h1>{title}</h1>

    The browser state is:

    text
    document.title

    The effect keeps them synchronized.

    Local Storage Example

    Suppose a theme needs to be persisted:

    javascript
    function ThemePreference({ theme }) {
    useEffect(() => {
    localStorage.setItem("theme", theme);
    }, [theme]);
    return <p>Current theme: {theme}</p>;
    }

    Here:

    text
    React state/prop
    ↓
    theme
    ↓
    useEffect
    ↓
    localStorage

    localStorage is an external browser system, so synchronization is a reasonable use of an effect.

    Browser Event Example

    javascript
    import { useEffect, useState } from "react";
    function OnlineStatus() {
    const [isOnline, setIsOnline] = useState(
    navigator.onLine
    );
    useEffect(() => {
    function handleOnline() {
    setIsOnline(true);
    }
    function handleOffline() {
    setIsOnline(false);
    }
    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);
    return () => {
    window.removeEventListener("online", handleOnline);
    window.removeEventListener("offline", handleOffline);
    };
    }, []);
    return (
    <p>
    {isOnline ? "Online" : "Offline"}
    </p>
    );
    }
    export default OnlineStatus;

    This demonstrates a common effect pattern:

    text
    Setup external subscription
    ↓
    Respond to external changes
    ↓
    Update React state
    ↓
    Render UI
    ↓
    Cleanup subscription

    This pattern can later be extracted into a custom Hook.

    WebSocket Example

    A component may need to connect to a real-time server.

    javascript
    useEffect(() => {
    const socket = new WebSocket(
    `wss://example.com/rooms/${roomId}`
    );
    socket.addEventListener("message", handleMessage);
    return () => {
    socket.removeEventListener("message", handleMessage);
    socket.close();
    };
    }, [roomId]);

    The effect synchronizes the connection with the current roomId.

    When the room changes:

    text
    Old room
    ↓
    Cleanup old connection
    ↓
    New room
    ↓
    Create new connection

    Third-Party Libraries

    Effects are often useful when integrating React with libraries that manage their own systems.

    For example:

    javascript
    useEffect(() => {
    const chart = createChart(chartContainer.current);
    chart.setData(data);
    return () => {
    chart.destroy();
    };
    }, [data]);

    The effect handles the lifecycle of the external chart instance.

    This is one of the strongest use cases for effects:

    text
    React
    ↕
    External library

    Strict Mode and useEffect

    When an application is wrapped in React Strict Mode during development, React may intentionally perform an additional setup → cleanup → setup cycle for effects.

    For example:

    text
    Development:
    setup
    cleanup
    setup

    This helps reveal effects that are not properly reversible.

    If your effect adds a listener:

    javascript
    window.addEventListener("resize", handler);

    the cleanup should remove it:

    javascript
    window.removeEventListener("resize", handler);

    If your effect opens a connection:

    javascript
    connection.connect();

    the cleanup should disconnect it:

    javascript
    connection.disconnect();

    The goal is for:

    text
    setup → cleanup

    to correctly undo the work performed by setup.

    Do not add flags simply to force an effect to appear to run once. Fix the effect's setup and cleanup logic instead.

    Common useEffect Mistakes

    Mistake 1: Missing Dependencies

    Incorrect:

    javascript
    useEffect(() => {
    console.log(userId);
    }, []);

    If the effect is supposed to synchronize with changing userId, omitting it can create stale behavior.

    Prefer:

    javascript
    useEffect(() => {
    console.log(userId);
    }, [userId]);

    Mistake 2: Using Effects for Derived Values

    Avoid:

    javascript
    useEffect(() => {
    setTotal(price * quantity);
    }, [price, quantity]);

    Prefer:

    javascript
    const total = price * quantity;

    Mistake 3: Forgetting Cleanup

    Incorrect:

    javascript
    useEffect(() => {
    window.addEventListener("resize", handleResize);
    }, []);

    Prefer:

    javascript
    useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => {
    window.removeEventListener("resize", handleResize);
    };
    }, []);

    Mistake 4: Creating Infinite Effect Loops

    Consider:

    javascript
    useEffect(() => {
    setCount(count + 1);
    }, [count]);

    The sequence is:

    text
    count changes
    ↓
    effect runs
    ↓
    setCount(...)
    ↓
    count changes
    ↓
    effect runs again
    ↓
    ...

    This can create an infinite update loop.

    Always ask whether an effect's state update changes one of the effect's own dependencies.

    Mistake 5: Putting Event Logic in Effects

    If something should happen because the user clicked a button, prefer:

    javascript
    function handleClick() {
    saveData();
    }

    rather than creating an intermediate state solely to trigger an effect.

    Mistake 6: Using an Effect to Reset Derived State

    Avoid unnecessary synchronization such as:

    javascript
    useEffect(() => {
    setFilteredItems(filterItems(items, search));
    }, [items, search]);

    If filtering is inexpensive, prefer:

    javascript
    const filteredItems = filterItems(items, search);

    If the calculation is genuinely expensive, performance-specific tools such as useMemo may be considered—but only when there is a measured need.

    Effect Dependency Checklist

    When writing an effect, ask:

    1. What external system am I synchronizing with?

    Examples:

    text
    Browser
    Network
    Timer
    WebSocket
    Subscription
    Third-party library

    2. What values does the effect read?

    text
    Props
    State
    Variables declared inside the component

    3. Which values should trigger re-synchronization?

    Those values generally belong in the dependency array.

    4. Does the effect create something that needs cleanup?

    If yes, return a cleanup function.

    5. Can this logic happen during rendering instead?

    If yes, an effect may not be necessary.

    6. Is this actually an event response?

    If yes, an event handler may be clearer.

    Separating Effects

    Avoid putting unrelated synchronization processes into one giant effect.

    Instead of:

    javascript
    useEffect(() => {
    document.title = title;
    localStorage.setItem("theme", theme);
    connectToChat(roomId);
    }, [title, theme, roomId]);

    consider separate effects:

    javascript
    useEffect(() => {
    document.title = title;
    }, [title]);
    useEffect(() => {
    localStorage.setItem("theme", theme);
    }, [theme]);
    useEffect(() => {
    const connection = connectToChat(roomId);
    return () => {
    connection.disconnect();
    };
    }, [roomId]);

    Each effect now has one clear synchronization responsibility.

    This makes dependency management and cleanup easier to understand.

    Effect Logic Should Be Reversible

    A useful design principle is:

    text
    Setup
    ↓
    External resource exists
    Cleanup
    ↓
    External resource removed

    Examples:

    SetupCleanup
    addEventListenerremoveEventListener
    setIntervalclearInterval
    setTimeoutclearTimeout when needed
    connect()disconnect()
    subscribe()unsubscribe()
    Create library instanceDestroy library instance

    Not every effect requires cleanup. Effects that only synchronize a value such as:

    javascript
    document.title = title;

    may not need one.

    A Complete Synchronization Example

    Consider a chat room:

    javascript
    import { useEffect } from "react";
    function ChatRoom({ roomId }) {
    useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    return () => {
    connection.disconnect();
    };
    }, [roomId]);
    return <h2>Room: {roomId}</h2>;
    }

    The effect describes one synchronization process:

    text
    Current roomId
    ↓
    Create connection
    ↓
    Connect
    roomId changes
    ↓
    Disconnect old room
    ↓
    Create new connection
    ↓
    Connect new room
    Component unmounts
    ↓
    Disconnect

    This is the mental model to aim for when designing effects.

    useEffect vs useState

    useState and useEffect solve different problems.

    HookPrimary purpose
    useStateStore changing component state
    useEffectSynchronize with external systems

    Example:

    javascript
    const [count, setCount] = useState(0);
    useEffect(() => {
    document.title = `Count: ${count}`;
    }, [count]);

    Here:

    text
    useState
    ↓
    stores count
    useEffect
    ↓
    synchronizes document.title

    useEffect vs Event Handlers

    Event handler:

    javascript
    function handleSubmit() {
    saveForm();
    }

    Effect:

    javascript
    useEffect(() => {
    document.title = title;
    }, [title]);

    Use an event handler when the logic is caused by a specific user action.

    Use an effect when the component needs to synchronize an external system with its current state or props.

    useEffect vs useMemo

    These Hooks have different purposes.

    useMemo:

    javascript
    const filteredItems = useMemo(
    () => filterItems(items, search),
    [items, search]
    );

    is about caching a calculation.

    useEffect:

    javascript
    useEffect(() => {
    saveToStorage(items);
    }, [items]);

    is about synchronizing with an external system.

    Do not use useEffect as a replacement for useMemo.

    useEffect vs useRef

    useRef stores a value that persists across renders without causing a render when it changes.

    For example:

    javascript
    const inputRef = useRef(null);

    An effect can use a ref to interact with the DOM:

    javascript
    useEffect(() => {
    inputRef.current?.focus();
    }, []);

    Here:

    text
    useRef
    ↓
    Access DOM node
    useEffect
    ↓
    Perform synchronization after render

    useRef will be covered in the next Hook lesson.

    Best Practices

    1. Start With the External System

    Before writing an effect, identify what is outside React that needs synchronization.

    2. Keep Each Effect Focused

    One synchronization concern per effect is often easier to maintain.

    3. Declare Dependencies Honestly

    Do not remove dependencies just to make an effect run less often.

    Instead, restructure the code if the dependencies reveal a design problem.

    4. Always Clean Up Subscriptions

    If setup creates a subscription or resource, provide matching cleanup.

    5. Avoid Effects for Derived Data

    Calculate derived values during rendering.

    6. Avoid Effects for User Events

    Use event handlers for event-specific actions.

    7. Be Careful With Object and Function Dependencies

    Unstable references can cause effects to re-run.

    Do not automatically add memoization everywhere. First understand why the dependency changes and whether the effect actually needs to synchronize again.

    8. Handle Async Work Carefully

    Consider loading states, errors, cancellation, and stale responses.

    9. Make Effects Safe to Re-run

    An effect should correctly handle setup and cleanup without relying on accidental execution counts.

    Practical Example: Search Synchronization

    Consider a search component:

    javascript
    import { useEffect, useState } from "react";
    function SearchResults({ query }) {
    const [results, setResults] = useState([]);
    const [loading, setLoading] = useState(false);
    useEffect(() => {
    let ignore = false;
    async function search() {
    setLoading(true);
    const response = await fetch(
    `/api/search?q=${encodeURIComponent(query)}`
    );
    const data = await response.json();
    if (!ignore) {
    setResults(data);
    setLoading(false);
    }
    }
    search();
    return () => {
    ignore = true;
    };
    }, [query]);
    if (loading) {
    return <p>Searching...</p>;
    }
    return (
    <ul>
    {results.map(result => (
    <li key={result.id}>
    {result.title}
    </li>
    ))}
    </ul>
    );
    }

    The effect synchronizes the external search request with the current query.

    text
    query changes
    ↓
    old synchronization cleaned up
    ↓
    new search starts
    ↓
    results update
    ↓
    UI renders

    Practical Example: Window Width

    javascript
    import { useEffect, useState } from "react";
    function WindowSize() {
    const [width, setWidth] = useState(window.innerWidth);
    useEffect(() => {
    function handleResize() {
    setWidth(window.innerWidth);
    }
    window.addEventListener("resize", handleResize);
    return () => {
    window.removeEventListener("resize", handleResize);
    };
    }, []);
    return <p>Window width: {width}px</p>;
    }
    export default WindowSize;

    This example demonstrates:

    • State
    • Browser events
    • Effect setup
    • Cleanup
    • External system synchronization

    Practical Example: Persisting State

    javascript
    import { useEffect, useState } from "react";
    function ThemeSelector() {
    const [theme, setTheme] = useState("light");
    useEffect(() => {
    localStorage.setItem("theme", theme);
    }, [theme]);
    return (
    <button
    onClick={() =>
    setTheme(previous =>
    previous === "light" ? "dark" : "light"
    )
    }
    >
    Theme: {theme}
    </button>
    );
    }
    export default ThemeSelector;

    The state is React-managed:

    text
    theme

    The persistence system is outside React:

    text
    localStorage

    The effect connects the two.

    Debugging useEffect

    When an effect behaves unexpectedly, inspect:

    1. Does it run too often?

    Check:

    • Dependency values
    • Object references
    • Function references
    • State updates inside the effect

    2. Does it run when it should not?

    Check whether the effect is actually necessary.

    3. Does it run with stale values?

    Check dependencies and closures.

    4. Is cleanup working?

    Look for:

    javascript
    return () => {
    // cleanup
    };

    5. Does Strict Mode expose duplicate behavior?

    If development causes repeated setup, verify that cleanup correctly reverses setup.

    Interview questions & answers

    Interview preparation

    10 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    4
    1. 1What is useEffect?
      Beginner

      Model answer

      useEffect is a React Hook used to synchronize a component with an external system.
    2. 2What does an empty dependency array mean?
      Beginner

      Model answer

      useEffect(() => { → // ... → }, []); The effect has no reactive dependencies and runs after the component mounts. In development Strict Mode, setup may be intentionally re-run to detect bugs.
    3. 3Should useEffect be used for derived state?
      Beginner

      Model answer

      Usually no. Prefer: const total = price * quantity; instead of creating another state value and synchronizing it with an effect.
    4. 4What is the difference between useEffect and event handlers?
      Beginner

      Model answer

      Event handlers respond to user interactions.

      Effects synchronize external systems based on rendering and dependency changes.

    Intermediate

    3
    1. 5When does useEffect run?
      Intermediate

      Model answer

      It runs after React commits a render when its dependency conditions indicate that the effect needs to synchronize.

    2. 6Why is cleanup needed?
      Intermediate

      Model answer

      Cleanup releases resources or removes subscriptions created by the effect. Examples: clearInterval(id); window.removeEventListener("resize", handler); connection.disconnect();
    3. 7What causes an effect to re-run?
      Intermediate

      Model answer

      When one or more dependencies change according to React's dependency comparison.

    Advanced

    3
    1. 8What does the dependency array do?
      Advanced

      Model answer

      It specifies the reactive values that the effect depends on. useEffect(() => { → // ... → }, [userId]); The effect can re-run when userId changes.
    2. 9Can useEffect return a value?
      Advanced

      Model answer

      The effect callback can return a cleanup function: useEffect(() => { → return () => { → // cleanup → }; → }, []); The returned value should be a cleanup function or nothing; it is not meant to return arbitrary data.
    3. 10Why can an effect run multiple times in development?
      Advanced

      Model answer

      React Strict Mode can intentionally perform an extra setup and cleanup cycle in development to help find effects that are not correctly implemented.

    Practice Exercises

    Exercise 1: Document Title

    Create a counter where:

    • The counter uses useState.
    • The document title displays the current count.
    • useEffect synchronizes the title.

    Exercise 2: Window Width

    Create a component that:

    • Displays browser width.
    • Updates when the window is resized.
    • Adds the event listener in an effect.
    • Removes the event listener during cleanup.

    Exercise 3: Timer

    Create a timer that:

    • Starts an interval.
    • Updates a counter every second.
    • Cleans up the interval when the component unmounts.

    Exercise 4: Local Storage

    Create a theme selector that:

    • Uses state for the current theme.
    • Stores the theme in localStorage.
    • Synchronizes storage whenever the theme changes.

    Exercise 5: User Fetch

    Create a component that:

    • Receives userId as a prop.
    • Fetches the corresponding user.
    • Displays loading state.
    • Displays error state.
    • Re-fetches when userId changes.

    Exercise 6: Chat Connection

    Create a component that:

    • Receives roomId.
    • Creates a connection when mounted.
    • Disconnects during cleanup.
    • Reconnects when roomId changes.

    Mini Project: Online Status Indicator

    Build a reusable component that displays whether the browser is online.

    Requirements:

    text
    Initial state
    ↓
    navigator.onLine

    Listen for:

    text
    online
    offline

    Update state when either event occurs.

    Clean up both event listeners.

    A possible implementation:

    javascript
    import { useEffect, useState } from "react";
    function OnlineStatus() {
    const [isOnline, setIsOnline] = useState(
    navigator.onLine
    );
    useEffect(() => {
    function handleOnline() {
    setIsOnline(true);
    }
    function handleOffline() {
    setIsOnline(false);
    }
    window.addEventListener("online", handleOnline);
    window.addEventListener("offline", handleOffline);
    return () => {
    window.removeEventListener("online", handleOnline);
    window.removeEventListener("offline", handleOffline);
    };
    }, []);
    return (
    <div>
    <strong>
    {isOnline ? "Online" : "Offline"}
    </strong>
    </div>
    );
    }
    export default OnlineStatus;

    This is a good candidate for extracting into a custom Hook later.

    A Practical Decision Tree

    When you are about to write an effect, ask:

    text
    Do I need to change or synchronize something
    outside React?
    │
    ┌────┴────┐
    │ │
    No Yes
    │ │
    ▼ ▼
    Probably useEffect
    no effect │
    needed ▼
    What values does it
    depend on?
    │
    ▼
    Add dependencies
    │
    ▼
    Does setup create
    a resource?
    │
    ┌────┴────┐
    │ │
    No Yes
    │ │
    ▼ ▼
    Done Return cleanup

    This decision process prevents many unnecessary effects.

    Quick Cheat Sheet

    Import

    javascript
    import { useEffect } from "react";

    Basic effect

    javascript
    useEffect(() => {
    // Synchronization
    });

    No dependencies

    javascript
    useEffect(() => {
    // Runs after every completed render
    });

    Empty dependencies

    javascript
    useEffect(() => {
    // No reactive dependencies
    }, []);

    With dependencies

    javascript
    useEffect(() => {
    // Synchronize with userId
    }, [userId]);

    Cleanup

    javascript
    useEffect(() => {
    // Setup
    return () => {
    // Cleanup
    };
    }, []);

    Event listener

    javascript
    useEffect(() => {
    window.addEventListener("resize", handleResize);
    return () => {
    window.removeEventListener("resize", handleResize);
    };
    }, []);

    Previous state update

    javascript
    setCount(previous => previous + 1);

    Derived data

    Prefer:

    javascript
    const total = price * quantity;

    instead of:

    javascript
    useEffect(() => {
    setTotal(price * quantity);
    }, [price, quantity]);

    Key Takeaways

    Ready to mark this lesson complete?Track your journey across the entire course.