cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

Removing effect dependencies

Stop needless Effect re-runs by changing the code, not the dependency list: event handlers, split Effects, updaters, Effect Events, and objects.

By the end, you can
  • 01
    Find why an Effect re-runsRead an Effect's dependencies and tell which ones change when nothing it does should change.
  • 02
    Remove a dependency by changing the codeMove code into a handler, split the Effect, use an updater or an Effect Event, so the linter agrees the value isn't needed.
  • 03
    Keep objects and functions from re-syncingBuild them inside the Effect, outside the component, or depend on the strings and numbers they hold.

A chat that reconnects on every keystroke

This chat room keeps its server settings in an options object and connects with an Effect. The chat.js tab is a pretend server that numbers every connection, so a needless reconnect is easy to spot. Type a few letters in the message box and watch the console under the result.

A chat whose Effect depends on an objectPop 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 }) {  const [message, setMessage] = useState("");   const options = { serverUrl, roomId };   useEffect(() => {    const connection = createConnection(options);    connection.connect();    return () => connection.disconnect();  }, [options]);   return (    <label>      Message <input value={message} onChange={(e) => setMessage(e.target.value)} />    </label>  );} export default function App() {  const [roomId, setRoomId] = useState("general");   return (    <div>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <h3>Room: {roomId}</h3>      <ChatRoom roomId={roomId} />    </div>  );}
ResultLoading…

Line 9 builds options from serverUrl and roomId. The Effect on lines 11 to 15 reads it, so line 15 lists [options]: every value the Effect reads is in the list. The first render logs connections 1 and 2, the development check from Synchronizing with effects. Then every letter you type logs Disconnected from general and a new connection: 3, 4, 5. The room never changed.

Typing changes message, so ChatRoom renders again, and line 9 runs again. It makes a brand-new object. React compares each dependency with the last render's using Object.is, and two objects made separately are never equal, even when they hold the same things. So React decides options changed and re-syncs.

The linter sees this coming. It has nothing to add to line 15, but it warns about line 9: The 'options' object makes the dependencies of useEffect Hook (at line 15) change on every render. Move it inside the useEffect callback. In plain words: this object is new on every render, so the Effect will run after every render. The section on objects and functions, further down, makes that change.

Two rows, comparing render 1 with render 2. Row one: an object with serverUrl and roomId in each render. Object.is says false, so the chat reconnects. Row two: the string "general" in each render. Object.is says true, so the Effect is skipped.render 1render 2Object.is{ serverUrl, roomId }{ serverUrl, roomId }falsereconnects"general""general"trueEffect skipped
Each render makes a new options object. It holds the same things, but Object.is compares objects by identity, so it says false and React reconnects. The string "general" is equal to itself in every render.
Real-life analogyParcels and tracking numbers

A warehouse sends a van whenever a parcel has a tracking number it hasn't seen. A clerk repacks the same book in a fresh box every hour, and each box gets a new number. The warehouse never opens the boxes, so it sends a van every hour, for the same book. If the clerk just kept the address on a label, the warehouse would see the same address and send nothing.

In real life: The warehouse checks only the tracking number
In React: React compares dependencies with Object.is
In real life: The same book, packed in a new box with a new number
In React: A new { serverUrl, roomId } object on every render
In real life: A new number means a new delivery run
In React: A changed dependency means the Effect runs again
In real life: Writing just the address on the label
In React: Depending on the string roomId instead
The main idea

The dependency array must list every reactive value the Effect reads; you don't choose it. To make an Effect run less often, change its code so it no longer needs the value, and then the linter agrees the value can go.

Change the code, not the dependency list

The quick way out is to tell the linter to be quiet. Here a product review box saves your draft every 2 seconds, and someone silenced the linter to keep the timer from restarting. Type Great tea and watch what gets saved.

An auto-save with the linter silenced (a bug on purpose)Pop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { saveDraft } from "./drafts.js"; export default function App() {  const [draft, setDraft] = useState("");   useEffect(() => {    const id = setInterval(() => saveDraft(draft), 2000);    return () => clearInterval(id);    // eslint-disable-next-line react-hooks/exhaustive-deps  }, []);   return <input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Write a review" />;}
ResultLoading…

Line 8 starts a timer that saves draft. Line 10 silences the linter, so line 11 says []. The console keeps printing Saved draft: "": the timer's callback is a closure from the first render, when draft was empty, and with [] React never replaces it. The silenced linter can't warn you, so the bug hides until someone loses a review.

Do what the linter asked, and list draft:

The auto-save with draft in the arrayPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { saveDraft } from "./drafts.js"; export default function App() {  const [draft, setDraft] = useState("");   useEffect(() => {    const id = setInterval(() => saveDraft(draft), 2000);    return () => clearInterval(id);  }, [draft]);   return <input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Write a review" />;}
ResultLoading…

Line 10 lists [draft]. Each letter re-runs the Effect: the cleanup on line 9 stops the old timer and a new one starts with the new text. Type, stop, and 2 seconds later the console says Saved draft: "Great tea". Restarting here is fine: the timer really should follow the text.

When a re-run really isn't wanted, as with the chat above, don't edit the array. Change the code, until the value isn't needed any more. The rest of this lesson is five questions to ask about the value that keeps changing.

Five rows, each a question and the change it leads to. Caused by a click: move it to the handler. Two unrelated jobs: split the Effect. Reads state to set state: use an updater. Read, but don't react: use an Effect Event. Object or function from render: build it in the Effect.Why does it re-run?Change the codeCaused by a click?move it to the handlerTwo unrelated jobs?split the EffectReads state to set state?use an updaterRead, but don't react?use an Effect EventObject or function from render?build it in the Effect
When an Effect re-runs too often, ask these five questions about the value that keeps changing. Each answer is a change to the code; once it's made, the linter agrees the value can leave the array.

How to read the table: each row is a reason an Effect re-runs, the change to the code, and what the array becomes.

Five ways to remove a dependency by changing the code
What makes it re-runHow to change the codeWhat the array becomes
A value only a click should use, like the theme after an order.Move the code into that click's event handler.The value leaves the array, or the Effect goes away.
A value that a second, unrelated job reads.Split the Effect into one Effect per job.Each Effect lists only what its own job reads.
State the Effect reads only to work out the next state.Pass an updater function to the set function.The state leaves the array.
A value it should read but not react to.Read it inside an Effect Event.The value leaves the array.
An object or function made during render.Make it inside the Effect or outside the component, or depend on the strings and numbers it holds.Only values that stay equal when they don't really change.

Code a click causes goes in the handler

First question: should this code run at all when the value changes? This order page posts the order from an Effect once submitted is true, and shows a thank-you in the current theme. Click Place order, then the theme button.

Posting an order from an Effect (a bug on purpose)Pop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { post } from "./api.js"; export default function App() {  const [submitted, setSubmitted] = useState(false);  const [theme, setTheme] = useState("light");   useEffect(() => {    if (submitted) {      post("/api/order");      console.log(`Thanks shown in ${theme}`);    }  }, [submitted, theme]);   return (    <div>      <button onClick={() => setSubmitted(true)}>Place order</button>      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Theme: {theme}</button>    </div>  );}
ResultLoading…

The Effect on lines 8 to 13 reads submitted and theme, so line 13 lists both. Placing the order logs POST /api/order. Changing the theme runs the Effect again, submitted is still true, and line 10 posts the order a second time. No list of dependencies fixes this, because the code shouldn't be reacting to anything: the click is the reason. As You might not need an effect showed, it belongs in the handler:

Posting the order from the click's handlerPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { post } from "./api.js"; export default function App() {  const [theme, setTheme] = useState("light");   function handleOrder() {    post("/api/order");    console.log(`Thanks shown in ${theme}`);  }   return (    <div>      <button onClick={handleOrder}>Place order</button>      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Theme: {theme}</button>    </div>  );}
ResultLoading…

handleOrder on lines 7 to 10 posts and reads theme at the moment of the click. Line 14 calls it. The Effect, and its dependencies, are gone, and changing the theme logs nothing.

Two unrelated jobs need two Effects

Second question: is the Effect doing two separate jobs? This chat connects to a room and also shows the unread count in the tab title, through the title.js tab. Click New message a few times.

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 { updateTitle } from "./title.js"; const serverUrl = "https://chat.example.com"; export default function App() {  const [roomId, setRoomId] = useState("general");  const [unread, setUnread] = useState(0);   useEffect(() => {    const connection = createConnection({ serverUrl, roomId });    connection.connect();    updateTitle(`(${unread}) ${roomId}`);    return () => connection.disconnect();  }, [roomId, unread]);   return (    <div>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <button onClick={() => setUnread(unread + 1)}>New message</button>      <p>{unread} unread in {roomId}</p>    </div>  );}
ResultLoading…

Lines 12 and 13 connect, and line 14 updates the title. Because both live in one Effect, line 16 lists roomId and unread, and every new message logs Disconnected from general and a new connection, just to change a title. Give each job its own Effect:

import { useEffect, useState } from "react";import { createConnection } from "./chat.js";import { updateTitle } from "./title.js"; const serverUrl = "https://chat.example.com"; export default function App() {  const [roomId, setRoomId] = useState("general");  const [unread, setUnread] = useState(0);   useEffect(() => {    const connection = createConnection({ serverUrl, roomId });    connection.connect();    return () => connection.disconnect();  }, [roomId]);   useEffect(() => {    updateTitle(`(${unread}) ${roomId}`);  }, [unread, roomId]);   return (    <div>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <button onClick={() => setUnread(unread + 1)}>New message</button>      <p>{unread} unread in {roomId}</p>    </div>  );}
ResultLoading…

The connection on lines 11 to 15 lists only [roomId]. The title on lines 17 to 19 lists [unread, roomId], because it reads both. Now New message logs only Title: (1) general. Switch room runs both, because both jobs need the new room.

Two halves. Left: one Effect connects to roomId and updates the title, with roomId and unread in its array, so a new message reconnects. Right: one Effect connects, with roomId in its array, and another updates the title, with unread and roomId. A new message only updates the title.One Effect, two jobsEffectconnect to roomIdupdate the title[roomId, unread]a new message reconnectsOne Effect per jobEffectconnect to roomId[roomId]Effectupdate the title[unread, roomId]
On the left, unread is a dependency of the connection too, so each new message reconnects. On the right, each job has its own Effect and its own array, and a new message only runs the title's Effect.

Split by job, not by line count: if two pieces of code must always run together with the same values, they're one job and stay in one Effect.

Use an updater to read the previous state

Third question: does the Effect read some state only to work out the next state? This chat adds each incoming message to a list. The Receive a message button stands in for someone else sending one: it calls receive from chat.js. Click it twice.

Reading messages to add a messagePop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { createConnection, receive } from "./chat.js"; const serverUrl = "https://chat.example.com"; export default function App() {  const [messages, setMessages] = useState([]);   useEffect(() => {    const connection = createConnection({ serverUrl, roomId: "general" });    connection.on("message", (text) => {      setMessages([...messages, text]);    });    connection.connect();    return () => connection.disconnect();  }, [messages]);   return (    <div>      <button onClick={() => receive("Asha: Is the tea ready?")}>Receive a message</button>      <ul>        {messages.map((text, index) => (          <li key={index}>{text}</li>        ))}      </ul>    </div>  );}
ResultLoading…

Line 12 builds the new list from messages, so line 16 has to list [messages]. Each message changes messages, which re-runs the Effect: disconnect, connection 3; disconnect, connection 4. A busy room would reconnect for every message. Pass an updater function instead, as in Queueing a series of state updates:

Adding a message with an updaterPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { createConnection, receive } from "./chat.js"; const serverUrl = "https://chat.example.com"; export default function App() {  const [messages, setMessages] = useState([]);   useEffect(() => {    const connection = createConnection({ serverUrl, roomId: "general" });    connection.on("message", (text) => {      setMessages((list) => [...list, text]);    });    connection.connect();    return () => connection.disconnect();  }, []);   return (    <div>      <button onClick={() => receive("Asha: Is the tea ready?")}>Receive a message</button>      <ul>        {messages.map((text, index) => (          <li key={index}>{text}</li>        ))}      </ul>    </div>  );}
ResultLoading…

Line 12 now passes (list) => [...list, text]. React calls it with the latest list, so the Effect never reads messages, and line 16 is [], with the linter's agreement. Receiving messages adds them to the page and logs nothing.

Two rows. Top: setMessages with messages spread in reads messages from the render, so messages must be in the array. Bottom: setMessages with an updater function; React passes it the latest list, so there's nothing to list.Reads state from the rendersetMessages([...messages, text])reads messagesmessages in the arrayLets React pass the latest listsetMessages((list) => [...list, text])React: latest listnothing to list
At the top, the Effect reads messages to build the next list, so messages is a dependency and every message reconnects. At the bottom, React hands the updater the latest list, and the Effect doesn't read messages at all.

Read a value without reacting to it

Fourth question: does the Effect need a value's latest version without needing to re-sync when it changes? That's what an Effect Event is for, as Separating events from effects showed with a theme. A common case is a function passed down as a prop. Here App passes onMessage to count unread messages. Click Receive a message twice.

An Effect that depends on a parent's functionPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { createConnection, receive } from "./chat.js"; const serverUrl = "https://chat.example.com"; function ChatRoom({ roomId, onMessage }) {  useEffect(() => {    const connection = createConnection({ serverUrl, roomId });    connection.on("message", (text) => onMessage(text));    connection.connect();    return () => connection.disconnect();  }, [roomId, onMessage]);   return <button onClick={() => receive("Asha: Hi!")}>Receive a message</button>;} export default function App() {  const [unread, setUnread] = useState(0);   return (    <div>      <p>Unread: {unread}</p>      <ChatRoom roomId="general" onMessage={() => setUnread(unread + 1)} />    </div>  );}
ResultLoading…

Line 9 calls onMessage, so line 12 lists it. But line 23 creates a new arrow function every time App renders, and each message renders App, since unread changes. So every message reconnects the chat. Wrap the prop in an Effect Event inside ChatRoom:

Wrapping the parent's function in an Effect EventPop out in the code editor (opens in a new tab)JSX
import { useEffect, useEffectEvent, useState } from "react";import { createConnection, receive } from "./chat.js"; const serverUrl = "https://chat.example.com"; function ChatRoom({ roomId, onMessage }) {  const onReceive = useEffectEvent(onMessage);   useEffect(() => {    const connection = createConnection({ serverUrl, roomId });    connection.on("message", (text) => onReceive(text));    connection.connect();    return () => connection.disconnect();  }, [roomId]);   return <button onClick={() => receive("Asha: Hi!")}>Receive a message</button>;} export default function App() {  const [unread, setUnread] = useState(0);   return (    <div>      <p>Unread: {unread}</p>      <ChatRoom roomId="general" onMessage={() => setUnread(unread + 1)} />    </div>  );}
ResultLoading…

Line 7 makes onReceive from onMessage. Line 11 calls it, and line 14 lists only [roomId]. The count still goes up, because an Effect Event always calls the latest onMessage, but the chat stays connected.

Objects and functions made during render

Fifth question: is the dependency an object or a function that the component body makes? That was the overview's problem. How to read the table: each row compares a value from two renders, what Object.is says, and why.

What Object.is says about values from two renders
What you compareObject.is saysWhy
"general" and "general"trueStrings are compared by what they say.
{ serverUrl, roomId } made in render 1 and in render 2falseTwo objects made separately are different objects, even with the same contents.
createOptions made in render 1 and in render 2falseEach render creates a new function.
options declared outside the componenttrueIt's made once, so both renders see the same object.

The simplest fix is to build the object where it's used, inside the Effect. Then the Effect reads roomId, a string, instead of options:

Building options inside the EffectPop 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 }) {  const [message, setMessage] = useState("");   useEffect(() => {    const options = { serverUrl, roomId };    const connection = createConnection(options);    connection.connect();    return () => connection.disconnect();  }, [roomId]);   return (    <label>      Message <input value={message} onChange={(e) => setMessage(e.target.value)} />    </label>  );} export default function App() {  const [roomId, setRoomId] = useState("general");   return (    <div>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <h3>Room: {roomId}</h3>      <ChatRoom roomId={roomId} />    </div>  );}
ResultLoading…

Line 10 makes options inside the Effect, and line 14 lists [roomId]. Type in the box: nothing. Switch the room: one disconnect and one connect, which is the re-sync you want. Functions work the same way. A function declared in the component body is a new function on every render, so an Effect that depends on it re-runs every time. Move it inside the Effect too. Try all three versions in the playground: type a few letters, then switch rooms, and watch Connections made.

Playground: does typing reconnect the chat?
The room chat, in the version you pickedJSX
import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; const serverUrl = "https://chat.example.com"; function ChatRoom({ roomId }) {  const [message, setMessage] = useState("");   const options = { serverUrl, roomId };   useEffect(() => {    const connection = createConnection(options);    connection.connect();    return () => connection.disconnect();  }, [options]);   return (    <label>      Message <input value={message} onChange={(e) => setMessage(e.target.value)} />    </label>  );} export default function App() {  const [roomId, setRoomId] = useState("general");   return (    <div>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <h3>Room: {roomId}</h3>      <ChatRoom roomId={roomId} />    </div>  );}
The pageConnections made: 2

Room: general

  1. Connection 1: connected to general
  2. Disconnected from general
  3. Connection 2: connected to general
Try it yourself
3 new lines in the log.

ChatRoom appeared and connected. In development, StrictMode disconnected and connected once more.

This playground replays React's rules for the code shown: it doesn't run the code itself. The lesson's test runs the same three versions with real React 19.3.1 and checks that they log exactly these lines. Changing the version starts the chat over.

Two more fixes cover the other cases. If the object doesn't use props or state at all, declare it outside the component: it's made once, isn't reactive, and doesn't belong in the array (exercise 2). And if the object comes from a parent as a prop, you can't move it, but you can read the strings inside it before the Effect:

Reading strings out of an object propPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; function ChatRoom({ options }) {  const { serverUrl, roomId } = options;   useEffect(() => {    const connection = createConnection({ serverUrl, roomId });    connection.connect();    return () => connection.disconnect();  }, [serverUrl, roomId]);   return <h3>Welcome to the {roomId} room</h3>;} export default function App() {  const [note, setNote] = useState("");   return (    <div>      <input value={note} onChange={(e) => setNote(e.target.value)} placeholder="A note to yourself" />      <ChatRoom options={{ serverUrl: "https://chat.example.com", roomId: "general" }} />    </div>  );}
ResultLoading…

Line 22 passes a new options object every time App renders, and typing a note renders it. Line 5 takes serverUrl and roomId out of it with destructuring, and line 11 lists those two strings. They're equal from one render to the next, so typing a note logs nothing.

A stock badge that subscribes once per product

Real Effects often need several of these changes at once. This badge subscribes to stock updates for a product, through the stock.js tab. It keeps a list of sales and tells its parent when stock runs low. Click Sell one three times, then switch the product.

A stock badge with three fixesPop out in the code editor (opens in a new tab)JSX
import { useEffect, useEffectEvent, useState } from "react";import { sell, subscribeToStock } from "./stock.js"; function StockBadge({ productId, warehouse, onLowStock }) {  const [sales, setSales] = useState([]);  const onLow = useEffectEvent(onLowStock);   useEffect(() => {    const options = { productId, warehouse };    return subscribeToStock(options, (count) => {      setSales((list) => [...list, `${productId} ${count}`]);      if (count < 3) onLow(count);    });  }, [productId, warehouse]);   return (    <p>      <button onClick={() => sell(productId)}>Sell one</button> Left after each sale: {sales.join(", ") || "none sold yet"}    </p>  );} export default function App() {  const [productId, setProductId] = useState("tea");  const [alerts, setAlerts] = useState(0);   return (    <div>      <button onClick={() => setProductId(productId === "tea" ? "cake" : "tea")}>Product: {productId}</button>      <p>Low-stock alerts: {alerts}</p>      <StockBadge        productId={productId}        warehouse="Leeds"        onLowStock={(count) => {          console.log(`Low stock: ${count} ${productId} left`);          setAlerts(alerts + 1);        }}      />    </div>  );}
ResultLoading…

The Effect on lines 8 to 14 depends only on what the subscription really needs, thanks to three changes:

  • Line 9 builds options inside the Effect, so the array lists the strings productId and warehouse, not a new object.
  • Line 11 adds a sale with an updater, so sales isn't a dependency.
  • Line 6 wraps the parent's onLowStock in an Effect Event. Lines 34 to 37 create a new function on every App render, and each alert renders App, but the subscription doesn't care.

The console shows Subscribed to tea in Leeds (twice in development), then only Low stock: 2 tea left on the third sale. Switching the product logs Unsubscribed from tea and Subscribed to cake in Leeds: the one re-sync that's needed.

Which change removes the dependency?
  • The order is posted again when the theme changes
  • A coupon is applied again when the cart's currency changes
  • A new unread message reconnects the chat, because the title code shares its Effect
  • Switching tabs re-sends the page view, because it shares an Effect with the tab's data
  • Each new message reconnects, because the Effect reads messages to add one
  • Muting the chat reconnects it
  • Typing reconnects, because options is built in the component body
  • Typing reconnects, because the Effect depends on createOptions
Try it yourself
0 of 8 correct

Each card describes an Effect that re-runs when it shouldn't. Sort it by the change that fixes it.

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

Common mix-ups

  • "I decide what goes in the dependency array." No. The code decides: every reactive value the Effect reads. Change the code, and the list changes with it.
  • "Silencing the linter is fine if I know what I'm doing." No. The Effect keeps old values, and the linter stops warning you about every later change too.
  • "An object with the same contents is the same dependency." No. Object.is compares objects by identity. A new object every render is a change every render.
  • "Functions are safe dependencies, because their code doesn't change." No. A function declared in the component body is a new function on every render.
  • "Putting more in one Effect means fewer Effects to worry about." No. One Effect re-runs for all its dependencies. Give separate jobs separate Effects.
  • "[] is always the goal." No. The goal is an array that matches the code. An Effect that should follow roomId must list it.

Practice exercises

Each exercise shows its result under the code, with the console under it. Change the code, and the result runs again as you type. For each one, find the value that keeps changing, and ask which of the five questions fits.

Exercise 1 · Warm-upA tea timer that keeps restarting

Watch the console for a few seconds. Which line does it print every second? Then make the timer start only once.

import { useEffect, useState } from "react"; export default function App() {  const [seconds, setSeconds] = useState(0);   useEffect(() => {    console.log("Starting the timer");    const id = setInterval(() => {      setSeconds(seconds + 1);    }, 1000);    return () => clearInterval(id);  }, [seconds]);   return <p>The tea has brewed for {seconds} seconds.</p>;}
ResultLoading…

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

    Exercise 2 · Warm-upSettings that never change

    Type one letter in the question box. What's the first line it logs? Then stop typing from reconnecting the support chat.

    import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; export default function App() {  const [question, setQuestion] = useState("");   const options = { serverUrl: "https://chat.example.com", roomId: "support" };   useEffect(() => {    const connection = createConnection(options);    connection.connect();    return () => connection.disconnect();  }, [options]);   return (    <label>      Question <input value={question} onChange={(e) => setQuestion(e.target.value)} />    </label>  );}
    ResultLoading…

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

      Exercise 3 · PracticeAn object that comes from a parent

      Typing a note reconnects the chat. Fix ChatRoom without changing App. After your fix, how many lines does one letter in the note log?

      import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; function ChatRoom({ options }) {  useEffect(() => {    const connection = createConnection(options);    connection.connect();    return () => connection.disconnect();  }, [options]);   return <h3>Welcome to the {options.roomId} room</h3>;} export default function App() {  const [note, setNote] = useState("");   return (    <div>      <input value={note} onChange={(e) => setNote(e.target.value)} placeholder="A note to yourself" />      <ChatRoom options={{ serverUrl: "https://chat.example.com", roomId: "general" }} />    </div>  );}
      ResultLoading…

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

        Exercise 4 · PracticeA newsletter that subscribes again

        Type an email, click Subscribe, then add one more letter. How many POST lines does that one letter add? Then make it post only when the button is clicked.

        import { useEffect, useState } from "react";import { post } from "./api.js"; export default function App() {  const [email, setEmail] = useState("");  const [subscribed, setSubscribed] = useState(false);   useEffect(() => {    if (subscribed) {      post(`/api/subscribe?email=${email}`);    }  }, [subscribed, email]);   return (    <div>      <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />      <button onClick={() => setSubscribed(true)}>Subscribe</button>    </div>  );}
        ResultLoading…

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

          Exercise 5 · ChallengeA welcome that reconnects on every like

          Click Like the room once. What's the last line it logs? Then make liking leave the connection alone.

          import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; const serverUrl = "https://chat.example.com"; function ChatRoom({ roomId, onConnected }) {  useEffect(() => {    const connection = createConnection({ serverUrl, roomId });    connection.connect();    onConnected();    return () => connection.disconnect();  }, [roomId, onConnected]);   return <h3>Welcome to the {roomId} room</h3>;} export default function App() {  const [likes, setLikes] = useState(0);   return (    <div>      <button onClick={() => setLikes(likes + 1)}>Like the room ({likes})</button>      <ChatRoom roomId="general" onConnected={() => console.log(`Welcome! Likes so far: ${likes}`)} />    </div>  );}
          ResultLoading…

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

            Exercise 6 · ChallengeA function made during render

            Type Hi in the message box. What's the last line the console logs? Then stop typing from reconnecting.

            import { useEffect, useState } from "react";import { createConnection } from "./chat.js"; const serverUrl = "https://chat.example.com"; function ChatRoom({ roomId }) {  const [message, setMessage] = useState("");   function createOptions() {    return { serverUrl, roomId };  }   useEffect(() => {    const connection = createConnection(createOptions());    connection.connect();    return () => connection.disconnect();  }, [createOptions]);   return (    <label>      Message <input value={message} onChange={(e) => setMessage(e.target.value)} />    </label>  );} export default function App() {  const [roomId, setRoomId] = useState("general");   return (    <div>      <button onClick={() => setRoomId(roomId === "general" ? "travel" : "general")}>Switch room</button>      <h3>Room: {roomId}</h3>      <ChatRoom roomId={roomId} />    </div>  );}
            ResultLoading…

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

              Check your understanding

              The removing dependencies quiz · 8 questionsScore: first tries count
              1. Question 1 of 8The linter says your Effect is missing roomId. What should you do?

                Choose an answer to see the explanation.

              2. Question 2 of 8This chat has just appeared, and StrictMode made connections 1 and 2. You type one letter. What does the console print?

                Read the code, then predictJSX
                import { useEffect, useState } from "react";
                import { createConnection } from "./chat.js";
                
                const serverUrl = "https://chat.example.com";
                
                function ChatRoom({ roomId }) {
                  const [message, setMessage] = useState("");
                
                  const options = { serverUrl, roomId };
                
                  useEffect(() => {
                    const connection = createConnection(options);
                    connection.connect();
                    return () => connection.disconnect();
                  }, [options]);
                
                  return (
                    <label>
                      Message <input value={message} onChange={(e) => setMessage(e.target.value)} />
                    </label>
                  );
                }

                Choose an answer to see the explanation.

              3. Question 3 of 8Why does React treat an options object made in the component body as changed after every render?

                Choose an answer to see the explanation.

              4. Question 4 of 8Which dependency array does this Effect need?

                Read the code, then predictJSX
                useEffect(() => {
                  const connection = createConnection({ serverUrl, roomId });
                  connection.on("message", (text) => {
                    setMessages((list) => [...list, text]);
                  });
                  connection.connect();
                  return () => connection.disconnect();
                }, [/* ? */]);

                Choose an answer to see the explanation.

              5. Question 5 of 8You click Place order, then click the theme button. What does the console print after the second click?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { useEffect, useState } from "react";
                import { post } from "./api.js";
                
                export default function App() {
                  const [submitted, setSubmitted] = useState(false);
                  const [theme, setTheme] = useState("light");
                
                  useEffect(() => {
                    if (submitted) {
                      post("/api/order");
                      console.log(`Thanks shown in ${theme}`);
                    }
                  }, [submitted, theme]);
                
                  return (
                    <div>
                      <button onClick={() => setSubmitted(true)}>Place order</button>
                      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>Theme: {theme}</button>
                    </div>
                  );
                }

                Choose an answer to see the explanation.

              6. Question 6 of 8In this review box, the linter was silenced. You type Great tea and wait 2 seconds. What does the console print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { useEffect, useState } from "react";
                import { saveDraft } from "./drafts.js";
                
                export default function App() {
                  const [draft, setDraft] = useState("");
                
                  useEffect(() => {
                    const id = setInterval(() => saveDraft(draft), 2000);
                    return () => clearInterval(id);
                    // eslint-disable-next-line react-hooks/exhaustive-deps
                  }, []);
                
                  return <input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Write a review" />;
                }

                Choose an answer to see the explanation.

              7. Question 7 of 8Which dependency array does this support chat's Effect need?

                Read the code, then predictJSX
                const options = { serverUrl: "https://chat.example.com", roomId: "support" };
                
                function SupportChat() {
                  useEffect(() => {
                    const connection = createConnection(options);
                    connection.connect();
                    return () => connection.disconnect();
                  }, [/* ? */]);
                  // …
                }

                Choose an answer to see the explanation.

              8. Question 8 of 8A parent passes onMessage={() => setUnread(unread + 1)} to ChatRoom, and ChatRoom's Effect lists onMessage. How do you stop it reconnecting every time the parent renders?

                Choose an answer to see the explanation.

              Key takeaways

              • The dependency array lists every reactive value the Effect reads. The code decides it, not you.
              • Never silence the linter: the Effect keeps old values, and later mistakes go unwarned.
              • Code a click causes goes in the handler. Separate jobs go in separate Effects.
              • Use an updater to read the previous state, and an Effect Event to read a value without reacting to it.
              • Objects and functions made during render are new every time. Build them inside the Effect or outside the component, or depend on the strings and numbers inside.

              To remove a dependency, prove the Effect doesn't need it.
              Change the code until the value isn't read, and the array follows.

              Coming next: Layout effects and timing. It looks at when Effects run compared with the browser's paint, and the rare cases that need useLayoutEffect.

              CompleteFrontend Clear concepts. Working examples.