1. Courses
  2. Developer Foundations
  3. React for the Web
  4. Stop 2

35 minIntermediate

State, effects and where data should live

Most React bugs on real screens are state in the wrong place: two copies of the same number that disagree, a filter that vanishes when you share a link, or an effect that shows the answer to an old search. This lesson gives you a short method for deciding where each piece of data lives, and shows the few jobs that really need an effect.

By the end you can

  • Predict what a state update does, using snapshots, updater functions and batching
  • Decide whether data is derived, server data, URL state, local state or lifted state
  • Write a pure reducer with immutable updates for the stock adjustment panel
  • Write an effect with the right dependencies and cleanup, and remove the effects you do not need
  • Stop a fetch-in-effect race condition with an ignore flag or an AbortController

The everyday picture

In a Mangrove store, every piece of information has one home. The stock count for rice lives in head office's system; the shelf label only shows it. The basket being scanned lives at one till and nobody else needs it. When the morning team wants the list of low-stock items, nobody keeps a second list on the back-room whiteboard, because the moment a delivery arrives that list is wrong. They look at the counts and the reorder levels and work it out again.

Some things should travel. If Ama writes "aisle 4, low stock only" on a request slip, she can photocopy it and hand it to anyone, and they see the same thing. And a few jobs mean picking up the phone to someone outside the store, like the supplier. You call, you talk, and you hang up when you are done. In React, those are derived values, server data, local state, URL state and effects.

State is a snapshot

useState gives a component a value that survives between renders, plus a function to change it. The key idea is that each render sees its own fixed copy of state, a . When you call the set function, React does not change the variable your code is holding. It queues an update and, after your event handler has finished, renders the component again with the new value.

That explains a classic surprise on the adjustment panel. A "+2" button that calls setDelta(delta + 1) twice adds 1, because both calls read the same delta from the same snapshot. If the next value depends on the previous one, pass an instead:

TypeScript
const [delta, setDelta] = useState(0);

function addTwoWrong() {
  setDelta(delta + 1);   // delta is 0 in this render: queue "set to 1"
  setDelta(delta + 1);   // still 0: queue "set to 1" again
}

function addTwo() {
  setDelta((d) => d + 1);  // queue "add 1" to whatever is latest
  setDelta((d) => d + 1);  // 0, then 1, then 2
}

React also updates: all the set calls in one event become one re-render. Since React 18 this happens everywhere, including inside timeouts and promise callbacks. And React compares the new value with the old one using Object.is. If you change an object in place and set the same object again, React sees the same reference and may skip the render. That is why state updates are immutable: you build a new object with the change (Objects, arrays and references shows the mechanism).

Quick check

delta is 0. A button runs for (let i = 0; i < 10; i++) setDelta(delta + 1). What is delta after one click?

Deciding where each piece of data lives

Before you write useState, ask five questions about the data, in this order. The first answer that fits wins.

  1. Can you compute it from props, other state or the URL? Then it is a . Calculate it during render and store nothing.
  2. Does it come from the server? Stock rows, prices and order counts belong to the server. The screen holds a cached copy that it loads and refreshes (Loading server data). Do not copy them into useState and edit them there.
  3. Should a link, a reload or the back button bring it back? Search text, filters, the current tab and the page number usually should. Put them in the URL's .
  4. Does only one component use it? An open dropdown, a half-typed note, a draft adjustment. Keep it local in that component.
  5. Do two components need it? Find their closest common parent, keep the state there, and pass it down as props, with functions to change it. This is called .

Applied to the stock screen: the rows come from the server. The search text and the low-only box go in the URL, because Ama shares links with her area manager on WhatsApp or email. The adjustment draft is local to the panel. The selected article is used by the table (to highlight the row) and by the panel, so it lives in StockScreen, their common parent. The low-stock count and the filtered rows are derived.

The most common wrong answer is storing a derived value. It looks harmless:

TypeScript
// Avoid: a second copy of the rows that must be kept in sync by hand
const [rows, setRows] = useState<StockItem[]>([]);
useEffect(() => {
  setRows(visibleItems(items, { q, lowOnly }));
}, [q]);                     // forgot items and lowOnly: rows go stale

// Better: compute during render. One source, always current.
const rows = visibleItems(items, { q, lowOnly });

The first version renders twice for every change (once with the old rows, then again after the effect), and it is wrong whenever someone forgets a dependency. Filtering 2,000 rows during render usually takes far less time than one frame. If you measure that a calculation is slow, wrap it in useMemo; the React Compiler can add this kind of memoisation for you. Either way, the value stays derived.

Quick check

The screen's header says "Stock: 7 low". Where should the 7 come from?

URL ?q=oil&lowOnly=1 Server stock rows reads items prop StockScreen derives the rows props SearchBar writes URL StockTable rows prop AdjustPanel own reducer replace() on typing URL: shareable Server: real counts Local: drafts
Where each piece of state lives on the stock screen. The search box writes the filters to the URL, StockScreen reads them and the server's rows and derives what to show, and the adjustment draft stays inside the panel. Nothing is stored twice.

A reducer for the adjustment panel

The adjustment panel has a delta, a reason and a note, and several buttons change them: +1, -1, +10, -10, pick a reason, type a note, clear. Spread across four useState calls, the rules ("the delta stays between -500 and 500", "clear keeps the article") end up copied into every handler. useReducer moves them into one function.

A takes the current state and an , a plain object describing what happened, and returns the next state. The component calls dispatch({ type: 'stepped', by: 10 }) and does not care how the state changes. Two rules apply. The reducer must be pure: no fetches, no timers, no random numbers, the same input always gives the same output. In development, StrictMode calls it twice to help you catch impure ones. And it must return a new object whenever something changed, never edit state in place:

TypeScript
case 'stepped': {
  // Wrong: state.delta += action.by; return state;  (same object, React may skip the render)
  const delta = Math.max(-500, Math.min(500, state.delta + action.by));
  return { ...state, delta };   // a new object with the change
}

A pure reducer is also the easiest code in the app to test. It needs no React, no browser and no mocks, which is why the Practice section tests one in your browser. Use useState for one or two independent values, and a reducer when several events change related values under shared rules.

Effects synchronise with things outside React

An is code that runs after React has updated the screen. Its job is to keep the component in step with an external system: a timer, a browser event, a WebSocket, a map or chart library that draws on its own. An effect has three parts: the setup code, an optional function it returns, and a .

The stock rows show "updated 3 min ago" from each row's updatedAt, and the text should move on even when nothing else changes. A clock is outside React, so this is a real effect:

TypeScript
function useNow(everyMs: number) {
  const [now, setNow] = useState(() => Date.now());
  useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), everyMs); // setup: start the timer
    return () => clearInterval(id);                             // cleanup: stop it
  }, [everyMs]);                                                // re-sync if the interval changes
  return now;
}

The lifecycle is always the same. After the first render, React runs the setup. After each later render, it compares every dependency with its previous value. If one changed, React runs the old cleanup and then the new setup. When the component leaves the screen, it runs the last cleanup. With no array the effect runs after every render; with [] it runs once after the component appears. In development, StrictMode runs setup, cleanup and setup again once when the component first appears. If that double run breaks something, your cleanup is missing or incomplete.

For subscribing to a browser value such as "is the store PC online?", React has a purpose-built hook, useSyncExternalStore, which handles the subscription and cleanup for you:

TypeScript
function subscribe(onChange: () => void) {
  window.addEventListener('online', onChange);
  window.addEventListener('offline', onChange);
  return () => {
    window.removeEventListener('online', onChange);
    window.removeEventListener('offline', onChange);
  };
}

export function useOnline() {
  return useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
}

The React docs page "You Might Not Need an Effect" lists the cases where an effect is the wrong tool. Three come up on almost every screen:

  • Data you can compute. Filtered rows, totals, "after this change" counts: compute them during render, as above.
  • Resetting state when a prop changes. When Ama moves from rice to oil, the panel's draft should start fresh. An effect that watches item and clears the state renders the stale draft first. Give the panel a instead: <AdjustPanel key={item.articleId} … />. A new key is a new component with fresh state.
  • Things the user did. Saving an adjustment happens because Ama pressed Save, so the request belongs in the click or submit handler. An effect that watches a submitted flag runs again at odd moments, such as the StrictMode double run or a re-render, and can send the same adjustment twice.

Quick check

Which of these is a job for an effect?

The race in fetch-in-effect

Fetching in an effect is allowed: the network is an external system. It has a trap. Each change of the search text starts a new request, and requests do not come back in the order you sent them. On a store's weak mobile connection, an older, slower answer can arrive after the newer one and overwrite it. This is a . The cleanup fixes it, because React runs the old effect's cleanup before starting the new one:

TypeScript
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/stores/${storeId}/stock?q=${encodeURIComponent(q)}`, { signal: controller.signal })
    .then((res) => res.json())
    .then((rows: StockItem[]) => setItems(rows))
    .catch((err) => {
      if (err.name !== 'AbortError') setError(err);  // an aborted request is not an error
    });
  return () => controller.abort();   // a newer search cancels this one
}, [storeId, q]);

The React docs show the same idea with an ignore flag: the setup declares let ignore = false, the cleanup sets it to true, and the code calls setItems only if !ignore. The flag stops the old answer from reaching the screen; AbortController also cancels the download, which saves data on a slow connection. Use both ideas together if your fetch helper cannot take a signal.

Even with the race fixed, this effect still has no caching, no loading state shared with other screens, and it waits for the JavaScript to load before it starts. That is why Loading server data moves the stock list out of effects into Server Components, and uses a data library for the live order counts.

0 s 2 s 3 s 5 s milk oil No cleanup last wins oil rows milk rows With cleanup latest wins oil rows ignored
Two searches on a slow connection. The milk request leaves at 0 s and answers at 5 s; the oil request leaves at 2 s and answers at 3 s. Without cleanup, the late milk answer overwrites the oil rows while the box still says "oil". With an abort or ignore flag in the cleanup, only the latest search can reach the screen.

Build it

Add the search and the low-only filter to the stock screen, stored in the URL, and an adjustment panel driven by a reducer. This uses the Next.js 16 App Router; the rows come from a sample file for now, and Loading server data replaces it with a server fetch.

  1. Pure helpers for the stock rows

    Keep the logic in plain functions, outside components, so you can test it without React.

    TypeScript
    // app/stock/stock.ts
    export type StockItem = {
      articleId: string; name: string; category: string;
      onHand: number; reorderLevel: number; priceCents: number; updatedAt: string;
    };
    export type Filters = { q: string; lowOnly: boolean };
    
    export const isLow = (item: StockItem) => item.onHand <= item.reorderLevel;
    
    // The URL is the source of the filters; this turns it into a typed object
    export function readFilters(params: { get(name: string): string | null }): Filters {
      return { q: params.get('q') ?? '', lowOnly: params.get('lowOnly') === '1' };
    }
    
    export function visibleItems(items: StockItem[], { q, lowOnly }: Filters): StockItem[] {
      const needle = q.trim().toLowerCase();
      return items
        .filter((item) => !lowOnly || isLow(item))
        .filter((item) => needle === ''
          || item.name.toLowerCase().includes(needle)
          || item.articleId.toLowerCase() === needle)
        .sort((a, b) => a.name.localeCompare(b.name)); // filter made a new array, so sorting it is safe
    }
  2. The adjustment reducer

    TypeScript
    // app/stock/adjust.ts
    export type Reason = 'damaged' | 'expired' | 'count-correction' | 'theft' | 'received';
    export type Draft = { articleId: string; delta: number; reason: Reason; note: string };
    export type DraftAction =
      | { type: 'stepped'; by: number }
      | { type: 'reasonPicked'; reason: Reason }
      | { type: 'noteTyped'; note: string }
      | { type: 'reset' };
    
    export const MAX_DELTA = 500;
    export const emptyDraft = (articleId: string): Draft =>
      ({ articleId, delta: 0, reason: 'damaged', note: '' });
    
    export function adjustReducer(state: Draft, action: DraftAction): Draft {
      switch (action.type) {
        case 'stepped': {
          const delta = Math.max(-MAX_DELTA, Math.min(MAX_DELTA, state.delta + action.by));
          return { ...state, delta };                 // new object, so React sees the change
        }
        case 'reasonPicked':
          return { ...state, reason: action.reason };
        case 'noteTyped':
          return { ...state, note: action.note };
        case 'reset':
          return emptyDraft(state.articleId);         // clearing keeps the article
        default:
          return state;
      }
    }
  3. Write the filters to the URL from the search bar

    The filters change because the user typed or ticked, so the URL is updated in the event handlers, with no effect. router.replace changes the URL without adding a history entry, so the back button does not step through every letter. scroll: false keeps the page where it is.

    TypeScript
    // app/stock/SearchBar.tsx
    'use client';
    import { usePathname, useRouter, useSearchParams } from 'next/navigation';
    
    export function SearchBar() {
      const searchParams = useSearchParams();
      const pathname = usePathname();
      const router = useRouter();
    
      function setParam(name: 'q' | 'lowOnly', value: string) {
        const params = new URLSearchParams(searchParams.toString());
        if (value) params.set(name, value);
        else params.delete(name);                       // default filters keep the link short
        const query = params.toString();
        router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
      }
    
      return (
        <div role="search">
          <label>
            Search
            <input type="search" defaultValue={searchParams.get('q') ?? ''}
              onChange={(e) => setParam('q', e.target.value)} />
          </label>
          <label>
            <input type="checkbox" defaultChecked={searchParams.get('lowOnly') === '1'}
              onChange={(e) => setParam('lowOnly', e.target.checked ? '1' : '')} />
            Low stock only
          </label>
        </div>
      );
    }
  4. The panel with useReducer

    The third argument of useReducer builds the first state from the article id. "After this change" is derived, never stored. Saving the draft is Forms that people can trust.

    TypeScript
    // app/stock/AdjustPanel.tsx
    'use client';
    import { useReducer } from 'react';
    import { adjustReducer, emptyDraft, type Reason } from './adjust';
    import type { StockItem } from './stock';
    
    const REASONS: Reason[] = ['damaged', 'expired', 'count-correction', 'theft', 'received'];
    
    export function AdjustPanel({ item, onClose }: { item: StockItem; onClose: () => void }) {
      const [draft, dispatch] = useReducer(adjustReducer, item.articleId, emptyDraft);
      const after = item.onHand + draft.delta;                          // derived
      const needsNote = draft.reason === 'count-correction' || draft.reason === 'theft';
    
      return (
        <section aria-label={`Adjust ${item.name}`}>
          <h2>{item.name}</h2>
          <p>On hand {item.onHand}, after this change {after}</p>
          {[-10, -1, 1, 10].map((by) => (
            <button key={by} type="button" onClick={() => dispatch({ type: 'stepped', by })}>
              {by > 0 ? `+${by}` : by}
            </button>
          ))}
          <select aria-label="Reason" value={draft.reason}
            onChange={(e) => dispatch({ type: 'reasonPicked', reason: e.target.value as Reason })}>
            {REASONS.map((r) => <option key={r} value={r}>{r}</option>)}
          </select>
          {needsNote && (
            <textarea aria-label="Note" value={draft.note}
              onChange={(e) => dispatch({ type: 'noteTyped', note: e.target.value })} />
          )}
          <button type="button" onClick={() => dispatch({ type: 'reset' })}>Clear</button>
          <button type="button" onClick={onClose}>Close</button>
        </section>
      );
    }
  5. The screen: URL state, local state, derived values

    The selection is stored as an id, so the panel always shows the current row from the server data, never an old copy. The key gives each article a fresh draft.

    TypeScript
    // app/stock/StockScreen.tsx
    'use client';
    import { useState } from 'react';
    import { useSearchParams } from 'next/navigation';
    import { readFilters, visibleItems, isLow, type StockItem } from './stock';
    import { SearchBar } from './SearchBar';
    import { AdjustPanel } from './AdjustPanel';
    
    export function StockScreen({ items }: { items: StockItem[] }) {
      const searchParams = useSearchParams();
      const filters = readFilters(searchParams);                          // URL state
      const [selectedId, setSelectedId] = useState<string | null>(null); // local state
    
      const rows = visibleItems(items, filters);                          // derived
      const lowCount = items.filter(isLow).length;                        // derived
      const selected = items.find((i) => i.articleId === selectedId) ?? null;
    
      return (
        <main>
          <h1>Stock: {lowCount} low</h1>
          <SearchBar />
          <table>
            <thead><tr><th>Item</th><th>On hand</th><th>Reorder at</th><th></th></tr></thead>
            <tbody>
              {rows.map((item) => (
                <tr key={item.articleId} className={item.articleId === selectedId ? 'selected' : undefined}>
                  <td>{item.name}</td><td>{item.onHand}</td><td>{item.reorderLevel}</td>
                  <td><button type="button" onClick={() => setSelectedId(item.articleId)}>Adjust</button></td>
                </tr>
              ))}
            </tbody>
          </table>
          {selected && (
            <AdjustPanel key={selected.articleId} item={selected} onClose={() => setSelectedId(null)} />
          )}
        </main>
      );
    }
  6. The page, with a Suspense boundary

    The page is a Server Component. A Client Component that calls useSearchParams on a prerendered page must sit inside <Suspense>; without it, the production build fails with "Missing Suspense boundary with useSearchParams". In development it works either way, so add the boundary now.

    TypeScript
    // app/stock/page.tsx
    import { Suspense } from 'react';
    import { StockScreen } from './StockScreen';
    import { sampleStock } from './sample-stock';   // a dozen StockItem rows; fe-03 fetches real ones
    
    export default function StockPage() {
      return (
        <Suspense fallback={<p>Loading stock…</p>}>
          <StockScreen items={sampleStock} />
        </Suspense>
      );
    }
  7. Try it

    Run npm run dev and open /stock. Type oil and tick "Low stock only": the address bar becomes /stock?q=oil&lowOnly=1. Copy it into a new tab, and the same view opens. Open the panel for one item, press +10 twice, then open another item: the draft starts at 0, thanks to the key. Press reload: the filters survive, because they were never component state.

Bring it to your team

Questions for your next front-end code review.

  • Is this value stored anywhere else already? Could we compute it during render instead?
  • If Ama shares this link or presses reload, does she see the same filters?
  • What external system does this effect synchronise with? If none, why is it an effect?
  • Does every effect clean up: timers cleared, listeners removed, old requests aborted or ignored?
  • Is the dependency array honest, or was a value left out to stop a loop?
  • Are reducers pure, with immutable updates and their own unit tests?

Red flags

  • useEffect(() => setSomething(compute(a, b)), [a]): derived state with a missing dependency.
  • Props copied into useState and then "kept in sync" with an effect.
  • A fetch in an effect with no cleanup, on a screen people use on slow connections.
  • Filters that live only in component state on a screen people share.
  • Turning off StrictMode because "effects run twice".

Practice

Fix the adjustment reducer

The starter reducer for the adjustment panel changes state in place and returns the same object, so React would not re-render. Rewrite it with immutable updates. Actions: { type: 'stepped', by } adds by to delta and keeps it between -500 and 500; { type: 'reasonPicked', reason } and { type: 'noteTyped', note } set those fields; { type: 'reset' } returns { articleId, delta: 0, reason: 'damaged', note: '' } for the same article; any other action returns the same state object.

JavaScript exerciseRuns in your browser with tests
function adjustReducer(state, action) {
  switch (action.type) {
    case 'stepped':
      state.delta += action.by;
      return state;
    case 'reasonPicked':
      state.reason = action.reason;
      return state;
    case 'noteTyped':
      state.note = action.note;
      return state;
    case 'reset':
      state.delta = 0;
      return state;
    default:
      return state;
  }
}
Ctrl + Enter runs
Show a solution
JavaScript
function adjustReducer(state, action) {
  switch (action.type) {
    case 'stepped': {
      const delta = Math.max(-500, Math.min(500, state.delta + action.by));
      return { ...state, delta };
    }
    case 'reasonPicked':
      return { ...state, reason: action.reason };
    case 'noteTyped':
      return { ...state, note: action.note };
    case 'reset':
      return { articleId: state.articleId, delta: 0, reason: 'damaged', note: '' };
    default:
      return state;
  }
}
Pass the exercise to finish this task.

Derive the visible rows

Write visibleItems(items, { q, lowOnly }). It returns a new array of the items to show, sorted by name (use a.name.localeCompare(b.name)). An item matches the search if its name contains q or its articleId equals q, ignoring case and spaces around q; an empty q matches everything. With lowOnly, keep only low items: onHand <= reorderLevel. Never change the array you were given. The starter has three bugs.

JavaScript exerciseRuns in your browser with tests
function visibleItems(items, { q, lowOnly }) {
  items.sort((a, b) => a.name.localeCompare(b.name));
  return items
    .filter((item) => !lowOnly || item.onHand < item.reorderLevel)
    .filter((item) => item.name.includes(q) || item.articleId === q);
}
Ctrl + Enter runs
Show a solution
JavaScript
function visibleItems(items, { q, lowOnly }) {
  const needle = q.trim().toLowerCase();
  return items
    .filter((item) => !lowOnly || item.onHand <= item.reorderLevel)
    .filter((item) => needle === ''
      || item.name.toLowerCase().includes(needle)
      || item.articleId.toLowerCase() === needle)
    .sort((a, b) => a.name.localeCompare(b.name));  // filter returned a new array
}
Pass the exercise to finish this task.

Check yourself

Ama opens the adjustment panel for rice, presses +10, then clicks Adjust on cooking oil. The panel now shows cooking oil with a delta of +10 left over from rice. What is the best fix?

The stock table highlights the selected row, and the adjustment panel shows the selected item. The team does not want links to open the panel. Where should selectedId live?

In development, the network panel shows the stock request twice when the screen opens. In production it appears once. What is happening, and what should you do?

Flashcards

Open my review

Key takeaways

  • State is a snapshot per render. Use an updater function when the next value depends on the last, and always update immutably.
  • Ask in order: derived, server data, URL, local, lifted. Store each piece of data in one place only.
  • Shareable filters belong in the URL; drafts with one owner stay local; use a pure reducer when several events change related values.
  • Effects synchronise with external systems, with honest dependencies and a cleanup. Derived data, key resets and user events do not need one.
  • Fetch in an effect only with an abort or ignore flag, and prefer moving server data out of effects.

Sources

Episode transcriptOne home for every number · about 12 minutes · tap any line to play from there
  1. NadiaImran, picture Ama at seven forty in the morning. Her store is at the edge of town and the mobile data is weak. She types milk into the stock screen, changes her mind, and types oil.
  2. ImranAnd she sees oil. That's what she typed.
  3. NadiaShe sees oil for two seconds. Then the screen quietly fills with milk rows, while the search box still says oil. She taps Adjust on the top row and records six units as damaged against a carton of milk.
  4. ImranThat's a bad morning. And a wrong stock count that nobody notices for a week.
  5. NadiaToday is about why that happens, and about the bigger question behind it. In a React screen, where should each piece of data live? Get that right and most of these bugs never get written.
  6. NadiaStart with the store itself. Every piece of information has one home. The rice count lives in head office's system. The shelf label only shows it. The basket being scanned lives at one till, and nobody else needs it.
  7. ImranAnd the low stock list? Some stores keep one on the whiteboard in the back room.
  8. NadiaAnd the moment a delivery arrives, that whiteboard is wrong. A good team doesn't keep a second list. They look at the counts and the reorder levels and work it out again. In React, that's a derived value.
  9. ImranSo the shelf label is the screen, head office is the server, the till basket is local state, and the whiteboard is the mistake.
  10. NadiaExactly. Two more pieces. If Ama writes aisle four, low stock only, on a request slip, she can photocopy it and hand it to anyone. That's the URL. And when you need to phone the supplier, you call, you talk, and you hang up. That's an effect.
  11. ImranLet's start with plain state then. Use State. I call the set function and the value changes.
  12. NadiaThat's the reasonable guess, and it's the first trap. Each render sees its own fixed copy of state. A snapshot. When you call the set function, the variable your code is holding doesn't change. React queues an update and renders again after your handler finishes.
  13. ImranGive me the case where that bites.
  14. NadiaThe adjustment panel has a plus two button. Someone writes set delta to delta plus one, twice. Delta is zero in this render, so both calls say set it to one. You press plus two and get one.
  15. ImranAnd the fix?
  16. NadiaAn updater function. You pass a function that takes the latest value and adds one. React runs them in order, zero, one, two. Use it whenever the next value depends on the last one.
  17. ImranAnd if I call set three times in one click, does it render three times?
  18. NadiaOnce. React batches updates from the same event into one render. Since React eighteen that happens everywhere, even inside timers and promises.
  19. NadiaOne more thing. React compares old and new state by reference. If you change an object in place and set the same object, React may skip the render. So you always build a new object.
  20. ImranThat's the immutable update from the JavaScript course.
  21. NadiaRight. Now the big question. Before you write use State, ask five things, in order. Can I compute it from what I already have? Does it come from the server? Should a link or a reload bring it back? Does only one component use it? Or do two components need it?
  22. ImranAnd the first yes wins.
  23. NadiaThe first yes wins. Computed means derived, so you work it out during render and store nothing. Server data, like stock rows and prices, belongs to the server; the next lesson loads it properly. Things a link should bring back go in the URL. One user, keep it local. Two siblings, lift it to their closest common parent.
  24. NadiaIf you can see your screen, the first diagram shows the stock screen. The URL and the server sit at the top. Stock Screen in the middle reads both. Below it, the search bar writes to the URL, the table gets rows as props, and the adjustment panel keeps its own draft.
  25. ImranWhy the URL for the search? It feels like the search bar's own business.
  26. NadiaBecause Ama shares links. She sends her area manager a link on WhatsApp or email that says oil, low stock only. If the filter lives in the search bar's state, the manager opens the link and sees all the stock in the store.
  27. ImranAnd the selected row? The table highlights it and the panel shows it.
  28. NadiaTwo siblings need it, so it lives in their parent, Stock Screen. And it's stored as an article ID, not a copy of the item. Then the panel always shows the current row from the server data.
  29. ImranI've seen code that keeps the filtered list in state and updates it in an effect whenever the search text changes. Is that so bad?
  30. NadiaIt renders twice for every keystroke, once with the old list, then again. And the day someone forgets a dependency, like the low only box, the list goes stale. Filtering a couple of thousand rows during render is fast. If you measure that it's slow, memoise it. It stays derived either way.
  31. ImranSame story with copying a prop into state, I guess.
  32. NadiaSame story. The copy is read once and never follows the prop again. The server sends a new count and the panel shows the old one.
  33. ImranNow the panel itself. Plus one, minus one, plus ten, a reason, a note, a clear button. That's a lot of set calls.
  34. NadiaThat's where use Reducer helps. A reducer is one pure function. It takes the current state and an action, a plain object that says what happened, like stepped by ten. It returns the next state. The rules live in one place: the delta stays between minus five hundred and five hundred, and clear keeps the article.
  35. ImranPure meaning no fetches, no timers inside it.
  36. NadiaYes, and it returns a new object every time something changes. A bonus: it's the easiest code in the app to test. No React, no browser. The practice section has you fix one that changes state in place.
  37. ImranOkay. Effects. When do I actually need one?
  38. NadiaWhen you have to keep the component in step with something outside React. A timer, a browser event, a socket, a chart library that draws on its own. The stock rows say updated three minutes ago, and that text has to move on. So an effect starts an interval, and its cleanup clears it.
  39. ImranAnd the dependency array decides when it runs again.
  40. NadiaRight. If a dependency changed, React runs the old cleanup first, then the new setup. When the component leaves, the last cleanup runs. And in development, StrictMode runs setup, cleanup and setup again once, on purpose, to show you whether your cleanup really undoes the setup.
  41. ImranWhat are the cases where people reach for an effect and shouldn't?
  42. NadiaThree on almost every screen. Data you can compute. Resetting state when the item changes, where you give the panel a key instead. And things the user did, like pressing Save. That request goes in the click handler, not in an effect watching a flag.
  43. ImranThe key trick. Ama moves from rice to oil and the draft starts fresh.
  44. NadiaBecause a new key means a new component with new state. No extra render with the old draft.
  45. NadiaSo back to the morning. The first version of the screen fetched in an effect with no cleanup. The milk request took about five seconds on that connection. The oil request, sent two seconds later, took about one.
  46. ImranSo oil answers at three seconds and milk at five. Last answer wins, and the last answer is the old one.
  47. NadiaThat's the second diagram. A time line along the top, a long bar for milk and a short bar for oil. Underneath, two lanes. With no cleanup, the milk rows replace the oil rows at five seconds. With cleanup, the late milk answer is ignored.
  48. ImranAnd the cleanup is what?
  49. NadiaEither an ignore flag that the cleanup sets to true, so the old answer never sets state. Or an abort controller, where the cleanup cancels the old request. On a slow connection the abort also saves data, because the download stops.
  50. ImranSo with that cleanup, Ama's morning is fine.
  51. NadiaThe race is fixed. But the effect still has no caching and waits for the JavaScript to load before it even starts. That's why the next lesson moves the stock list out of effects altogether.
  52. NadiaThe Build it puts this together in the Next.js App Router. You write the filter and sort logic as plain functions. Then the reducer for the panel. Then a search bar that writes the search text and the low only box to the URL, inside its change handlers, with router replace.
  53. ImranWhy replace and not push?
  54. NadiaPush adds a history entry for every letter, and the back button walks through o, oi, oil. Replace changes the address without that. Then the screen reads the filters from the URL, keeps the selected ID, and computes the rows and the low count during render.
  55. ImranAnd the page wraps it in a Suspense boundary, because the screen reads search params.
  56. NadiaYes. Without it the production build fails, even though development seems fine. Last step: you copy the link into a new tab and get the same view.
  57. ImranFor my next review, three questions. Is this value already stored somewhere, and could we compute it? If Ama shares this link, does she see the same filters? And what outside system does this effect talk to?
  58. NadiaGood. I'd add one: does every effect clean up? Timers cleared, listeners removed, old requests cancelled.
  59. NadiaOne to think about. In development you see every stock request twice when the screen opens. In production, once. A teammate wants to turn off StrictMode. What do you say? Take a second.
  60. ImranI'd say the double run is deliberate. If the cleanup aborts or ignores the first request, the screen is correct. Turning it off just hides a missing cleanup until the race shows up in a store.
  61. NadiaExactly right.
  62. ImranMy summary. One: state is a snapshot. Use an updater when the next value depends on the last, and always build new objects.
  63. ImranTwo: ask in order. Derived, server, URL, local, lifted. Every number gets one home.
  64. ImranThree: effects are for talking to things outside React, with a cleanup. Everything else goes in render or in a handler.
  65. NadiaOne addition to the third. Fetching in an effect is allowed, but only with an abort or an ignore flag. And it's usually a sign the data wants a better home.
  66. ImranLike the back-room whiteboard.
  67. NadiaExactly. Next time: loading server data. Server Components, loading states and error boundaries for the stock list.
Finished reading?Mark it complete to fill in its stop on your course line.

Use it at work

Loyalty card screen (React)

A phone screen with its points rules in a tested helper file.

Open in the Code Lab
Next stopLoading server data
Was this lesson helpful?
Questions and discussion

Loading the discussion…