Programming with AI
ENRU
10.3

Props and state

where the data comes from, and why the counter counts wrong

What it is

Two sources of data in a component, and they mustn't be confused:

  • props — came from outside, from the parent. They can't be changed.
  • state — lives inside the component. It changes, and on every change the component is drawn again.

Types give you more here than anywhere else in the chapter: props are the boundary between components, and TypeScript watches it strictly.

What it looks like in code

const [count, setCount] = useState(0);

useState returns a pair: the current value and a function that changes it. The square brackets unpack a two-element array.

Assigning directly doesn't work. count = 5 redraws nothing: React learns about a change only through setCount.

What the types catch

Here are the compiler's real messages for four typical mistakes:

A required prop is missing:

<CityCard name="Oslo" temp={4} />
measured
error TS2741: Property 'onRemove' is missing in type
'{ name: string; temp: number; }' but required in type 'CityCardProps'.

The wrong type:

<CityCard name="Oslo" temp="4" onRemove={() => {}} />
measured
error TS2322: Type 'string' is not assignable to type 'number'.

The wrong handler signature:

<CityCard name="Oslo" temp={4} onRemove={(id: number) => {}} />
measured
error TS2322: Type '(id: number) => void' is not assignable
to type '(name: string) => void'.

A forgotten emptiness check:

function T({ temp }: { temp: number | null }) { return <p>{temp.toFixed(1)}</p>; }
measured
error TS18047: 'temp' is possibly 'null'.

All four are classic AI mistakes, and none of the four makes it as far as the run. The third especially: an AI often remembers a handler with a different signature and substitutes it silently.

A trap you have to see

Three buttons, the same job — add one three times:

<button onClick={() => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }}>
  three times in a row
</button>

<button onClick={() => { setCount(c => c + 1); setCount(c => c + 1); setCount(c => c + 1); }}>
  three times with a function
</button>

Measured in the browser:

measured
start:                            count: 0 | renders: 1
after one +1:                     count: 1 | renders: 2
after "three times in a row":     count: 2 | renders: 3
after "three times with function": count: 5 | renders: 4

Three calls in a row gave +1, not +3. Three calls with a function gave +3.

The reason: inside the handler count is the value as of the render, and calls to setCount don't change it. Three times in a row it runs setCount(0 + 1). Writing setCount(c => c + 1) asks React to take the current value itself.

Note the render counter too: three calls produced one redraw. React collects the changes and applies them in one go — that is called batching.

Where state should live

The rule: in the nearest common parent of the components that need it.

If two sibling components need the state, it is lifted to the parent and passed back down as props — the value itself and the function that changes it:

<CityCard name={c.name} temp={c.temp} onRemove={handleRemove} />

That is the answer to "why are there so many props": data goes top-down, events bottom-up.

What to tell the AI

Prompt
The city list's state is currently in CityCard. Lift it to
App, pass the value and the handler down. Describe the prop
types with an interface.
Prompt
This handler has several setState calls in a row that depend
on the previous value. Rewrite them with the functional form
setX(prev => ...).
Prompt
Describe this component's props with an interface: what's
required, what isn't, what types the handlers have. Don't
touch the implementation.

What to look for

A prop changed inside the component. props.items.push(...) — props can't be changed, they are the parent's data.

setState in a row depending on the old value. Taken apart above.

Duplicated state. The same thing held both in the parent and in the child — guaranteed to go out of sync.

State where an ordinary variable would do. If the value doesn't affect what is visible, useState isn't needed — it causes extra redraws.

Props with no types, or with any. All the benefit from article 9.5 is lost.

Ten props on one component. A sign the component does too much (article 2.5, on the number of arguments).

Cheat sheet

  • Props come from above and are immutable; state lives inside.

  • Direct assignment redraws nothing — only setX does.

  • New value depends on the old one — the functional form setX(prev => ...).

  • Three setCount(count + 1) in a row give +1, not +3.

  • Several changes give one redraw: React batches.

  • Shared state belongs in the nearest common parent.