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} />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={() => {}} />error TS2322: Type 'string' is not assignable to type 'number'.The wrong handler signature:
<CityCard name="Oslo" temp={4} onRemove={(id: number) => {}} />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>; }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:
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: 4Three 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
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.This handler has several setState calls in a row that depend
on the previous value. Rewrite them with the functional form
setX(prev => ...).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
setXdoes.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.