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

35 minIntermediate

Components, props and how React renders

Every React screen is a tree of small functions that turn data into a description of the page. In this lesson you cut Mangrove Market's back-office stock screen into components, and you learn exactly when React calls them again and what it changes on screen afterwards.

By the end you can

  • Turn a screen sketch into a component tree with clear, typed props
  • Explain what JSX becomes, and why a component must be pure while it renders
  • Walk through trigger, render, reconcile and commit, and say which components run after a state change
  • Choose list keys that keep each row's state with the right item
  • Build the static stock screen in TypeScript and watch it re-render in the console

The everyday picture

Think of the shelf labels in a supermarket. Every night head office sends each store a fresh plan of what should sit on every shelf, with prices. The night staff do not tear down the aisles and build them again. They walk along the shelves with the new plan, compare it with what is already there, and swap only the labels that differ. A price went from $2.40 to $2.20: one label changes. Nothing changed on a shelf: they leave it alone.

They also match products by barcode, never by "third slot from the left". If the flour is removed from a shelf, the oil and the sugar keep their labels, and the sticky note a colleague left on the oil ("count again, 12 found") stays on the oil. If they matched by slot number, the note would slide onto the sugar.

React works the same way. Your components write a fresh plan of the screen. React compares it with the last plan and changes only what differs on the real page. The barcode is the key you give each row. Keep this picture in mind: plan, compare, swap.

From a sketch to a component tree

Store managers like Ama open the back-office stock screen every morning before the store opens. Leila, the designer, has sketched it: a search box at the top, a table of items with their stock and reorder levels, a panel on the side to adjust a count, and a small card with today's app orders. The data behind each row is a StockItem: articleId, name, category, onHand, reorderLevel, priceCents and updatedAt. An item is low when onHand <= reorderLevel, and money is in integer cents (Working with data: arrays, Maps, JSON, dates and money explains why).

A is a function that takes data and returns a description of a piece of the screen. To cut a sketch into components, draw a box around every part and use three tests:

  • One job each. The search box collects a query. The table lays out rows. If you need "and" to describe a box ("shows the table and saves adjustments"), split it.
  • Anything that repeats is a component. One StockRow per article, whether the store shows 40 low items or about 2,000 items.
  • Name it after what it shows, so a teammate can find it: StockScreen, SearchBar, StockTable, StockRow, AdjustPanel, OrderSummary.

Components talk through . A parent writes <StockTable items={visible} /> and the child receives one object, { items: visible }. Props are read-only for the child. When a child needs something to change, such as the search text, the parent passes a function as a prop (onQueryChange) and the child calls it. So data flows down the tree as props, and events flow up as calls to those functions. That rule keeps every value with one clear owner.

One prop is special: children. Whatever you write between a component's tags arrives as props.children. A small Panel with a title and a border can wrap the adjust form and the order card without knowing what is inside. This is composition: you build bigger parts by nesting smaller ones, instead of adding a new prop for every variation. With TypeScript, each component declares its props as a type, so a missing or misspelled prop fails in your editor before the page ever loads.

Quick check

When Ama clicks "Adjust" on a row, the screen should open the adjust panel for that item. How does StockRow tell its parents?

StockScreen holds the state: items, query SearchBar query, onQueryChange StockTable items (filtered) StockRow ×42 item key: articleId AdjustPanel selected item OrderSummary counts
The stock screen as a component tree. StockScreen owns the data and passes each child only the props it needs (right). StockTable renders one StockRow per visible item, 42 of them in this store's low-stock view. Typing goes back up through the onQueryChange function that SearchBar received.

JSX is a function call, and rendering must be pure

looks like HTML, but the browser never sees it. Your build tool (Vite, Next.js, or TypeScript with "jsx": "react-jsx") turns every tag into a function call. With the modern JSX transform that React 19 requires, the compiler adds an import from react/jsx-runtime for you:

TypeScript
<StockRow key={item.articleId} item={item} />

The call does not run StockRow and does not create a table row in the page. It returns an , a plain object that describes what you want. Here is what React 19 returns, printed in Node with the production build:

Output
{
  '$$typeof': Symbol(react.transitional.element),
  type: [Function: StockRow],
  key: 'A2040',
  ref: null,
  props: { item: { articleId: 'A2040', name: 'Cooking oil 2 L', onHand: 6 } }
}

The type is either a tag name such as 'td' or your component function. The key sits next to props, which is why a component cannot read its own key. A component returns a tree of these objects, and React decides later what to do with them. Creating elements is cheap: thousands of small objects per render is normal.

Because React may call your component at any time, as often as it likes, the function must be while it renders. That means two things. Same props and state in, same elements out. And it changes nothing that existed before it was called: no edits to props, no writes to outside variables, no requests to the server, no timers. Those side effects belong in event handlers, such as the click on "Save", or, as a last resort, in an effect (State, effects and where data should live).

A classic impure bug hides in one innocent line. StockTable wants rows in name order and writes items.sort(byName). But sort() changes an array in place, and items belongs to StockScreen. Now the parent's own list is reordered as a side effect of drawing a child, and anything else that reads that array sees an order nobody asked for. Copy first: [...items].sort(byName), or items.toSorted(byName) in browsers from 2023 onwards. Objects, arrays and references covers which array methods mutate.

React helps you catch these bugs. Inside <StrictMode>, in development only, React calls each component function twice, along with the functions you pass to useState, useMemo and useReducer, and keeps one result. A pure component gives the same answer both times, so you see no difference. An impure one shows itself: a row pushed into an array appears twice, a counter jumps by two. StrictMode also runs every effect's setup and cleanup one extra time when a component first appears. None of this happens in the production build. In React 19 the second call reuses the results of useMemo and useCallback from the first. A new Vite React project wraps the app in <StrictMode>, and the Next.js App Router turns it on by default (since Next.js 13.5.1).

Quick check

What does the expression <StockRow item={item} /> give you when it runs?

Trigger, render, reconcile, commit

Every change on a React screen goes through the same four steps. Learn them once and most "why did this run?" questions answer themselves.

  1. Trigger. Something asks for a render. The first time, that is createRoot(...).render(<App />). After that, it is a state update: Ama types "o", the input's change handler calls setQuery('o'), and React queues a render of StockScreen, the component that owns that state.
  2. Render. React calls StockScreen again. It reads the new query, filters the items and returns new elements. Then React calls every component in those elements, and their children, all the way down. here means calling your functions. Nothing on the page has changed yet.
  3. Reconcile. While it renders, React compares the new tree with the previous one, child by child. Same type in the same place (and the same key in a list) means "this is the same component": React keeps its state and its page nodes and only notes changed attributes or text. A different type, or a key that is gone, means "destroy the old one and its state". This comparison is called .
  4. Commit. React applies the changes it found to the in one go: remove two rows, update the input's value. Then the browser paints. Commit is the only step that touches the page (JavaScript in the browser covers the DOM itself).
1. Trigger setQuery('o') own state, a parent, a context queues a render 2. Render call the components must be pure: no page changes new element tree 3. Reconcile compare type + key same key: keep state and nodes list of changes 4. Commit change only the diff the only step that edits the DOM DOM updated Browser paints
One keystroke in the search box, step by step. Rendering and reconciling are calculations in memory. Only the commit changes the page, and only where the new tree differs from the old one. The next state change starts the cycle again.

What makes a component render again

There are three triggers, and only three, to keep in your head:

  • Its own state changed (a set function from useState or useReducer, which State, effects and where data should live covers properly).
  • Its parent rendered. By default, when a component renders, React renders all of its children and their children too, whether their props changed or not.
  • A context it reads changed. A context passes a value to a whole subtree without props, such as the current store or the signed-in user. Every component that reads it renders when the value changes.

Notice what is missing from the list: "its props changed". Props are not a trigger of their own. A child only ever gets new props because its parent rendered, and that render already includes the child. So when Ama types one letter, React calls StockScreen, SearchBar, StockTable, every visible StockRow, AdjustPanel and OrderSummary, even though the order counts did not change. Components above StockScreen do not run at all.

That sounds expensive, but a re-render is not a page change. If a StockRow returns exactly the same elements as last time, reconciliation finds no difference and the commit touches nothing in that row. Calling a small function is fast. It only becomes a problem with heavy components or long lists on slow devices: 2,000 rows on a cheap Android phone at the edge of town, with a render on every keystroke. Accessible, fast pages measures and fixes that case.

When one component is expensive and usually gets the same props, you can wrap it in memo: const OrderSummary = memo(function OrderSummary(props) {…}). Before calling it again, React compares each new prop with the old one using Object.is, and if all are equal it reuses the last result and skips the whole subtree. Two warnings. An object, array or function written inline in the parent (counts={{ now: 18, later: 41 }}) is a new value on every render, so it never compares equal. And a memoized component still renders when its own state or a context it reads changes. React Compiler, stable since version 1.0 in October 2025, can add this kind of memoization for you at build time. Treat memo as a performance tool you reach for after measuring.

Quick check

The search query is state in StockScreen. OrderSummary (not wrapped in memo) receives the same counts object on every render. Ama types "o". Does the OrderSummary function run?

Keys: how React knows which row is which

React keeps a component's state for as long as the same component type is rendered at the same position in the tree. Swap a StockTable for a LoadingTable in that place and the old table and everything under it are destroyed, state included. Keep the same type and the state survives, even if every prop changed.

Lists need more than position. When the low-only filter hides one row, every row after it moves up one place. A tells React which item each element stands for, so it matches rows by key instead of by place. Same type and same key: React keeps that row's state and DOM nodes, even if it moved. A key that disappeared: React removes that row. A new key: React creates a row.

If you give no key, React falls back to the position (the array index) and logs a warning in the console. Writing key={index} yourself only hides the warning; the behaviour is the same. Index keys work for a list that never reorders, filters or inserts. A stock table does all three, so the rule for this screen is simple: key={item.articleId}, the stable id that comes from the data. Item names are a poor key, because two products can share a name and names get corrected. Never make up a key during render with Math.random() or crypto.randomUUID(): every render then has all-new keys, so React destroys and rebuilds every row, which is slow and wipes what the user was typing.

Keys only need to be unique among siblings, and they can also reset state on purpose: <AdjustPanel key={selected.articleId} /> gives each item a fresh panel. State, effects and where data should live uses that trick.

Quick check

The stock API returns rows with articleId, name and updatedAt. Which key should StockRow get?

key = index key = articleId 0 Flour 1 Oil draft 12 2 Sugar A1180 Flour A2040 Oil draft 12 A3300 Sugar low-only hides Flour low-only hides Flour 0 Oil 1 Sugar draft 12 A2040 Oil draft 12 A3300 Sugar 12 jumps to Sugar 12 stays on Oil
Each row keeps a draft count as its own state. With index keys, hiding Flour shifts every row up, React matches "row 1" to "row 1", and Oil's draft lands on Sugar. With articleId keys, React sees that A1180 is gone and the other two rows are unchanged, so the draft stays with Oil.

Build it

Build the static stock screen in React 19 and TypeScript with typed props, then add one piece of state and watch exactly which components render on each keystroke.

  1. Create a React + TypeScript project

    Vite needs Node.js 20.19+ or 22.12+. Follow any prompts the first command shows, then start the dev server and open http://localhost:5173. Delete the demo content in src/App.tsx.

    Terminal
    npm create vite@latest stock-screen -- --template react-ts
    cd stock-screen
    npm install
    npm run dev

    Open src/main.tsx: the template already wraps <App /> in <StrictMode> inside createRoot(...).render(...). That is the first trigger of every React app. The same components work unchanged in a Next.js app, which Loading server data uses.

  2. Type the data and add sample rows

    TypeScript
    // src/types.ts: what the stock API returns for one article (money in integer cents)
    export type StockItem = {
      articleId: string;
      name: string;
      category: string;
      onHand: number;
      reorderLevel: number;
      priceCents: number;
      updatedAt: string; // ISO date-time, e.g. '2026-10-20T06:10:00Z'
    };
    
    export type OrderCounts = { now: number; later: number };
    
    export const isLow = (item: StockItem) => item.onHand <= item.reorderLevel;
    export const formatCents = (cents: number) => `$${(cents / 100).toFixed(2)}`;
  3. Write the table and its rows

    Each component declares its props type. The console.log lines are temporary, so you can see renders in step 6.

    TypeScript
    // src/StockRow.tsx: one row, nothing but props in, elements out
    import { formatCents, isLow, type StockItem } from './types';
    
    type StockRowProps = { item: StockItem };
    
    export function StockRow({ item }: StockRowProps) {
      console.log('render StockRow', item.articleId);
      const low = isLow(item);
      return (
        <tr className={low ? 'low' : undefined}>
          <td>{item.name}</td>
          <td>{item.onHand}</td>
          <td>{item.reorderLevel}</td>
          <td>{formatCents(item.priceCents)}</td>
          <td>{low ? 'Low' : 'OK'}</td>
        </tr>
      );
    }
  4. Add the other parts, using children for the card

    Panel types its children as ReactNode, which accepts anything React can show: elements, text, numbers, or nothing.

    TypeScript
    // src/Panel.tsx: a card that wraps whatever you put between its tags
    import type { ReactNode } from 'react';
    
    type PanelProps = { title: string; children: ReactNode };
    
    export function Panel({ title, children }: PanelProps) {
      return (
        <section className="panel">
          <h2>{title}</h2>
          {children}
        </section>
      );
    }
  5. Assemble the screen

    TypeScript
    // src/StockScreen.tsx: composes the parts and passes each one its props
    import { AdjustPanel } from './AdjustPanel';
    import { OrderSummary } from './OrderSummary';
    import { SearchBar } from './SearchBar';
    import { StockTable } from './StockTable';
    import type { OrderCounts, StockItem } from './types';
    
    const counts: OrderCounts = { now: 18, later: 41 }; // module level: the same object every render
    
    type StockScreenProps = { items: StockItem[] };
    
    export function StockScreen({ items }: StockScreenProps) {
      console.log('render StockScreen');
      const query = '';                  // static for now
      return (
        <main className="stock-screen">
          <h1>Morning stock check</h1>
          <SearchBar query={query} onQueryChange={() => {}} />
          <StockTable items={items} />
          <AdjustPanel item={null} />
          <OrderSummary counts={counts} />
        </main>
      );
    }

    The page shows four rows in name order, with oil, milk and sugar marked Low (milk is exactly at its reorder level). Now type in the search box. Nothing appears, and the console shows no new logs. Nobody changed any state, so nothing triggered a render, and React keeps the input showing its value, which is still an empty string.

  6. Add one piece of state and watch the renders

    useState gets a full lesson next (State, effects and where data should live). Here it is only the trigger.

    Diff
    +import { useState } from 'react';
     import { AdjustPanel } from './AdjustPanel';
    @@
     export function StockScreen({ items }: StockScreenProps) {
    +  const [query, setQuery] = useState('');
       console.log('render StockScreen');
    -  const query = '';                  // static for now
    +  const q = query.trim().toLowerCase();
    +  const visible = items.filter((item) => item.name.toLowerCase().includes(q));
       return (
         <main className="stock-screen">
           <h1>Morning stock check</h1>
    -      <SearchBar query={query} onQueryChange={() => {}} />
    -      <StockTable items={items} />
    +      <SearchBar query={query} onQueryChange={setQuery} />
    +      <StockTable items={visible} />

    Open the browser console, clear it, and type o. Only "Cooking oil 2 L" and "Flour 2 kg" contain an "o". In development you will usually see each line twice, because StrictMode calls each component twice; one copy per render is shown here:

    Output
    render StockScreen
    render SearchBar
    render StockTable
    render StockRow A2040
    render StockRow A1180
    render AdjustPanel
    render OrderSummary

    Read it against the rules. StockScreen owns the state, so it renders. Every child renders because its parent did, including AdjustPanel and OrderSummary, whose props did not change. The milk and sugar rows do not log: their keys are gone, so React removes their DOM rows in the commit. The oil and flour rows run, but their output is identical, so the commit leaves their cells alone.

  7. Let OrderSummary skip renders with memo

    Diff
    +import { memo } from 'react';
     import { Panel } from './Panel';
    @@
    -export function OrderSummary({ counts }: OrderSummaryProps) {
    +export const OrderSummary = memo(function OrderSummary({ counts }: OrderSummaryProps) {
       console.log('render OrderSummary');
    @@
       );
    -}
    +});

    Type another letter: render OrderSummary no longer appears, because counts is the same module-level object every time. Now move counts inside StockScreen as an inline object and type again: the log comes back, because each render creates a new object. Put it back afterwards. Finally, run npm run build, which type-checks the code with tsc before bundling: remove the item prop from one <StockRow /> and the build fails with a missing-property error. Remove the console.log lines when you are done.

Bring it to your team

Ask these in design reviews and pull requests for any React screen.

  • Can you draw the component tree for this screen, and say which component owns each piece of state?
  • Does any component change its props, sort an array it was given, or write to a variable outside itself while rendering?
  • What key does each list use? Is it a stable id from the data, and what happens to the rows when we filter or sort?
  • Is the app wrapped in StrictMode in development, and do we read its double-render warnings instead of switching it off?
  • Where we added memo, did we measure a slow render first, and are the props it compares stable?

Red flags

  • key={index} or key={Math.random()} on a list that can be filtered, sorted or edited.
  • A component defined inside another component's body.
  • Fetching data, starting timers or writing to storage in the body of a component.
  • StrictMode removed "because things ran twice".
  • memo on every component, with inline objects passed to them anyway.

Practice

Match rows by key, the way React does

Write reconcile(prevKeys, nextKeys). It gets the row keys of the last render and of the new render, and returns { kept, added, removed }: kept holds the keys in both lists (in the new order), added the keys only in the new list (in the new order), and removed the keys only in the old list (in the old order). The starter matches rows by position, which is how index keys behave. Make it match by key.

JavaScript exerciseRuns in your browser with tests
function reconcile(prevKeys, nextKeys) {
  // Matches by position, like key={index}. Make it match by key instead.
  const kept = [];
  const added = [];
  const removed = [];
  const length = Math.max(prevKeys.length, nextKeys.length);
  for (let i = 0; i < length; i++) {
    if (prevKeys[i] === nextKeys[i]) {
      kept.push(nextKeys[i]);
    } else {
      if (nextKeys[i] !== undefined) added.push(nextKeys[i]);
      if (prevKeys[i] !== undefined) removed.push(prevKeys[i]);
    }
  }
  return { kept, added, removed };
}
Ctrl + Enter runs
Show a solution
JavaScript
function reconcile(prevKeys, nextKeys) {
  const before = new Set(prevKeys);   // Set lookups are fast even for 2,000 rows
  const after = new Set(nextKeys);
  return {
    kept: nextKeys.filter((key) => before.has(key)),
    added: nextKeys.filter((key) => !before.has(key)),
    removed: prevKeys.filter((key) => !after.has(key)),
  };
}

Matching by position turned a one-row filter into "three rows changed", which is exactly why index keys move state to the wrong item. React's real algorithm also tracks moves and component types, but the idea is the same: identity comes from the key.

Pass the exercise to finish this task.

Predict which components render

Write whoRenders(tree, changed). A tree node is { name, memo, sameProps, children }: memo is true when the component is wrapped in memo, and sameProps is true when its props are equal to last time. The component named changed had a state update. Return the names that render, parent before children (depth first), using React's default rules: the changed component renders; a child renders whenever its parent renders, except a memo child with the same props, which is skipped together with everything under it. The starter believes "a child renders only when its props change".

JavaScript exerciseRuns in your browser with tests
function findNode(node, name) {
  if (node.name === name) return node;
  for (const child of node.children || []) {
    const found = findNode(child, name);
    if (found) return found;
  }
  return null;
}

function whoRenders(tree, changed) {
  const out = [];
  function visit(node) {
    out.push(node.name);
    for (const child of node.children || []) {
      if (!child.sameProps) visit(child);   // "children render only when their props change"
    }
  }
  visit(findNode(tree, changed));
  return out;
}
Ctrl + Enter runs
Show a solution
JavaScript
function findNode(node, name) {
  if (node.name === name) return node;
  for (const child of node.children || []) {
    const found = findNode(child, name);
    if (found) return found;
  }
  return null;
}

function whoRenders(tree, changed) {
  const out = [];
  function visit(node) {
    out.push(node.name);
    for (const child of node.children || []) {
      if (child.memo && child.sameProps) continue;   // memo bails out, subtree included
      visit(child);                                   // otherwise: parent rendered, so child renders
    }
  }
  visit(findNode(tree, changed));
  return out;
}

Equal props matter only for a memo component. Without memo, a parent's render always includes its children. Real React adds one more case this model leaves out: a skipped component still renders if its own state or a context it reads changes.

Pass the exercise to finish this task.

Check yourself

Every time StockTable re-renders, the draft counts Ama typed into rows disappear, even though each row has key={item.articleId}. The code defines function StockRow() {…} inside the body of StockTable. What is going on?

A developer wraps OrderSummary in memo and renders it as <OrderSummary counts={{ now: orders.now, later: orders.later }} /> inside StockScreen. The numbers have not changed. Does OrderSummary still render on every keystroke in the search box?

To silence the missing-key warning, someone writes key={crypto.randomUUID()} on each StockRow. What will Ama notice?

Flashcards

Open my review

Key takeaways

  • Cut a screen into components with one job each; data goes down as typed props, events come up through function props, and children lets you compose.
  • JSX builds plain element objects. Components must be pure while rendering: no mutated props, no side effects.
  • Each update is trigger, render, reconcile, commit. A parent's render renders its children; only the commit changes the page, and only where something differs.
  • Use stable keys from the data, such as articleId. Index keys and random keys move or wipe row state.
  • StrictMode's double call in development is a purity test. Reach for memo only after measuring, with stable props.

Sources

Episode transcriptPlan, compare, swap: how React redraws · about 11 minutes · tap any line to play from there
  1. NadiaImran, seven forty in the morning. Ama is counting cooking oil in her store. She types twelve into the oil row on the new stock screen, then ticks low only to hide the items that are fine.
  2. ImranAnd the twelve is still on the oil row, I hope.
  3. NadiaIt's on the sugar row. She nearly saves twelve bags of sugar when the shelf holds eight. Today is about why that happens, and about how React decides what to run and what to change on the screen.
  4. ImranBefore the bug, give me the picture. I'm a product person. What is React actually doing when the screen changes?
  5. NadiaThink of shelf labels in a supermarket. Every night head office sends a fresh plan for every shelf. The night staff don't rebuild the aisles. They walk the shelves, compare the plan with what's there, and swap only the labels that differ.
  6. ImranSo if one price changed, one label changes. If nothing changed, they leave the shelf alone.
  7. NadiaExactly. And they match products by barcode, never by slot number. If the flour is removed, the note a colleague stuck on the oil stays on the oil. Plan, compare, swap. Hold on to that.
  8. ImranFine. Where do components come in?
  9. NadiaA component is a function that takes data and returns a description of part of the screen. Leila sketched the stock screen: a search box, a table of items, an adjust panel on the side, and a card with today's app orders. We draw a box around each part.
  10. ImranAnd each box becomes a component. How do you decide where to cut?
  11. NadiaThree tests. One job each. Anything that repeats becomes its own component, so one stock row per article. And you name it after what it shows, so a teammate can find it.
  12. NadiaIf you can see your screen, the first diagram shows the tree. Stock screen at the top. Below it the search bar, the stock table with its rows, the adjust panel and the order summary. Next to each one, the props it receives.
  13. ImranProps being the inputs.
  14. NadiaYes. The parent passes them down, and the child must not change them. If the child needs something changed, like the search text, the parent also passes a function, and the child calls it. Data goes down, events come up.
  15. ImranSo the search bar never owns the query. It shows what it's given and reports what Ama typed.
  16. NadiaRight. And one prop is special, called children. Whatever you put between a component's tags arrives there. A small panel with a border and a title can wrap the order card or the adjust form without knowing what's inside.
  17. ImranNow, those tags. They look like HTML. Is that HTML?
  18. NadiaNo. That's JSX, and the browser never sees it. The build tool turns every tag into a function call. And that call doesn't draw anything. It returns a plain JavaScript object with a type, the props and a key.
  19. ImranSo writing a stock row tag doesn't run the stock row function.
  20. NadiaCorrect. It writes a line in the plan. React decides later when to call your function. That's why your function has to be pure while it renders. Same inputs, same output, and it changes nothing that existed before.
  21. ImranGive me an impure one that a real developer would write.
  22. NadiaThe stock table wants rows in name order, so it calls sort on the items it was given. But sort changes the array in place. That array belongs to the parent. Drawing a child has just reordered the parent's data.
  23. ImranSneaky. So you copy the array first and sort the copy.
  24. NadiaYes. And React gives you a tool to catch this. In development, inside strict mode, React calls each component twice and keeps one result. A pure component doesn't care. An impure one shows itself, like a row appearing twice.
  25. ImranI've heard developers complain that everything runs twice and they switch it off.
  26. NadiaThat's the red flag. It's development only. Production runs once. The double call is a free purity test, so read what it shows you instead of silencing it.
  27. ImranOkay. Ama types the letter o in the search box. Walk me through what happens.
  28. NadiaThe second diagram has four boxes from top to bottom. One, trigger. The input's change handler sets the query state in the stock screen, and React queues a render of that component.
  29. NadiaTwo, render. React calls the stock screen again, and then every component inside it, all the way down. Rendering in React means calling your functions. Nothing on the page has changed yet.
  30. ImranThree must be the compare step from the shelf story.
  31. NadiaYes, reconcile. React compares the new tree with the last one. Same type in the same place, and the same key in a list, means it's the same component, so React keeps its state. And four, commit. React changes only the differences in the page, and the browser paints.
  32. ImranSo which components run when Ama types? I'd guess the search bar and the table, because their props changed.
  33. NadiaThat's the common guess, and it's wrong. Every child of the stock screen runs. The adjust panel and the order summary too, even though their props are exactly the same.
  34. ImranWhy? Nothing changed for them.
  35. NadiaBecause there are three triggers. A component's own state changes. Its parent renders. Or a context it reads changes. Changed props aren't on the list. A child only gets new props because its parent rendered, and that render already includes the child.
  36. ImranThat sounds wasteful on Ama's old store PC.
  37. NadiaMostly it isn't. A re-render isn't a page change. If the order card returns the same description, the commit touches nothing. Calling a small function is fast. It hurts with two thousand rows on a cheap phone, and a later lesson on fast pages handles that case.
  38. ImranAnd memo? I see it in code reviews everywhere.
  39. NadiaMemo wraps a component so React compares each prop with the last one and skips it when they're all equal. But an object written inline in the parent is new every render, so it never looks equal. And a memo component still renders when its own state or a context changes.
  40. ImranSo measure first, then reach for memo, with stable props.
  41. NadiaYes. And React Compiler, stable since October twenty twenty-five, can add that memoization for you when you build.
  42. ImranNow back to Ama's twelve on the sugar row.
  43. NadiaThe third diagram shows two columns. On the left, rows keyed by their position: zero, one, two. Flour, oil with Ama's draft of twelve, then sugar. On the right, the same rows keyed by article ID.
  44. NadiaLow only hides the flour. On the left, every row moves up one place. React matches row one to row one. The state that belonged to row one, the twelve, now sits on whatever is in position one. That's the sugar.
  45. ImranAnd on the right, React sees the flour's ID is gone, removes that row, and the oil keeps its twelve.
  46. NadiaExactly. The key is the barcode from our shelf story. Position is the slot number.
  47. ImranSo in Ama's store, the first version used the index as the key. Nadia, that was you, wasn't it?
  48. NadiaIt was. One line in the stock table. The fix was to key each row by its article ID. You added an acceptance check to the ticket: type a count, change the filter, the count stays on its item. And we added a test for it.
  49. ImranWhat about random keys? Someone once silenced the key warning with a random ID.
  50. NadiaWorse. New keys on every render tell React that every row is new. It throws away all of them and builds them again, so it's slow, and a half-typed count vanishes. Keys must be unique and stable, and the article ID from the data is both.
  51. NadiaThe Build it section makes this real. You create a React and TypeScript project with Vite, type the stock item, and write the row, the table, a panel that wraps children, the search bar, the adjust panel and the order card.
  52. ImranWith a console log in each one, so you can see the renders.
  53. NadiaYes. First the screen is static, and typing in the search box does nothing at all, because no state changed. Then you add one piece of state, type the letter o, and read the log. Every child runs, and the two hidden rows disappear.
  54. ImranAnd then memo on the order card makes its log line disappear, until you pass it an inline object.
  55. NadiaRight. The practice section has two small exercises that run in your browser. One matches rows by key, the way React does. The other predicts which components render after a state change.
  56. ImranFor my next review, here are my questions. Which component owns each piece of state? What key does each list use, and what happens to the rows when we filter or sort?
  57. ImranAnd does any component change its props or sort what it was given while it renders?
  58. NadiaGood. Add one: if you see memo, ask whether someone measured a slow render first.
  59. NadiaOne to think about. A developer writes the stock row function inside the body of the stock table. The rows have good keys. But every time the table renders, the drafts disappear. Why? Take a second.
  60. ImranBecause each render creates a new function, so React sees a different component type and rebuilds the rows. Good keys don't help if the type changed.
  61. NadiaExactly. Declare components at the top level of the file.
  62. ImranMy summary. One: cut the screen into components with one job each. Props go down, events come up, and children lets you wrap things.
  63. ImranTwo: an update is trigger, render, reconcile, commit. When a component renders, its children render too. Changed props aren't a trigger on their own.
  64. ImranThree: key list rows by a stable ID from the data. Index keys move state to the wrong row when you filter.
  65. NadiaOne nuance on the second point. A render isn't a page change. React calls the functions, but the page only changes where the new plan differs from the old one.
  66. ImranPlan, compare, swap.
  67. NadiaExactly. Next time: state, effects, and where each piece of data should live on the stock screen.
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 stopState, effects and where data should live
Was this lesson helpful?
Questions and discussion

Loading the discussion…