cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

The lifecycle of an effect

See an effect start and stop synchronizing as a chat room changes. Learn which values are reactive, why each is a dependency, and why the linter is right.

By the end, you can
  • 01
    Think in start and stopDescribe an effect by how it starts and stops synchronizing, and predict each start and stop as props change and the component comes and goes.
  • 02
    Find the reactive valuesTell props, state, and values worked out from them from module values, set functions, refs, and values outside React.
  • 03
    Trust the dependency listList every reactive value an effect reads, read the linter's messages, and fix a dependency by changing the code, not by silencing it.

A chat room that follows the room you pick

In Synchronizing with effects, a chat room connected to a server in an effect, and its cleanup disconnected. This lesson follows one such effect over the whole life of a page: you switch rooms, close the chat, and open it again, and you watch every start and stop. chat.js, in the second tab, is a pretend server that prints them instead of connecting.

A chat room you can switchPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; const serverUrl = "https://chat.example.com"; function ChatRoom({ roomId }) {  useEffect(() => {    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [roomId]);   return <h3>Welcome to the {roomId} room</h3>;} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
ResultLoading…

Line 4 is the server's address, outside any component. ChatRoom (lines 6 to 14) takes the room as a prop. Its effect (lines 7 to 11) makes a connection for this roomId (line 8), connects (line 9), and returns a cleanup that disconnects (line 10). Line 11 lists [roomId]. App keeps the room (line 17) and whether the chat is open (line 18), makes a button per room (lines 22 to 24), and shows ChatRoom only while the chat is open (line 27).

The console starts with Start, Stop, Start for general: in development, StrictMode stops and starts every new effect once, to check that it can. Now press travel. Two lines: Stop: disconnect from "general", then Start: connect to "travel". ChatRoom didn't leave the page; only its heading changed. Press travel again: nothing, because the room didn't change. Press music, then Close chat: a stop and a start, then one last stop.

One component, which appeared once and went away once. One effect, which started and stopped three times. They don't have the same life.

Real-life analogyA radio and its stations

You switch a kitchen radio on in the morning and off at night. In between, you turn the dial three times: each turn stops one station and starts another. Nobody would say the radio was switched on three times. It was on once, and it played three stations.

In real life: The radio, switched on and later off
In React: ChatRoom, on the page and later removed
In real life: Tuning to a station
In React: The effect starting to synchronize with a room
In real life: Turning the dial stops one station and starts the next
In React: A new roomId runs the cleanup, then the effect again
In real life: The station on the dial
In React: The reactive value the effect reads, roomId
The main idea

An effect has its own lifecycle. A component mounts, updates, and unmounts. An effect does only two things: it starts synchronizing with something, and later stops. React stops it and starts it again each time a value it reads changes, and stops it for good when the component goes away.

Start and stop, not mount and unmount

It's tempting to read an effect as "this runs when the component mounts, and the cleanup when it unmounts". That story breaks as soon as a prop changes. Read the effect on its own instead:

The effect alone (part of ChatRoom)JSX
useEffect(() => {
  const connection = createConnection(serverUrl, roomId);
  connection.connect();
  return () => connection.disconnect();
}, [roomId]);

This fragment of ChatRoom isn't meant to run on its own. Lines 2 and 3 say how to start synchronizing with one room: connect to it. Line 4 says how to stop: disconnect. Line 5 says what it follows: roomId. Nothing in it mentions mounting. From the effect's point of view, the session above was:

  1. Start synchronizing with general.
  2. Stop synchronizing with general. Start synchronizing with travel.
  3. Stop synchronizing with travel. Start synchronizing with music.
  4. Stop synchronizing with music.

Write every effect so that it can start and stop like this, any number of times, for any value of what it reads. Then you never have to think about which moment in the component's life it is. The playground runs the same ChatRoom and draws both lives side by side.

Playground: one component, many synchronizations
The pageroomId: "general"

Welcome to the general room

Timeline1 of 7 columns
start

ChatRoom on the page

Effect synchronized with

    Try it yourself
    Pick a room.

    Pick travel, then music, then Close chat. Each action adds a column. Watch which lane changes.

    ChatRoom runs here, with React on this page; each bar is drawn from its real effect starting and stopping. The top lane is ChatRoom on the page, the bottom lane its effect, synchronized with one room. Each column is one of your actions. In development, StrictMode may also stop and start a new ChatRoom's effect once, to check it can; that quick check shows in the log but is too short for a bar.

    Pick travel and music: the top lane keeps one bar, and the bottom lane gets a new bar for each room. Close chat ends both. Open chat starts a new ChatRoom, with a new bar in each lane. Pick a room while the chat is closed, and nothing happens until you open it.

    A timeline. Top lane: ChatRoom is on the page in one long bar, from when it appears to Close chat. Bottom lane: the effect synchronizes three times, one after another: with "general", then "travel", then "music". Below, the moments that start and stop each one: appears, travel, music, Close chat.componentChatRoom on the pageeffect"general""travel""music"appearstravelmusicClose chat
    While ChatRoom stays on the page, its effect stops and starts again each time roomId changes. The component mounted once; the effect synchronized three times.

    Each row asks the same question of the component and of its effect:

    A component's life and an effect's
    QuestionThe componentIts effect
    What can happen to it?It mounts, updates any number of times, and unmounts.It starts synchronizing, and later stops. That can happen many times.
    What makes it happen?A parent shows it, its state or props change, or a parent removes it.A render where a dependency changed, or the component leaving the page.
    How many at once?One component, on the page from mount to unmount.One synchronization at a time per effect: the old one stops before the new one starts.
    What does its code describe?What the screen shows for the current props and state.How to start, and how to stop, for the values of one render.

    This is also why StrictMode stops and starts every new effect once in development. If the effect survives a stop and a start straight away, it will survive one when the room changes too.

    How React re-synchronizes

    When you press travel, React renders ChatRoom with roomId "travel" and puts the new heading on the page. Then it compares the dependency list with last time, using Object.is: "general" and "travel" differ, so it re-synchronizes, in two steps:

    1. It calls the cleanup that the effect returned after the last render. That cleanup still holds "general".
    2. It runs the effect from the new render, which holds "travel".
    Two windows. Render 1, with roomId "general", holds its effect, connect general, and its cleanup, disconnect general. Render 2, with roomId "travel", holds connect travel and disconnect travel. When you pick travel, step 1 is the cleanup from render 1, disconnect general, and step 2 is the effect from render 2, connect travel.render 1roomId = "general"effect: connect generalcleanup: disconnect generalrender 2roomId = "travel"effect: connect travelcleanup: disconnect travelYou pick travel:1. disconnect general2. connect travelfrom render 1from render 2
    Every render makes its own effect and cleanup, and each remembers that render's roomId. When you pick travel, React runs the cleanup from render 1, then the effect from render 2.

    Each render makes its own effect function and its own cleanup, and each sees that render's roomId, the way any function remembers the variables around it where it was made (a closure). That's why the cleanup disconnects from the right room without you storing the old one anywhere.

    The same two steps cover the component's whole life. When ChatRoom leaves the page, React calls the last cleanup and runs nothing after it. Close the chat, press general, and open it again: a brand-new ChatRoom appears, and its effect starts with "general", with StrictMode's check again, because the component is new.

    Reactive values: props, state, and what you work out from them

    roomId is a reactive value: a value declared inside the component, so a render can give it a new value. Props are reactive, state is reactive, and so is anything you work out from them in the component body. This chat lets you switch to a backup server:

    A server address worked out from statePop out in the code editor (opens in a new tab)JSX
    import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; function ChatRoom({ roomId }) {  const [useBackup, setUseBackup] = useState(false);  const serverUrl = useBackup ? "https://backup.example.com" : "https://chat.example.com";   useEffect(() => {    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [serverUrl, roomId]);   return (    <>      <h3>Welcome to the {roomId} room</h3>      <label>        <input type="checkbox" checked={useBackup} onChange={() => setUseBackup(!useBackup)} /> Use the backup server      </label>    </>  );} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
    ResultLoading…

    Line 5 keeps useBackup in state. Line 6 works out serverUrl from it. serverUrl isn't state, but it's made again on every render from state, so it can change: it's reactive. The effect reads it, so line 12 lists it next to roomId.

    Tick Use the backup server. The console prints Stop: disconnect from "general" at https://chat.example.com, then Start: connect to "general" at https://backup.example.com. Same room, new server, so the effect stops and starts again. Pick travel: it stops at the backup server and starts travel there.

    Left, reactive values, which the effect must list: roomId, a prop; useBackup, state; serverUrl, worked out from them. Right, values that are not dependencies: a const above the component, setMessages and inputRef, which never change, and mutedRef.current and location.pathname, which change without a render, so they can't be listed.Reactive: list themroomId: a propuseBackup: stateserverUrl: worked outNot dependenciesa const above itsetMessages, inputRefchange without a rendermutedRef.currentlocation.pathname
    Anything a render can change is reactive, and an effect that reads it must list it. Values that never change don't need to be listed. Values that change without a render, like a ref's current, can't be.

    Now move both values out of the component:

    An effect that reads nothing reactivePop out in the code editor (opens in a new tab)JSX
    import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; const serverUrl = "https://chat.example.com";const roomId = "general"; function ChatRoom() {  useEffect(() => {    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, []);   return <h3>Welcome to the {roomId} room</h3>;} export default function App() {  const [show, setShow] = useState(true);  return (    <>      <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      {show && <ChatRoom />}    </>  );}
    ResultLoading…

    Lines 4 and 5 are made once, when the module loads, and no render can change them. The effect reads nothing reactive, so its list on line 12 is empty, and the linter agrees. From the effect's point of view, [] means: start when ChatRoom appears, and stop when it goes. Press Close chat: one stop. That's the only time a list of dependencies means "mount and unmount", and it's because nothing in it can change.

    Every reactive value is a dependency

    Here is the backup-server chat with one change: line 12 lists only roomId.

    A dependency left out (a bug on purpose)Pop out in the code editor (opens in a new tab)JSX
    import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; function ChatRoom({ roomId }) {  const [useBackup, setUseBackup] = useState(false);  const serverUrl = useBackup ? "https://backup.example.com" : "https://chat.example.com";   useEffect(() => {    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [roomId]);   return (    <>      <h3>Welcome to the {roomId} room</h3>      <label>        <input type="checkbox" checked={useBackup} onChange={() => setUseBackup(!useBackup)} /> Use the backup server      </label>    </>  );} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
    ResultLoading…

    Tick Use the backup server. Nothing prints. The checkbox is ticked, serverUrl is the backup address in this render, and the connection is still on the main server: React compared [roomId], found it the same, and left the effect alone. Now pick travel. The console prints Stop: disconnect from "general" at https://chat.example.com and Start: connect to "travel" at https://backup.example.com. The server change arrived late, carried in by a room change.

    React's ESLint plugin, eslint-plugin-react-hooks, catches this before you run the code. Its rule react-hooks/exhaustive-deps reads the effect, finds every reactive value it uses, and compares them with the list. For line 12 it says:

    The linter's warning for line 12Text
    React Hook useEffect has a missing dependency: 'serverUrl'. Either include it or remove the dependency array.

    You don't choose an effect's dependencies; its code does. Every reactive value it reads goes in the list. If you want a value to stop being a dependency, change the code so it isn't reactive, as the section on the linter shows.

    What isn't reactive

    Three kinds of value an effect can read without listing them:

    • Values outside the component, like serverUrl above ChatRoom. They're made once.
    • Stable values React promises never to change: the set function from useState and the object useRef returns.
    • Values made inside the effect itself. The effect makes them; it doesn't read them from the component.
    An effect with stable values (part of a component)JSX
    function ChatRoom({ roomId }) {
      const [messages, setMessages] = useState([]);
      const inputRef = useRef(null);
    
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        setMessages([]);
        inputRef.current.focus();
        return () => connection.disconnect();
      }, [roomId]);
    }

    This fragment isn't meant to run on its own. Line 8 calls setMessages and line 9 uses inputRef, and line 11 lists neither. React gives back the same setMessages and the same ref object on every render, so they can't make the effect start again. Listing them is allowed and changes nothing. The linter knows this and doesn't ask.

    A ref's current is different. You change it by assigning to it, and that doesn't render anything. So it isn't reactive, and putting it in the list doesn't make it one:

    A ref's current value in the list (a bug on purpose)Pop out in the code editor (opens in a new tab)JSX
    import { useEffect, useRef, useState } from "react"; function ChatRoom({ roomId }) {  const mutedRef = useRef(false);  const [draft, setDraft] = useState("");   useEffect(() => {    console.log(`Start: "${roomId}", muted: ${mutedRef.current}`);    return () => console.log(`Stop: "${roomId}"`);  }, [roomId, mutedRef.current]);   return (    <>      <button onClick={() => (mutedRef.current = !mutedRef.current)}>Mute</button>{" "}      <input aria-label="Message" value={draft} onChange={(event) => setDraft(event.target.value)} />    </>  );} export default function App() {  return <ChatRoom roomId="general" />;}
    ResultLoading…

    Line 4 keeps muted in a ref, and line 10 lists mutedRef.current. Line 14's Mute button flips it. Press Mute: nothing prints. Now type a letter in the box. The console prints Stop: "general" and Start: "general", muted: true. React only compares the list while it renders, and typing caused the next render, so the effect followed the mute late, and for the wrong reason. The linter flags line 10:

    The linter's warning for line 10Text
    React Hook useEffect has an unnecessary dependency: 'mutedRef.current'. Either exclude it or remove the dependency array. Mutable values like 'mutedRef.current' aren't valid dependencies because mutating them doesn't re-render the component.

    If an effect must follow a value, the value must be state: exercise 5 makes that change. Values the browser changes, like location.pathname, are the same kind of trap:

    A browser value in the list (part of a component)JSX
    function ChatRoom({ roomId }) {
      useEffect(() => {
        logVisit(roomId, location.pathname);
      }, [roomId, location.pathname]);
    }
    The linter's warning for line 4Text
    React Hook useEffect has an unnecessary dependency: 'location.pathname'. Either exclude it or remove the dependency array. Outer scope values like 'location.pathname' aren't valid dependencies because mutating them doesn't re-render the component.

    The browser changes location.pathname without telling React. To read it and render when it changes, subscribe to it with useSyncExternalStore, as You might not need an effect does for the online status. Each row says whether an effect must list a kind of value:

    Which values an effect must list
    A value the effect readsMust it be listed?Why
    A prop, like roomIdYesA parent can pass a new one on any render.
    State, like useBackupYesSetting it renders the component with a new value.
    A value worked out from them, like serverUrlYesIt's made again on every render, from values that change.
    A const above the componentNoIt's made once when the module loads and never changes.
    A set function, or the object useRef returnsNoReact gives back the same one on every render. Listing it is allowed and changes nothing.
    A ref's current, or location.pathnameNo, and it can't beIt changes without a render, so React would only see the change at some later, unrelated render.

    Why the linter is right

    Every warning from react-hooks/exhaustive-deps points at a value that can change while the effect keeps using an old one. That's exactly the missing-server bug. The tempting fix is a comment that silences the rule:

    Silencing the linter (part of ChatRoom)JSX
    useEffect(() => {
      const connection = createConnection(serverUrl, roomId);
      connection.connect();
      return () => connection.disconnect();
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [roomId]);

    Line 5 tells ESLint to skip the next line. The warning disappears and the bug stays: this fragment is the missing-server effect again. A silenced warning is where to look first when an effect uses old values. If you never silence the rule, you don't get that kind of bug.

    When an effect re-synchronizes more than you want, change the code so the value isn't reactive. You have already seen one way: move it outside the component. The other is to move it inside the effect:

    A value worked out inside the effectPop out in the code editor (opens in a new tab)JSX
    import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; function ChatRoom({ roomId }) {  useEffect(() => {    const serverUrl = roomId === "music" ? "https://backup.example.com" : "https://chat.example.com";    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [roomId]);   return <h3>Welcome to the {roomId} room</h3>;} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
    ResultLoading…

    Line 6 picks the server inside the effect, from roomId: the music room uses the backup server. The effect reads only roomId from the component, so [roomId] on line 10 is the complete list, and the linter is quiet. Press music: Start: connect to "music" at https://backup.example.com.

    Two harder cases get lessons of their own. Sometimes an effect needs the latest value of something without re-synchronizing when it changes; useEffectEvent, added in React 19.2, does that (Separating events from effects). And sometimes a dependency like an object or a function changes on every render; Removing effect dependencies shows how to change the code so it doesn't. Neither case is solved by silencing the linter.

    One effect for each synchronization

    A real chat also records a visit when you open a room, for analytics. logVisit, in analytics.js, prints a line instead of sending it. It's tempting to add the call to the effect you already have:

    One effect doing two jobsPop out in the code editor (opens in a new tab)JSX
    import { useEffect, useState } from "react";import { createConnection } from "./chat.js";import { logVisit } from "./analytics.js"; function ChatRoom({ roomId }) {  const [useBackup, setUseBackup] = useState(false);  const serverUrl = useBackup ? "https://backup.example.com" : "https://chat.example.com";   useEffect(() => {    logVisit(roomId);    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [serverUrl, roomId]);   return (    <>      <h3>Welcome to the {roomId} room</h3>      <label>        <input type="checkbox" checked={useBackup} onChange={() => setUseBackup(!useBackup)} /> Use the backup server      </label>    </>  );} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
    ResultLoading…

    Lines 9 to 14 record the visit and connect, in one effect, which lists both values it reads. It works until you tick Use the backup server: the console prints Visit: "general" again, between the stop and the start. Nobody visited anything; the server changed, and the visit came along because it shares an effect with the connection.

    Two effects, two synchronizationsPop out in the code editor (opens in a new tab)JSX
    import { useEffect, useState } from "react";import { createConnection } from "./chat.js";import { logVisit } from "./analytics.js"; function ChatRoom({ roomId }) {  const [useBackup, setUseBackup] = useState(false);  const serverUrl = useBackup ? "https://backup.example.com" : "https://chat.example.com";   useEffect(() => {    logVisit(roomId);  }, [roomId]);   useEffect(() => {    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [serverUrl, roomId]);   return (    <>      <h3>Welcome to the {roomId} room</h3>      <label>        <input type="checkbox" checked={useBackup} onChange={() => setUseBackup(!useBackup)} /> Use the backup server      </label>    </>  );} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
    ResultLoading…

    Lines 9 to 11 record a visit, and follow only roomId. Lines 13 to 17 keep the connection, and follow serverUrl and roomId. Tick the backup server: a stop and a start, and no visit. Pick travel: both effects run. Split effects by what they keep in step, not by when they happen to run. Each one then lists exactly what it reads, and starts and stops only for its own reasons.

    Must it be a dependency?
    • roomId, a prop
    • useBackup, a state variable
    • const serverUrl = useBackup ? backup : main; in the component
    • const serverUrl = "https://chat.example.com"; above the component
    • mutedRef.current
    • location.pathname
    • setMessages, a set function from useState
    • A const made inside the effect itself
    Try it yourself
    0 of 8 correct

    Sort each value an effect inside ChatRoom reads.

    Choose a category for every card. You can change an answer at any time; Reset clears them all.

    Common mix-ups

    • "An effect runs on mount and its cleanup on unmount." No. It starts and stops each time a value it reads changes. Mount and unmount are only the first start and the last stop.
    • "Leaving a value out of the list makes the effect run less." No. It makes the effect keep an old value, like the main server after you picked the backup.
    • "An empty list means run once." No. It means the effect reads nothing reactive, so it starts and stops with the component. StrictMode still stops and starts it once in development.
    • "Putting ref.current in the list makes React watch it." No. React only compares the list while rendering, and changing a ref doesn't render.
    • "Silencing the linter is fine when you know the value won't change." No. If it can't change, move it outside the component and the warning goes away on its own.
    • "Set functions must be in the list." No. They never change. Listing them is allowed and does nothing.

    Practice exercises

    Each exercise runs live with chat.js. Read the console under the result after every click.

    Exercise 1 · Warm-upCount the stops

    The chat starts in general. You press travel, then music. Not counting StrictMode's check at the start, how many Stop lines print? Answer first, then press Edit & run.

    import { useEffect, useState } from "react";
    import { createConnection } from "./chat.js";
    
    const serverUrl = "https://chat.example.com";
    
    function ChatRoom({ roomId }) {
      useEffect(() => {
        const connection = createConnection(serverUrl, roomId);
        connection.connect();
        return () => connection.disconnect();
      }, [roomId]);
    
      return <h3>Welcome to the {roomId} room</h3>;
    }
    
    export default function App() {
      const [roomId, setRoomId] = useState("general");
      const [show, setShow] = useState(true);
      return (
        <>
          <p>
            {["general", "travel", "music"].map((room) => (
              <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>
            ))}{" "}
            <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>
          </p>
          {show && <ChatRoom roomId={roomId} />}
        </>
      );
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 2 · Warm-upFill in the dependencies

      The effect has an empty list, so ticking the backup server or picking a room does nothing. Fix line 12. What dependency array does the effect need?

      import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; function ChatRoom({ roomId }) {  const [useBackup, setUseBackup] = useState(false);  const serverUrl = useBackup ? "https://backup.example.com" : "https://chat.example.com";   useEffect(() => {    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, []);   return (    <>      <h3>Welcome to the {roomId} room</h3>      <label>        <input type="checkbox" checked={useBackup} onChange={() => setUseBackup(!useBackup)} /> Use the backup server      </label>    </>  );} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
      ResultLoading…

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 3 · PracticeFollow the server

        Ticking Use the backup server prints nothing. Make the effect follow the server. After your fix, you tick the box: which server does the new Start line name?

        import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; function ChatRoom({ roomId }) {  const [useBackup, setUseBackup] = useState(false);  const serverUrl = useBackup ? "https://backup.example.com" : "https://chat.example.com";   useEffect(() => {    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [roomId]);   return (    <>      <h3>Welcome to the {roomId} room</h3>      <label>        <input type="checkbox" checked={useBackup} onChange={() => setUseBackup(!useBackup)} /> Use the backup server      </label>    </>  );} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
        ResultLoading…

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 4 · PracticeSwitch the chat off

          Unticking Chat on says "Chat is off", but the connection stays open. Make the effect stop synchronizing while isOn is false. After your fix, you untick the box: what does the console print?

          import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; const serverUrl = "https://chat.example.com"; function ChatRoom({ roomId, isOn }) {  useEffect(() => {    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [roomId]);   return <h3>{isOn ? `Connected to ${roomId}` : "Chat is off"}</h3>;} export default function App() {  const [isOn, setIsOn] = useState(true);  return (    <>      <label>        <input type="checkbox" checked={isOn} onChange={() => setIsOn(!isOn)} /> Chat on      </label>      <ChatRoom roomId="general" isOn={isOn} />    </>  );}
          ResultLoading…

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 5 · ChallengeMute straight away

            Pressing Mute doesn't restart the effect until you type. Make it restart right away, and quiet the linter. After your fix, you press Mute once: what is the last line in the console?

            import { useEffect, useRef, useState } from "react"; function ChatRoom({ roomId }) {  const mutedRef = useRef(false);  const [draft, setDraft] = useState("");   useEffect(() => {    console.log(`Start: "${roomId}", muted: ${mutedRef.current}`);    return () => console.log(`Stop: "${roomId}"`);  }, [roomId, mutedRef.current]);   return (    <>      <button onClick={() => (mutedRef.current = !mutedRef.current)}>Mute</button>{" "}      <input aria-label="Message" value={draft} onChange={(event) => setDraft(event.target.value)} />    </>  );} export default function App() {  return <ChatRoom roomId="general" />;}
            ResultLoading…

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 6 · ChallengeSplit the effect

              Ticking the backup server records a visit that never happened. Split the effect so each synchronization has its own. After your fix, you tick Use the backup server: how many Visit lines print?

              import { useEffect, useState } from "react";import { createConnection } from "./chat.js";import { logVisit } from "./analytics.js"; function ChatRoom({ roomId }) {  const [useBackup, setUseBackup] = useState(false);  const serverUrl = useBackup ? "https://backup.example.com" : "https://chat.example.com";   useEffect(() => {    logVisit(roomId);    const connection = createConnection(serverUrl, roomId);    connection.connect();    return () => connection.disconnect();  }, [serverUrl, roomId]);   return (    <>      <h3>Welcome to the {roomId} room</h3>      <label>        <input type="checkbox" checked={useBackup} onChange={() => setUseBackup(!useBackup)} /> Use the backup server      </label>    </>  );} export default function App() {  const [roomId, setRoomId] = useState("general");  const [show, setShow] = useState(true);  return (    <>      <p>        {["general", "travel", "music"].map((room) => (          <button key={room} aria-pressed={room === roomId} onClick={() => setRoomId(room)}>{room}</button>        ))}{" "}        <button onClick={() => setShow(!show)}>{show ? "Close chat" : "Open chat"}</button>      </p>      {show && <ChatRoom roomId={roomId} />}    </>  );}
              ResultLoading…

              Answer, then press Check. Spacing and letter case don’t matter.

                Check your understanding

                Read each effect on its own: what it starts, what it stops, and which values can make it do that again.

                The lifecycle of an effect quiz · 7 questionsScore: first tries count
                1. Question 1 of 7From the effect's own point of view, what does it do?

                  Choose an answer to see the explanation.

                2. Question 2 of 7ChatRoom is connected to general. You pick travel. What does chat.js print, in order?

                  Read the code, then predictJSX
                  function ChatRoom({ roomId }) {
                    useEffect(() => {
                      const connection = createConnection(serverUrl, roomId);
                      connection.connect();
                      return () => connection.disconnect();
                    }, [roomId]);
                    return <h3>Welcome to the {roomId} room</h3>;
                  }

                  Choose an answer to see the explanation.

                3. Question 3 of 7Which values in this ChatRoom are reactive?

                  Read the code, then predictJSX
                  const serverUrl = "https://chat.example.com";
                  
                  function ChatRoom({ roomId }) {
                    const [useBackup, setUseBackup] = useState(false);
                    const room = roomId.toLowerCase();
                    const inputRef = useRef(null);
                    // ...
                  }

                  Choose an answer to see the explanation.

                4. Question 4 of 7Why can't mutedRef.current be a dependency?

                  Choose an answer to see the explanation.

                5. Question 5 of 7An effect reads only values declared above the component, and its array is []. When does it start and stop?

                  Choose an answer to see the explanation.

                6. Question 6 of 7The linter says serverUrl is a missing dependency. What's a right fix?

                  Choose an answer to see the explanation.

                7. Question 7 of 7Does an effect that calls setMessages have to list it?

                  Choose an answer to see the explanation.

                Key takeaways

                • An effect starts synchronizing and later stops. It can do that many times while its component stays on the page.
                • When a dependency changes, React runs the cleanup from the last render, then the effect from the new one.
                • Reactive values are props, state, and values worked out from them in the component body.
                • Every reactive value an effect reads must be in its list. Leaving one out keeps an old value.
                • Module values, set functions, and ref objects aren't reactive. A ref's current value and browser values can't be dependencies.
                • The linter is right: fix a warning by changing the code, never by silencing it. Give each synchronization its own effect.

                An effect starts and stops; React restarts it when what it reads changes.
                List everything it reads, and let the code decide the list.

                Coming next: Separating events from effects. It shows how an effect can read the latest value of something without starting again when it changes.

                CompleteFrontend Clear concepts. Working examples.