cf.completefrontend
Open lab My progress
THE REACT FIELD GUIDE

You might not need an effect

Spot the effects you don't need. Calculate during render, cache with useMemo, reset with a key, and move event code into handlers, with live render logs.

By the end, you can
  • 01
    See what an unneeded effect costsRead a render and effect log, and point at the extra render and the out-of-date screen an unneeded effect causes.
  • 02
    Replace it with the right toolCalculate during render, cache with useMemo, reset with a key, and run event code in the handler, including updates to a parent.
  • 03
    Keep the effects you needTell code that keeps a component in step with something outside React from code that doesn't, and give a fetch a cleanup.

A full name that arrives one render late

An effect is code you hand to useEffect. React runs it after a render has reached the page, and again after later renders when one of its dependencies, the values in the array you pass second, has changed. Synchronizing with effects teaches how to write one. This lesson is about when not to.

Effects are tempting for any "when this changes, update that". Here is a gift card that keeps a full name up to date that way. The result is live, and the console under it lists every render and every effect run.

A full name copied into state by an effectPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; export default function GiftCard() {  const [firstName, setFirstName] = useState("Asha");  const [lastName, setLastName] = useState("Rao");  const [fullName, setFullName] = useState("");  console.log(`Render: card says "${fullName}"`);   useEffect(() => {    console.log("Effect: set fullName");    setFullName(firstName + " " + lastName);  }, [firstName, lastName]);   return (    <>      <p>        <label>First name <input value={firstName} onChange={(event) => setFirstName(event.target.value)} /></label>      </p>      <p>        <label>Last name <input value={lastName} onChange={(event) => setLastName(event.target.value)} /></label>      </p>      <p>Gift card for: <strong>{fullName}</strong></p>    </>  );}
ResultLoading…

Lines 4 and 5 keep the first and last name in state. Line 6 keeps a third piece of state, fullName, starting empty. Line 7 prints a line on every render. Lines 9 to 12 are the effect: after a render where firstName or lastName changed, line 11 sets fullName from them.

Read the console. First comes Render: card says "" with a small 2 beside it: every live example here runs in StrictMode, which calls each component twice in development to catch mistakes (Keeping components pure explains it), so read a 2 as one render. Then Effect: set fullName, also twice, because StrictMode also starts a new component's effects twice. Only then Render: card says "Asha Rao". The first render put an empty card on the page.

Now click at the end of First name and type one letter, say n. The count beside Render: card says "Asha Rao" goes from 2 to 4: the first render after your key still has the old name. Then come Effect: set fullName and Render: card says "Ashan Rao". Every key costs two renders. Here is the same card with the full name worked out in the component body instead:

The full name, calculated during renderPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; export default function GiftCard() {  const [firstName, setFirstName] = useState("Asha");  const [lastName, setLastName] = useState("Rao");  const fullName = firstName + " " + lastName;  console.log(`Render: card says "${fullName}"`);   return (    <>      <p>        <label>First name <input value={firstName} onChange={(event) => setFirstName(event.target.value)} /></label>      </p>      <p>        <label>Last name <input value={lastName} onChange={(event) => setLastName(event.target.value)} /></label>      </p>      <p>Gift card for: <strong>{fullName}</strong></p>    </>  );}
ResultLoading…

Line 6 is the whole change: const fullName = firstName + " " + lastName;. No third state, no effect. The console starts with Render: card says "Asha Rao", and each letter you type adds one render with the new name. The value is ready in the same render that needs it, because React runs the whole component body on every render.

Top row, with the effect: five cards for one keystroke. Render, with the card still saying "Asha Rao". Commit, the old name on the page. Effect, setFullName. Render, "Ashan Rao". Commit, the new name. Bottom row, calculated during render: Render, "Ashan Rao", then Commit, the new name.With the effect: one keystrokeRender"Asha Rao"Commitold nameEffectsetFullNameRender"Ashan Rao"Commitnew nameCalculated during renderRenderCommit"Ashan Rao"new name
Typing one letter with the effect takes two renders and two commits, and the first pass puts the old name on the page. Calculated during render, the name is right in the first and only pass.

Will you see the old name flash? Sometimes. After a key press, React runs the effect before the browser paints, the step where it draws the new screen. But the render the effect asks for with setFullName comes in a later piece of work, and the browser can paint the old name in between if a new frame is due. On a fast computer that frame is gone in a few milliseconds, so you rarely notice it. On the first render, which no key caused, the browser may paint the empty card before the effect fills it in. Either way, the extra render and commit happen on every key, and anything slow in them slows typing. Layout effects and timing, later in this stage, shows this gap with a tooltip.

Real-life analogyA price board copied by hand

A tea shop changes a price at the till. A worker sees the change, walks to the chalkboard, and writes the new price. Until the worker gets there, the board shows the old price, and the worker spends time on every change. A screen wired straight to the till never shows an old price, and nobody has to walk anywhere.

In real life: The till, which knows every price
In React: Props and state
In real life: A chalkboard a worker copies the price onto
In React: A state variable an effect keeps up to date
In real life: The worker walks over after each change
In React: The effect runs after the render
In real life: A customer who looks in between sees the old price
In React: The first render shows the old value
In real life: A screen wired to the till
In React: A value calculated during render
The main idea

Effects are for keeping a component in step with something outside React, like a server connection, a browser API, or a widget written without React. If what you need can be worked out from props and state, calculate it during render. If it happens because the person did something, do it in that event's handler. Neither needs an effect.

What an unneeded effect costs

An effect that sets state, when nothing outside React is involved, costs you three things:

  • An extra render and commit, every time it runs.
  • One render with out-of-date values, which reaches the page.
  • More state to keep in step, and more code to read to find out why a value changed.

The playground runs three small components twice: once with an effect, once without. Pick a case, use both versions, and compare their logs. Each line is a real render, effect run, or handler call, in order.

Playground: count the renders
With an effect0 renders
The effect versionJSX
const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(firstName + " " + lastName);
}, [firstName, lastName]);

Gift card for:

Nothing yet. Use the effect version above.

Without an effect0 renders
The version without itJSX
const fullName = firstName + " " + lastName;

Gift card for: Asha Rao

Nothing yet. Use the no-effect version above.

Try it yourself
Use both versions.

Type one letter at the end of First name, in each version.

Both versions run here, with React on this page. Each log lists, in order, every render (blue), effect (purple), and parent handler (green) your actions cause; the logs start at your first click or key in each version. A line React repeats in development, for StrictMode, is shown once. Picking a case starts both versions again.

Full name is the gift card from above: two renders per key with the effect, one without. In Stamp card, the third Buy a tea runs three effects in a row: four renders against one, and one of them puts a full card of 3 stamps on the page. In Gift wrap, the parent hears about one tick twice, and the toggle renders before the total catches up. The stamp card and the gift wrap get their own sections below, with the full code.

Calculate during render, and cache slow work

The fix for the full name works for anything made from props and state: a count, a total, a filtered list. This tea search filters its list in the component body. filterTeas lives in teas.js, in the second tab.

A search that filters during renderPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { teas, filterTeas } from "./teas.js"; export default function TeaSearch() {  const [query, setQuery] = useState("");  const [isLarge, setIsLarge] = useState(false);  const visibleTeas = filterTeas(teas, query);   return (    <div style={{ fontSize: isLarge ? "1.25em" : "1em" }}>      <label>Search <input value={query} onChange={(event) => setQuery(event.target.value)} /></label>{" "}      <button onClick={() => setIsLarge(!isLarge)}>Larger text</button>      <ul>        {visibleTeas.map((tea) => <li key={tea}>{tea}</li>)}      </ul>    </div>  );}
ResultLoading…

In teas.js, line 1 is the list and lines 3 to 6 are filterTeas: line 4 prints a line each time it runs, and line 5 keeps the teas whose name contains the search text. In App.jsx, line 7 calls it on every render, and line 14 shows the result.

Type gr in the box. The list shows Green tea, and the console prints filterTeas ran for "gr". Now press Larger text. The list didn't change, but filterTeas ran again: the button changed isLarge, React rendered TeaSearch, and every render runs line 7.

For five teas that costs nothing. For 5,000 products, it might. You might reach for an effect that filters only when the search text changes:

Filtering in an effect (part of TeaSearch)JSX
const [visibleTeas, setVisibleTeas] = useState([]);

useEffect(() => {
  setVisibleTeas(filterTeas(teas, query));
}, [query]);

That fragment of TeaSearch isn't meant to run on its own. It's the gift card's mistake again: an empty list on the first render, and two renders for every key. What you want is to keep the calculation in the render and skip it when its input is the same. That's useMemo:

The same search, cached with useMemoPop out in the code editor (opens in a new tab)JSX
import { useMemo, useState } from "react";import { teas, filterTeas } from "./teas.js"; export default function TeaSearch() {  const [query, setQuery] = useState("");  const [isLarge, setIsLarge] = useState(false);  const visibleTeas = useMemo(() => filterTeas(teas, query), [query]);   return (    <div style={{ fontSize: isLarge ? "1.25em" : "1em" }}>      <label>Search <input value={query} onChange={(event) => setQuery(event.target.value)} /></label>{" "}      <button onClick={() => setIsLarge(!isLarge)}>Larger text</button>      <ul>        {visibleTeas.map((tea) => <li key={tea}>{tea}</li>)}      </ul>    </div>  );}
ResultLoading…

Line 7 wraps the call: useMemo(() => filterTeas(teas, query), [query]). On the first render, React calls the function and keeps its result. On each later render, it compares query with last time. Same value: it hands back the kept list without calling anything. New value: it calls the function again and keeps the new result.

Type gr, then press Larger text. The console stays quiet. Type another letter and it runs again. (StrictMode calls the function twice when it does run, in development, like the component itself.)

  • useMemo runs during render, so the function must be pure: work out a value, and change nothing outside it.
  • It doesn't make the first render faster. It only skips repeated work on later renders.
  • Use it for slow work. To find out, wrap the call in console.time("filter") and console.timeEnd("filter"); if it takes a millisecond or more, caching can help.
The React Compiler caches for you

The React Compiler, a build tool from the React team whose stable 1.0 release came out in October 2025, adds this kind of caching to your components automatically. With it, you rarely write useMemo by hand. The examples on this page don't use it, so you can watch what useMemo does.

When a prop changes: a key, or less state

A review form for two shops. You type a comment and give some stars, then switch shops. The comment should start fresh for the new shop, so this version clears it in an effect when shopName changes:

Clearing a review in an effectPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; function Stars() {  const [stars, setStars] = useState(0);  return <p>Stars: {stars} <button onClick={() => setStars(stars + 1)}>Add a star</button></p>;} function Review({ shopName }) {  const [comment, setComment] = useState("");  console.log(`Render: ${shopName}, comment "${comment}"`);   useEffect(() => {    setComment("");  }, [shopName]);   return (    <>      <h3>Review {shopName}</h3>      <label>Comment <input value={comment} onChange={(event) => setComment(event.target.value)} /></label>      <Stars />    </>  );} export default function App() {  const [shop, setShop] = useState("Tea Corner");  return (    <>      <button onClick={() => setShop("Tea Corner")}>Tea Corner</button>{" "}      <button onClick={() => setShop("Cake Shop")}>Cake Shop</button>      <Review shopName={shop} />    </>  );}
ResultLoading…

Stars (lines 3 to 6) keeps its own count of stars. Review (lines 8 to 23) keeps the comment, and lines 12 to 14 clear it after a render where shopName changed. App (lines 25 to 34) switches the shop with two buttons.

Type a comment, press Add a star twice, then press Cake Shop. Two things go wrong. The console prints Render: Cake Shop, comment "…" with your Tea Corner comment: a render of the Cake Shop review with the wrong text, before the effect clears it. And Stars still says 2. The effect clears the comment and nothing else; every new piece of state, in Review or below it, would need its own line.

(The first key you type prints its render line 4 times, not 2. The effect's setComment("") on the first render changed nothing, and React's useState reference notes that React may then still call a component once more before it skips it. That call never reaches the page; it's one more render the effect costs.)

React already has a way to say "this is a different review now": a key. React keeps a component's state while the same component stays at the same place with the same key. Change the key, and React treats it as a new component (Preserving and resetting state shows this in detail):

Starting the review fresh with a keyPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; function Stars() {  const [stars, setStars] = useState(0);  return <p>Stars: {stars} <button onClick={() => setStars(stars + 1)}>Add a star</button></p>;} function Review({ shopName }) {  const [comment, setComment] = useState("");  console.log(`Render: ${shopName}, comment "${comment}"`);   return (    <>      <h3>Review {shopName}</h3>      <label>Comment <input value={comment} onChange={(event) => setComment(event.target.value)} /></label>      <Stars />    </>  );} export default function App() {  const [shop, setShop] = useState("Tea Corner");  return (    <>      <button onClick={() => setShop("Tea Corner")}>Tea Corner</button>{" "}      <button onClick={() => setShop("Cake Shop")}>Cake Shop</button>      <Review key={shop} shopName={shop} />    </>  );}
ResultLoading…

The effect is gone, and line 27 passes key={shop}. When shop changes, React removes the old Review, with its Stars and all their state, and creates new ones. Try the same steps: one render, Render: Cake Shop, comment "", and Stars: 0. Back to Tea Corner, and that review starts fresh too.

Sometimes only part of the state should change. This tea picker clears the picked tea when the list changes from green teas to black teas:

Clearing the picked tea in an effectPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; const menu = { Green: ["Sencha", "Matcha"], Black: ["Assam", "Darjeeling"] }; function TeaList({ teas }) {  const [selection, setSelection] = useState(null);  console.log(`Render: ${teas.join(", ")}; picked ${selection ?? "nothing"}`);   useEffect(() => {    setSelection(null);  }, [teas]);   return (    <ul>      {teas.map((tea) => (        <li key={tea}>          <button onClick={() => setSelection(tea)}>{tea}</button> {tea === selection && "(picked)"}        </li>      ))}    </ul>  );} export default function App() {  const [kind, setKind] = useState("Green");  return (    <>      <button onClick={() => setKind("Green")}>Green teas</button>{" "}      <button onClick={() => setKind("Black")}>Black teas</button>      <TeaList teas={menu[kind]} />    </>  );}
ResultLoading…

Line 6 keeps the picked tea, and lines 9 to 11 set it back to null after a render with a new teas list. Press Sencha, then Black teas. The console prints Render: Assam, Darjeeling; picked Sencha: a render where the picked tea isn't on the list. Then the effect runs, and picked nothing follows. Instead, store only what you can't work out, and work out the rest:

Working out the picked tea during renderPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; const menu = { Green: ["Sencha", "Matcha"], Black: ["Assam", "Darjeeling"] }; function TeaList({ teas }) {  const [pickedName, setPickedName] = useState(null);  const selection = teas.includes(pickedName) ? pickedName : null;  console.log(`Render: ${teas.join(", ")}; picked ${selection ?? "nothing"}`);   return (    <ul>      {teas.map((tea) => (        <li key={tea}>          <button onClick={() => setPickedName(tea)}>{tea}</button> {tea === selection && "(picked)"}        </li>      ))}    </ul>  );} export default function App() {  const [kind, setKind] = useState("Green");  return (    <>      <button onClick={() => setKind("Green")}>Green teas</button>{" "}      <button onClick={() => setKind("Black")}>Black teas</button>      <TeaList teas={menu[kind]} />    </>  );}
ResultLoading…

Line 6 keeps the name you pressed. Line 7 works out the picked tea: that name if it's on the current list, null if not. Press Sencha, then Black teas: one render, nothing picked. Press Green teas, and Sencha is picked again, because the name was kept. If you want it forgotten instead, clear pickedName in the category buttons' click handlers: switching is an event.

As a last resort, React lets a component set its own state during render, when a prop is different from the one it saw last time. This fragment of TeaList isn't meant to run alone:

Adjusting state during render (part of TeaList)JSX
const [selection, setSelection] = useState(null);
const [prevTeas, setPrevTeas] = useState(teas);
if (teas !== prevTeas) {
  setPrevTeas(teas);
  setSelection(null);
}

Line 2 remembers the list from the last render. When line 3 finds a new list, React throws away this render's result and renders TeaList again straight away, before its children and before anything reaches the page. That's better than an effect, but harder to follow than a key or storing less, so reach for those first.

Code caused by a click belongs in the handler

A product page shows a toast, a short message, when green tea goes into the cart. This version shows it from an effect that watches inCart:

A toast shown from an effectPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; function ProductPage({ inCart, onAdd }) {  useEffect(() => {    if (inCart) {      console.log("Toast: Green tea is in your cart!");    }  }, [inCart]);   function handleAdd() {    onAdd();  }   function handleBuyNow() {    onAdd();    console.log("Go to checkout");  }   return (    <>      <h3>Green tea</h3>      <button onClick={handleAdd} disabled={inCart}>Add to cart</button>{" "}      <button onClick={handleBuyNow}>Buy now</button>    </>  );} export default function App() {  const [inCart, setInCart] = useState(false);  const [page, setPage] = useState("product");  return (    <>      <nav>        <button onClick={() => setPage("product")}>Product</button>{" "}        <button onClick={() => setPage("help")}>Help</button>      </nav>      {page === "product" ? <ProductPage inCart={inCart} onAdd={() => setInCart(true)} /> : <p>Ask us anything about tea.</p>}    </>  );}
ResultLoading…

Lines 4 to 8 show the toast after any render where inCart is true and changed. Both buttons call onAdd (lines 11 and 15), and App keeps inCart (line 29) and which page is open (line 30).

Press Add to cart: one toast in the console. So far so good. Now press Help, then Product. The toast shows again, twice in development, and you didn't add anything. The product page was created again with inCart already true, and an effect only knows what's true now, not what happened. Press Buy now: no toast at all, because inCart didn't change.

The toast shown by the handlersPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; function ProductPage({ inCart, onAdd }) {  function addToCart() {    onAdd();    console.log("Toast: Green tea is in your cart!");  }   function handleAdd() {    addToCart();  }   function handleBuyNow() {    addToCart();    console.log("Go to checkout");  }   return (    <>      <h3>Green tea</h3>      <button onClick={handleAdd} disabled={inCart}>Add to cart</button>{" "}      <button onClick={handleBuyNow}>Buy now</button>    </>  );} export default function App() {  const [inCart, setInCart] = useState(false);  const [page, setPage] = useState("product");  return (    <>      <nav>        <button onClick={() => setPage("product")}>Product</button>{" "}        <button onClick={() => setPage("help")}>Help</button>      </nav>      {page === "product" ? <ProductPage inCart={inCart} onAdd={() => setInCart(true)} /> : <p>Ask us anything about tea.</p>}    </>  );}
ResultLoading…

Lines 4 to 7 put the shared work, adding and showing the toast, in one function, addToCart. Both handlers call it (lines 10 and 14). Now Help and Product show no toast, and Buy now shows one every time you press it.

The question to ask is why the code runs. If it runs because the person did something, it's event code: put it in that event's handler. If it runs because the component is on the screen, an effect may fit. Each row asks one question of both:

An event handler or an effect?
QuestionEvent handlerEffect
When does the code run?When the person clicks, types, or submits.After React puts a render on the page, if a dependency changed.
Does it know what happened?Yes. It runs because of one particular event.No. It only sees the props and state of the render that just reached the page.
How many renders does a state change in it cost?One render, after the handler finishes.A second render, after the one that ran the effect.
What is it for?Doing what the person asked for: add to cart, send the form.Keeping the component in step with something outside React: a connection, a browser API, a server.

The same question sorts requests to a server. This sign-up form sends two. post, in api.js, prints each request instead of sending it.

One request from an effect, one from a handlerPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { post } from "./api.js"; export default function SignupForm() {  const [email, setEmail] = useState("");   // Runs because the form was shown: an effect is right.  useEffect(() => {    post("/analytics/event", { name: "saw_signup_form" });  }, []);   // Runs because the person pressed Join: the handler is right.  function handleSubmit(event) {    event.preventDefault();    post("/api/signup", { email });  }   return (    <form onSubmit={handleSubmit}>      <label>Email <input value={email} onChange={(event) => setEmail(event.target.value)} /></label>{" "}      <button>Join</button>    </form>  );}
ResultLoading…

Lines 8 to 10 send an analytics event because the form was shown: nothing was clicked, so an effect is right. The console shows it twice, because StrictMode creates the form twice in development; a real visit sends it once. Lines 13 to 16 send the sign-up because the person pressed Join: type an email and press Join, and one POST /api/signup appears. Here is the same request sent from an effect, a fragment that isn't meant to run on its own:

The sign-up sent from an effect (part of SignupForm)JSX
const [jsonToSubmit, setJsonToSubmit] = useState(null);

useEffect(() => {
  if (jsonToSubmit !== null) {
    post("/api/signup", jsonToSubmit);
  }
}, [jsonToSubmit]);

function handleSubmit(event) {
  event.preventDefault();
  setJsonToSubmit({ email });
}

The submit handler only stores the data (line 11), and the effect sends it after the next render (lines 3 to 7). That's an extra state and an extra render, and the request is no longer tied to the press that caused it.

Effects that set state can also start each other. This stamp card gives a free tea for every 3 teas, with one effect per step:

A stamp card run by a chain of effectsPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; export default function StampCard() {  const [cups, setCups] = useState(0);  const [stamps, setStamps] = useState(0);  const [freeTeas, setFreeTeas] = useState(0);  const [message, setMessage] = useState("Buy 3 teas, get 1 free.");  console.log(`Render: ${stamps} stamps, ${freeTeas} free`);   useEffect(() => {    if (cups === 0) return;    console.log("Effect 1: add a stamp");    setStamps((count) => count + 1);  }, [cups]);   useEffect(() => {    if (stamps < 3) return;    console.log("Effect 2: swap 3 stamps for a free tea");    setStamps(0);    setFreeTeas((count) => count + 1);  }, [stamps]);   useEffect(() => {    if (freeTeas === 0) return;    console.log("Effect 3: update the message");    setMessage(`You have ${freeTeas} free tea${freeTeas === 1 ? "" : "s"}!`);  }, [freeTeas]);   return (    <>      <p>Stamps: {stamps} of 3</p>      <p>{message}</p>      <button onClick={() => setCups(cups + 1)}>Buy a tea</button>    </>  );}
ResultLoading…

The button (line 33) only counts cups. Effect 1 (lines 10 to 14) adds a stamp when cups changes. Effect 2 (lines 16 to 21) swaps 3 stamps for a free tea when stamps changes. Effect 3 (lines 23 to 27) writes the message when freeTeas changes. Press Buy a tea three times and read the third press in the console: a render, Effect 1, a render with 3 stamps, Effect 2, a render with a free tea, Effect 3, and a last render. Four renders for one press, and one of them puts a card with 3 of 3 stamps on the page.

The third Buy a tea, with three chained effects: Buy a tea, Render with 2 stamps, Effect 1 adds a stamp, Render with 3 of 3 stamps, then down to Effect 2 swapping them for a free tea, Render with 0 stamps and 1 free, Effect 3 setting the message, and a last Render. Below, with one handler: Buy a tea, buyTea works out every new value, one Render.Chained effects: the third pressBuy a teayour clickRender2 stampsEffect 1add a stampRender3 of 3 stampsEffect 2swap for a free teaRender0 stamps, 1 freeEffect 3new messageRendermessage showsOne handler: the third pressBuy a teabuyTea()Renderevery new value0 stamps, 1 free
With three chained effects, one press takes four renders, and one of them puts a full card of 3 stamps on the page. In one handler, buyTea works out every new value, and React renders once.
The stamp card in one handlerPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; export default function StampCard() {  const [stamps, setStamps] = useState(0);  const [freeTeas, setFreeTeas] = useState(0);  console.log(`Render: ${stamps} stamps, ${freeTeas} free`);   function buyTea() {    if (stamps + 1 === 3) {      setStamps(0);      setFreeTeas(freeTeas + 1);    } else {      setStamps(stamps + 1);    }  }   const message = freeTeas === 0 ? "Buy 3 teas, get 1 free." : `You have ${freeTeas} free tea${freeTeas === 1 ? "" : "s"}!`;   return (    <>      <p>Stamps: {stamps} of 3</p>      <p>{message}</p>      <button onClick={buyTea}>Buy a tea</button>    </>  );}
ResultLoading…

buyTea (lines 8 to 15) works out every new value at once: on the third stamp it sets the stamps to 0 and adds a free tea, otherwise it adds a stamp. Line 17 calculates the message during render. Each press is one render. A chain of effects is still right when each step talks to something outside React, like a form whose second list of options loads from a server after you pick from the first.

Tell the parent in the same event

A gift wrap checkbox keeps its own isOn state and tells its parent, Checkout, through an onChange prop, so the total can include the wrapping. This version tells the parent from an effect:

Telling the parent from an effectPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; function GiftWrapToggle({ onChange }) {  const [isOn, setIsOn] = useState(false);  console.log(`Render Toggle: ${isOn ? "on" : "off"}`);   useEffect(() => {    console.log("Effect: tell the parent");    onChange(isOn);  }, [isOn, onChange]);   return (    <label>      <input type="checkbox" checked={isOn} onChange={() => setIsOn(!isOn)} /> Gift wrap (+$1)    </label>  );} export default function Checkout() {  const [giftWrap, setGiftWrap] = useState(false);  const total = 3 + (giftWrap ? 1 : 0);  console.log(`Render Checkout: total $${total}`);   function handleWrapChange(isOn) {    console.log(`Parent heard: ${isOn ? "on" : "off"}`);    setGiftWrap(isOn);  }   return (    <>      <p>Green tea: $3</p>      <GiftWrapToggle onChange={handleWrapChange} />      <p>Total: ${total}</p>    </>  );}
ResultLoading…

Lines 7 to 10 call onChange(isOn) after any render where isOn or onChange changed. In Checkout, handleWrapChange (lines 24 to 27) prints what it heard and sets giftWrap, and line 21 works out the total.

Before you click anything, the console already says Parent heard: off, twice: the effect runs when the toggle first appears, and StrictMode runs it again. Now tick Gift wrap. The toggle renders on while the total is still $3. Then the effect tells the parent, the parent renders with $4, and the toggle renders again. Each render of Checkout makes a new handleWrapChange function, so onChange changed, and the effect tells the parent a second time. React renders Checkout once more and finds nothing changed.

Telling the parent from the handlerPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; function GiftWrapToggle({ onChange }) {  const [isOn, setIsOn] = useState(false);  console.log(`Render Toggle: ${isOn ? "on" : "off"}`);   function handleClick() {    const next = !isOn;    setIsOn(next);    onChange(next);  }   return (    <label>      <input type="checkbox" checked={isOn} onChange={handleClick} /> Gift wrap (+$1)    </label>  );} export default function Checkout() {  const [giftWrap, setGiftWrap] = useState(false);  const total = 3 + (giftWrap ? 1 : 0);  console.log(`Render Checkout: total $${total}`);   function handleWrapChange(isOn) {    console.log(`Parent heard: ${isOn ? "on" : "off"}`);    setGiftWrap(isOn);  }   return (    <>      <p>Green tea: $3</p>      <GiftWrapToggle onChange={handleWrapChange} />      <p>Total: ${total}</p>    </>  );}
ResultLoading…

handleClick (lines 7 to 11) works out the new value once, sets it, and tells the parent in the same event. React batches set calls from one event into one render (Queueing state updates), so the console shows Parent heard: on, then one render of each component, with the total already $4. Nothing is heard when the page loads.

Another way is to move the state up: Checkout keeps giftWrap and passes it down as an isOn prop, so there's only one copy to keep in step (Sharing state between components).

The same goes for data. Here a child reads the saved address and hands it up to its parent from an effect:

A child passing data up from an effectPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { loadSavedAddress } from "./address.js"; function AddressLine({ onLoad }) {  const [address] = useState(() => loadSavedAddress());   useEffect(() => {    onLoad(address);  }, [address, onLoad]);   return <p>Address: {address}</p>;} export default function Delivery() {  const [address, setAddress] = useState(null);  console.log(`Render Delivery: ships to ${address ?? "nowhere yet"}`);   return (    <>      <AddressLine onLoad={setAddress} />      <p>Ships to: {address ?? "…"}</p>    </>  );}
ResultLoading…

AddressLine reads the address (line 5) and sends it up (lines 7 to 9). The console shows Render Delivery: ships to nowhere yet, then a second render with 12 Park Street: the parent waits a whole render for data its child had from the start. Data in React flows down, from parents to children, so let the parent get it:

The parent reads the data and passes it downPop out in the code editor (opens in a new tab)JSX
import { useState } from "react";import { loadSavedAddress } from "./address.js"; function AddressLine({ address }) {  return <p>Address: {address}</p>;} export default function Delivery() {  const [address] = useState(() => loadSavedAddress());  console.log(`Render Delivery: ships to ${address ?? "nowhere yet"}`);   return (    <>      <AddressLine address={address} />      <p>Ships to: {address ?? "…"}</p>    </>  );}
ResultLoading…

Line 9 reads the address in Delivery, and line 14 passes it to AddressLine as a prop. One render, and both lines show the address. When you wonder where a value came from, you only have to look up the tree.

When an effect is the right tool

Some code really is about the world outside React: a sign-in check, a browser setting, a server. Three cases: one that looks like an effect but shouldn't be one, one with a hook built for the job, and one where an effect is right, with a cleanup.

Code that runs once when the app starts. This app checks a saved sign-in token. In auth.js, the token works once, like the token in a sign-in link: the second check finds nothing.

Checking a one-time token in an effectPop out in the code editor (opens in a new tab)JSX
import { useEffect } from "react";import { checkSavedLogin } from "./auth.js"; export default function App() {  useEffect(() => {    checkSavedLogin();  }, []);   return <p>Welcome back to Tea Corner.</p>;}
ResultLoading…

Lines 5 to 7 check the token after the first render. The console says Signed in with token tea-123, then No token left: you are signed out. In development, StrictMode runs App's effects, cleans them up, and runs them again, as if App had been removed and put back, to check that its effects can run twice. This effect can't. A real app rarely creates App twice, but any component can be, so code that must run once per page load doesn't belong in a component's effect:

Running the check once per page loadPop out in the code editor (opens in a new tab)JSX
import { useEffect } from "react";import { checkSavedLogin } from "./auth.js"; let didInit = false; export default function App() {  useEffect(() => {    if (didInit) return;    didInit = true;    checkSavedLogin();  }, []);   return <p>Welcome back to Tea Corner.</p>;}
ResultLoading…

Line 4 is a variable in the module, outside the component, so it lives as long as the page, not as long as one App. Lines 8 and 9 make the second run stop early. The console shows one sign-in. You can also call checkSavedLogin() at the top of the module, outside any component; it runs once, when the file is first imported. Keep such code in the app's main file, so importing a component never starts anything.

Data that lives outside React. Whether the browser is online lives in navigator.onLine, and the browser fires online and offline events when it changes. You can copy it into state by hand:

Subscribing by hand (part of a component)JSX
const [isOnline, setIsOnline] = useState(true);

useEffect(() => {
  function update() {
    setIsOnline(navigator.onLine);
  }
  update();
  window.addEventListener("online", update);
  window.addEventListener("offline", update);
  return () => {
    window.removeEventListener("online", update);
    window.removeEventListener("offline", update);
  };
}, []);

That fragment works, but it's state that copies something else, kept up to date by an effect. React has a hook for exactly this, useSyncExternalStore:

Reading the browser's online statusPop out in the code editor (opens in a new tab)JSX
import { useSyncExternalStore } from "react"; function subscribe(onStoreChange) {  window.addEventListener("online", onStoreChange);  window.addEventListener("offline", onStoreChange);  return () => {    window.removeEventListener("online", onStoreChange);    window.removeEventListener("offline", onStoreChange);  };} export default function CartStatus() {  const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine);  console.log(`Render: ${isOnline ? "online" : "offline"}`);   return <p>{isOnline ? "Online: your cart is saved." : "Offline: your cart saves when you reconnect."}</p>;}
ResultLoading…

Lines 3 to 10 say how to listen: subscribe adds the two listeners, and returns a function that removes them. Line 13 passes it to useSyncExternalStore, with a function that reads the current value. React subscribes while the component is on screen, reads the value during every render, and renders again when a listener fires. To see it change, turn off your Wi-Fi, or open Chrome's DevTools, go to the Network tab, and choose Offline. The Subscribing to external stores lesson covers the hook in full.

Data from a server. Search results must match the text in the box, and the server is outside React. That's synchronizing, so an effect is right. In api.js, shorter searches take longer: 1100 ms for one letter, 700 for two, 300 for three or more.

Fetching search results without a cleanupPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { searchTeas } from "./api.js"; export default function TeaSearch() {  const [query, setQuery] = useState("");  const [results, setResults] = useState(null);   useEffect(() => {    if (!query) return;    console.log(`Fetch "${query}"`);    searchTeas(query).then((teas) => {      console.log(`Show results for "${query}"`);      setResults({ query, teas });    });  }, [query]);   return (    <>      <label>Search <input value={query} onChange={(event) => setQuery(event.target.value)} /></label>      {results && <p>Results for "{results.query}": {results.teas.join(", ") || "none"}</p>}    </>  );}
ResultLoading…

Lines 8 to 15 start a search after each render where query changed, and line 13 shows its results when they arrive. Type gre quickly. The console prints three fetches, then the answers in the reverse order: "gre" first, "g" last. The page ends on Results for "g", with Ginger tea in it, though the box says gre. That's a race condition: the answer that arrives last wins, not the one you asked for last.

A timeline of three searches typed 150 ms apart. "g" starts at 0 ms and answers at 1100 ms. "gr" starts at 150 ms and answers at 850 ms. "gre" starts at 300 ms and answers first, at 600 ms. Without a cleanup, the last answer, for "g", stays on screen. With a cleanup, the answers for "g" and "gr" are ignored and "gre" stays."g"1100 msat 1100 ms"gr"700 msat 850 ms"gre"300 msat 600 mstime after the first keyNo cleanup: "g" answers last and winsWith a cleanup: "g" and "gr" are ignored
Each bar is one search, from the key press to its answer, with the time it takes inside. The shortest search answers last. Without a cleanup, whatever arrives last wins, so the page ends on the results for "g". With a cleanup, the two old answers are ignored.
Ignoring old answers in the cleanupPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react";import { searchTeas } from "./api.js"; export default function TeaSearch() {  const [query, setQuery] = useState("");  const [results, setResults] = useState(null);   useEffect(() => {    if (!query) return;    let ignore = false;    console.log(`Fetch "${query}"`);    searchTeas(query).then((teas) => {      if (ignore) {        console.log(`Ignore results for "${query}"`);        return;      }      console.log(`Show results for "${query}"`);      setResults({ query, teas });    });    return () => {      ignore = true;    };  }, [query]);   return (    <>      <label>Search <input value={query} onChange={(event) => setQuery(event.target.value)} /></label>      {results && <p>Results for "{results.query}": {results.teas.join(", ") || "none"}</p>}    </>  );}
ResultLoading…

Line 10 makes an ignore flag for this one search. Lines 20 to 22 are the cleanup: React calls it before it runs the effect again for a new query, and when the search box leaves the screen. It sets that search's flag to true, and lines 13 to 16 drop its answer when it arrives. Each effect run keeps its own flag, because each then callback remembers the ignore of the run that made it (a closure). Type gre quickly again: Ignore results for "gr" and "g", and the page shows Green tea for "gre".

Fetching in effects has more problems to solve, like caching and loading while the page is built on the server. Frameworks and data libraries solve them, and Fetching data covers them later.

Cleaning up a checkout page

Real components collect effects over time. This checkout has two: one keeps the total, and one sends the order once it's placed.

A checkout with two effectsPop out in the code editor (opens in a new tab)JSX
import { useEffect, useState } from "react"; const prices = { tea: 3, cake: 4 }; export default function Checkout() {  const [cart, setCart] = useState({ tea: 1, cake: 0 });  const [total, setTotal] = useState(0);  const [isPlaced, setIsPlaced] = useState(false);  console.log(`Render: total $${total}`);   useEffect(() => {    setTotal(cart.tea * prices.tea + cart.cake * prices.cake);  }, [cart]);   useEffect(() => {    if (isPlaced) {      console.log(`POST /api/orders: $${total}`);    }  }, [isPlaced, total]);   return (    <>      <p>Tea: {cart.tea}, cake: {cart.cake}. Total: ${total}</p>      <button onClick={() => setCart({ ...cart, cake: cart.cake + 1 })}>Add cake</button>{" "}      <button onClick={() => setIsPlaced(true)}>Place order</button>      {isPlaced && <p>Order placed.</p>}    </>  );}
ResultLoading…

Lines 11 to 13 copy the total into state, and lines 15 to 19 send the order after any render where isPlaced is true and isPlaced or total changed. Press Place order: POST /api/orders: $3. Now press Add cake. The console prints POST /api/orders: $7: a second order nobody placed, sent because the total changed while isPlaced was true.

The same checkout without effectsPop out in the code editor (opens in a new tab)JSX
import { useState } from "react"; const prices = { tea: 3, cake: 4 }; export default function Checkout() {  const [cart, setCart] = useState({ tea: 1, cake: 0 });  const [isPlaced, setIsPlaced] = useState(false);  const total = cart.tea * prices.tea + cart.cake * prices.cake;  console.log(`Render: total $${total}`);   function placeOrder() {    console.log(`POST /api/orders: $${total}`);    setIsPlaced(true);  }   return (    <>      <p>Tea: {cart.tea}, cake: {cart.cake}. Total: ${total}</p>      <button onClick={() => setCart({ ...cart, cake: cart.cake + 1 })}>Add cake</button>{" "}      <button onClick={placeOrder}>Place order</button>      {isPlaced && <p>Order placed.</p>}    </>  );}
ResultLoading…

Line 8 calculates the total. placeOrder (lines 11 to 14) sends the order because the button was pressed, once. Place order, then Add cake: one order, and the page shows the new total in one render.

When you meet an effect in your own code, ask these three questions, in order:

Three questions on the left, each with an arrow to an answer on the right. Made from props or state: calculate it in render. Caused by a click or a key: an event handler. Keeping in step with something outside React: an effect.Made from props or state?calculate in renderCaused by a click or a key?event handlerIn step with the outside?effect
Ask in this order. Most code stops at the first or second question. Only code that keeps a component in step with something outside React, like a server, a browser API, or a widget, ends up in an effect.

Each row is one effect you might write, and what to write instead:

Effects you might not need, and what to write instead
When you want toWrite this insteadWhy it's better
Show a value worked out from props or stateCalculate it in the component body.It's ready in the same render. No extra state, no second render.
Skip a slow calculation when its inputs are the sameWrap it in useMemo with its inputs as dependencies.React keeps the last result and reuses it until an input changes.
Start a component fresh when a prop changesGive it a key with that prop.React treats a new key as a new component, so all its state, and its children's, starts over.
Change part of the state when a prop changesStore less: keep an id, and work out the rest during render.Nothing goes out of date, so there's nothing to adjust.
Run code because the person clicked or typedPut it in the event handler.The handler knows what happened. An effect only knows what's on the screen.
Send a form's dataSend it in the submit handler.Sending is caused by the submit, not by the form being shown.
Update several pieces of state after one clickWork out every new value in that click's handler.One render instead of a chain of renders, each running the next effect.
Tell a parent about a changeCall the parent's function in the same handler, or move the state up.Both components update in one render.
Give a parent data a child loadedLoad it in the parent and pass it down as a prop.Data flows one way, and the parent has it on its first render.
Run code once when the app startsGuard it with a module variable, or run it when the module loads.Effects run twice in development, and again whenever a component mounts.
Read data that lives outside ReactUse useSyncExternalStore.React subscribes for you and always reads the latest value.
Load data for what's on screenKeep the effect, and ignore old answers in its cleanup.This is synchronizing with a server. The cleanup stops a slow, old answer from winning.
Render, handler, or effect?
  • Show a full name made from the first and last name in state
  • Show how many items are in the cart's items array
  • Filter 5,000 products by the search text, without redoing it for other changes
  • Show "Added to cart" after the Add button is pressed
  • POST the form's data after Join is pressed
  • Stay connected to the chat server while the chat is on screen
  • Send one analytics event because a page was shown
  • Load search results for the text in the search box
Try it yourself
0 of 8 correct

Sort each piece of code by where it belongs.

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

Common mix-ups

  • "Effects are how you react to a change in state." No. Rendering already reacts: every change renders the component again with the new values. Effects keep a component in step with something outside React.
  • "An effect with the right dependencies is fine." No. It can be correct and still cost an extra render, put old values on the page for one render, and run at times no event explains, like the toast on returning to the page.
  • "useMemo is an effect that runs before rendering." No. It runs during rendering, must be pure, and only skips a calculation whose inputs didn't change.
  • "A key is only for lists." No. Any component can take a key, and a new key gives it fresh state.
  • "An effect that runs twice in development is a bug in React." No. StrictMode does it on purpose. If running twice breaks your effect, like the one-time token, the code usually doesn't belong in an effect.
  • "Fetching in an effect is always wrong." No. Keeping results in step with the screen is synchronizing. Give the effect a cleanup that ignores old answers.

Practice exercises

Each exercise runs live. Read the console under the result: a small number beside a line counts StrictMode's repeats.

Exercise 1 · Warm-upCount the renders

You press Add tea once. Not counting StrictMode's repeats, how many renders does that cause? Answer first, then press Edit & run and read the console.

import { useEffect, useState } from "react";

export default function Cart() {
  const [items, setItems] = useState([]);
  const [count, setCount] = useState(0);
  console.log(`Render: count ${count}`);

  useEffect(() => {
    setCount(items.length);
  }, [items]);

  return (
    <>
      <p>Items: {count}</p>
      <button onClick={() => setItems([...items, "Tea"])}>Add tea</button>
    </>
  );
}

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

    Exercise 2 · Warm-upRemove the total's effect

    Rewrite the cart so the total is calculated during render, with no effect. Then press Add tea and Add cake. What does the total say?

    import { useEffect, useState } from "react"; export default function Cart() {  const [items, setItems] = useState([]);  const [total, setTotal] = useState(0);   useEffect(() => {    setTotal(items.reduce((sum, item) => sum + item.price, 0));  }, [items]);   return (    <>      <p>Total: ${total}</p>      <button onClick={() => setItems([...items, { name: "Tea", price: 3 }])}>Add tea</button>{" "}      <button onClick={() => setItems([...items, { name: "Cake", price: 4 }])}>Add cake</button>    </>  );}
    ResultLoading…

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

      Exercise 3 · PracticeStart each draft fresh

      Tick Attach a photo for Asha, then press Ravi: Ravi's draft still has a photo attached. Fix it so each contact's draft starts fresh. After your fix, what does Ravi's photo line say?

      import { useEffect, useState } from "react"; function PhotoOption() {  const [withPhoto, setWithPhoto] = useState(false);  return (    <p>      <label><input type="checkbox" checked={withPhoto} onChange={() => setWithPhoto(!withPhoto)} /> Attach a photo</label>      {" "}{withPhoto ? "Photo attached" : "No photo"}    </p>  );} function Draft({ contact }) {  const [text, setText] = useState("");   useEffect(() => {    setText("");  }, [contact]);   return (    <>      <label>Message to {contact} <input value={text} onChange={(event) => setText(event.target.value)} /></label>      <PhotoOption />    </>  );} export default function Chat() {  const [contact, setContact] = useState("Asha");  return (    <>      <button onClick={() => setContact("Asha")}>Asha</button>{" "}      <button onClick={() => setContact("Ravi")}>Ravi</button>      <Draft contact={contact} />    </>  );}
      ResultLoading…

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

        Exercise 4 · PracticeA toast only when you subscribe

        This visitor subscribed last week, and the page thanks them every time it loads (look at the console). Move the toast so it shows only when someone presses Subscribe. After your fix, how many toasts print when the page loads?

        import { useEffect, useState } from "react"; function NewsletterBox({ isSubscribed, onSubscribe }) {  useEffect(() => {    if (isSubscribed) {      console.log("Toast: Thanks for subscribing!");    }  }, [isSubscribed]);   return (    <button onClick={onSubscribe} disabled={isSubscribed}>      {isSubscribed ? "Subscribed" : "Subscribe"}    </button>  );} export default function Footer() {  // This visitor subscribed last week; the saved setting says so.  const [isSubscribed, setIsSubscribed] = useState(true);  return <NewsletterBox isSubscribed={isSubscribed} onSubscribe={() => setIsSubscribed(true)} />;}
        ResultLoading…

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

          Exercise 5 · ChallengeBreak the chain

          Press Buy a cake twice. The second press reaches 20 points and takes three renders. Remove the chain. After your fix, how many renders does the second press cause, not counting StrictMode's repeats?

          import { useEffect, useState } from "react"; export default function Rewards() {  const [points, setPoints] = useState(0);  const [level, setLevel] = useState("Bronze");  const [badge, setBadge] = useState("");  console.log(`Render: ${points} points, ${level}, badge "${badge}"`);   useEffect(() => {    if (points >= 20) setLevel("Silver");  }, [points]);   useEffect(() => {    if (level === "Silver") setBadge("Silver member");  }, [level]);   return (    <>      <p>{points} points. Level: {level} {badge && `(${badge})`}</p>      <button onClick={() => setPoints(points + 10)}>Buy a cake (+10 points)</button>    </>  );}
          ResultLoading…

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

            Exercise 6 · ChallengeStop the race

            Type gre quickly: the page ends on the results for "g". Add a cleanup so an old answer can't replace a newer one. After your fix, which search do the results on screen belong to?

            import { useEffect, useState } from "react";import { searchTeas } from "./api.js"; export default function TeaSearch() {  const [query, setQuery] = useState("");  const [results, setResults] = useState(null);   useEffect(() => {    if (!query) return;    console.log(`Fetch "${query}"`);    searchTeas(query).then((teas) => {      console.log(`Show results for "${query}"`);      setResults({ query, teas });    });  }, [query]);   return (    <>      <label>Search <input value={query} onChange={(event) => setQuery(event.target.value)} /></label>      {results && <p>Results for "{results.query}": {results.teas.join(", ") || "none"}</p>}    </>  );}
            ResultLoading…

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

              Check your understanding

              For each question, ask what the code is for: a value made from props and state, something a person did, or keeping in step with the world outside React.

              The You might not need an effect quiz · 8 questionsScore: first tries count
              1. Question 1 of 8When does a component really need an effect?

                Choose an answer to see the explanation.

              2. Question 2 of 8You click this button once. Not counting StrictMode's repeats, how many times does it render?

                Read the code, then predictPop out in the code editor (opens in a new tab)JSX
                import { useEffect, useState } from "react";
                
                export default function Price() {
                  const [price, setPrice] = useState(10);
                  const [sale, setSale] = useState(5);
                  console.log("Render");
                
                  useEffect(() => {
                    setSale(price / 2);
                  }, [price]);
                
                  return <button onClick={() => setPrice(price + 2)}>Sale price: {sale}</button>;
                }

                Choose an answer to see the explanation.

              3. Question 3 of 8What does key={shop} do when shop changes from Tea Corner to Cake Shop?

                Read the code, then predictJSX
                export default function App({ shop }) {
                  return <Review key={shop} shopName={shop} />;
                }

                Choose an answer to see the explanation.

              4. Question 4 of 8In the tea search with useMemo, you press Larger text. Does filterTeas run again?

                Choose an answer to see the explanation.

              5. Question 5 of 8A sign-up form must send its data when the person presses Join. Where does the POST go?

                Choose an answer to see the explanation.

              6. Question 6 of 8Why does checkSavedLogin run twice in the first app-start example?

                Choose an answer to see the explanation.

              7. Question 7 of 8In the search without a cleanup, you type gre quickly. Which results stay on the screen?

                Choose an answer to see the explanation.

              8. Question 8 of 8Where should GiftWrapToggle tell its parent that the box was ticked?

                Choose an answer to see the explanation.

              Key takeaways

              • An effect that only sets state from other state costs an extra render, and puts old values on the page for one render.
              • Calculate values from props and state during render. Wrap slow calculations in useMemo.
              • Give a component a key to start it fresh when a prop changes; store less state to avoid adjusting it.
              • Code caused by a click, a key, or a submit goes in that event's handler, including telling a parent and sending a form.
              • Code that must run once per page load goes outside components; data outside React is read with useSyncExternalStore.
              • Effects are for keeping a component in step with something outside React. A fetch is one, and needs a cleanup that ignores old answers.

              Calculate it, handle it, or synchronize it.
              Only the last one is an effect.

              Coming next: The lifecycle of an effect. It looks at the effects you keep: how each one starts and stops synchronizing, and why every value it reads must be a dependency.

              CompleteFrontend Clear concepts. Working examples.