Layer 4 · React

Stop updating the page. Describe it.

Remember the todo list where every change to the data needed a hand-written change to the DOM? React's idea is to delete that second half. You write a function that says: given this data, the page looks like this. When the data changes, React calls your function again and works out the difference itself.

Move the slider. You never wrote an update. The output is just the function, re-run.

UI = f(state)

a component is a function of its state
3
function Cart({ count }) {
  return (
    <div>
      <h2>{count} {count === 1 ? "item" : "items"}</h2>
      {count === 0 && <p>Your cart is empty.</p>}
      {count > 9 && <p className="warn">Free shipping!</p>}
    </div>
  );
}
Two words

A component is a function. Props are its arguments.

That's it. <Greeting name="Ada" /> is a call: Greeting({ name: "Ada" }). The parent decides the arguments. The child can read them but not change them. Data flows down.

Edit the parent's input. The child re-renders with the new prop. It has no idea where the value came from and doesn't need to.

parent → child

Parent

<Greeting name={name} />

Child: Greeting({ name })

Hello, Ada.
renders: 1×
Place your bet

Why can't a plain variable hold the count?

A component is a function, and React calls it again on every render. Locals inside a function are born and die with each call.

So: what happens when you click a counter written with let count = 0; count++ instead of useState?

Never changes. The variable increments, but nothing tells React to re-render. And if something did, the function would run again and let count = 0 would reset it. useState solves both: it stores the value outside the function, and setting it schedules a re-render.

bet, then try both
try both

let count

let count = 0;
<button onClick={() => count++}>
  {count}
</button>
real variable: 0

useState

const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>
  {count}
</button>
renders: 1
Place your bet

When List sets state, which components re-render?

Setting state re-runs the component that owns it. But what about its parent? Its siblings? Its children?

List and everything under it. Re-rendering flows down, never up or sideways. Header is untouched. Every Item re-runs even if its props didn't change (that's what memo is for, later). This one rule explains most "why is this slow" and "why didn't this update" questions.

bet, then click any node to set state there
click a component to set state in it
App
Header
Listowns: items
Item
Item
Item

Highlighted = re-rendered.

Under the hood

Re-render doesn't mean redraw.

Your function returns a description: a tree of plain objects (the "virtual DOM"). React keeps the previous description, compares the two, and touches the real DOM only where they differ.

So re-rendering a component is cheap; it's building a description. Touching the DOM is the expensive part, and React does the minimum of it.

old description vs new, then the patch

previous render

this render

DOM operations sent: 0

The HTML in your JavaScript

JSX is a function call in a costume.

The tags in your components aren't HTML. They're a shorthand that the bundler rewrites into calls before the browser ever sees them. That's why you can put a JavaScript expression inside { }: you were always in JavaScript.

It also explains the quirks: className instead of class (because class is a reserved word), and why a component must start with a capital letter (lowercase means a built-in tag).

same thing, before and after the bundler
<Button size="lg" onClick={save}>
  Save {count} items
</Button>
The escape hatch

"After you've drawn it, also do this."

Rendering should only describe. But real apps need to fetch data, set a document title, subscribe to a socket, start a timer. Those are effects: things that happen after the render, outside React's description of the page.

useEffect(fn, deps): run fn after render, and again only when something in deps changed. The rules around this are where most React confusion lives, and they turn out to have a simple mechanical reason.

the shape of it
function Profile({ id }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${id}`)
      .then(r => r.json())
      .then(setUser);
  }, [id]);   // re-run only when id changes

  if (!user) return <p>Loading…</p>;
  return <h1>{user.name}</h1>;
}
But

React draws. It has no idea what a URL is.

Nor what a server is, nor what to send a Google crawler, nor how to load data before the first paint. Plain React ships an empty <div> and a script, and the script does everything. That's fine for a dashboard and terrible for a blog.