State and Props
7 pages · ~5 min✓ Reviewed
Builds on Components and JSX. Next up: Hooks.
Props, State and One-Way Flow
1. Props vs State
| Props | State | |
|---|---|---|
| Owner | Parent | The component |
| Mutable? | Read-only | Via setter only |
| Change means | New input | Re-render |
2. One-Way Flow
- Parent stateuseState
- Props downread-only
- Childrenders
- Callback uponAdd()
jsx
const [n, setN] = useState(0); <Child n={n} onAdd={() => setN(c => c + 1)} />
data down as n, event up as onAdd
- UI = f(props, state)
- A re-render re-runs the function with new values
3. Derive, Don't Store
- Derivable from props or other state? Compute it in render
- Stored copies drift out of sync
Never mutate props
The child asks the parent through a callback prop
useState and Immutable Updates
1. The Hook and Snapshots
const [value, setValue] = useState(initial)→ current value + setterinitialis used only on the first render- Setter call schedules a re-render
- State is a snapshot: the variable keeps its old value in this render
2. Updater Form
- New value depends on the previous one → pass
c => c + 1 - Avoids stale values in batched updates
javascript
setCount(count + 1); setCount(count + 1); // +1 total setCount(c => c + 1); setCount(c => c + 1); // +2 total
3. Immutable Updates
- React compares with Object.is
- A mutated object keeps its reference → no re-render
| Goal | Pattern |
|---|---|
| Object | {...obj, key: v} |
| Add | [...arr, item] |
| Remove | arr.filter(x => x.id !== id) |
| Edit | arr.map(x => x.id === id ? {...x, done: true} : x) |
Replace, never mutate
Build a new object or array, then pass it to the setter.
Lifting State Up
1. Lift to the Common Parent
- Siblings need the same data: move the state to their closest common parent
- Parent passes the value as a prop and a setter as a callback prop
- Child calls the callback; the parent stays the single source of truth
- Child eventcalls callback
- Parent setStateowns state
- Re-rendernew props down
2. Value Down, Callback Up
jsx
function Parent() { const [temp, setTemp] = useState(''); return <> <Input value={temp} onChange={setTemp} /> <Display value={temp} /> </>; }
both siblings read one state
3. Mistakes and Limits
useState(props.value)in a child: the copy ignores later prop changes- Use the prop directly; keep no second copy
| Situation | Reach for |
|---|---|
| Props pass through many layers | Context or composition |
| Only one component uses it | Local useState |
Remember
One owner for the state. Props down, events up.
Controlled vs Uncontrolled Inputs
1. Who Holds the Value
- Controlled:
value+onChangebound to state - React is the source of truth for the field
jsx
<input value={v}
onChange={e => setV(e.target.value)} />- Uncontrolled: the DOM holds the value
- Read it with
ref.current.value, or on submit viaFormData - Set the initial value with
defaultValue
2. Which to Use
| Controlled | Uncontrolled | |
|---|---|---|
| Value lives in | React state | The DOM |
| Suits | Live validation, formatting, disabling submit | Simple forms, file inputs |
| Initial value | useState('') | defaultValue |
3. Common Mistakes
valuewithoutonChangemakes the field read-only and logs a warningundefinedto a defined value flips uncontrolled to controlled
Initialise to ''
Start state as '', never undefined, so the input never switches mode.
Context Basics
1. Provider and Consumer
createContext(default)makes the contextuseContext(Ctx)gives the nearest Provider value above- The default applies only when no Provider is above
jsx
const ThemeCtx = createContext("light"); <ThemeCtx.Provider value={theme}> <App /> </ThemeCtx.Provider> const theme = useContext(ThemeCtx);
no props passed through the layers between
2. State and Re-renders
- Context is not state: keep
useStateoruseReducerin the Provider - Put the value and setter in the context
- Consumers all re-render when the value changes by reference
- Memoise object values with
useMemo, or split contexts
jsx
const [theme, setTheme] = useState("light"); const value = useMemo( () => ({ theme, setTheme }), [theme]); return <ThemeCtx.Provider value={value}>
stable object until theme changes
| Good fit | Poor fit |
|---|---|
| Theme | Form input |
| Current user | Mouse position |
| Locale | Rapidly changing data |
Mistake
Inline value={{ a, b }} re-renders every consumer.
State and Data Flow Cheat Sheet
1. Flow and Setters
- Props go down, events go up as callback props
- Props are read-only; state is owned, and a change triggers a re-render
- State is a snapshot per render: the variable keeps its old value after
setX - Depends on previous state? Use the updater:
setN(n => n + 1) - Replace, never mutate: same reference means no re-render
2. Immutable Cheats
| Do | Pattern |
|---|---|
| Update object | {...obj, key: v} |
| Add | [...arr, x] |
| Remove | arr.filter(x => x.id !== id) |
| Edit | arr.map(x => x.id === id ? {...x, done: true} : x) |
3. Where State Lives
- One componentlocal
useState - Siblingslift to common parent
- Many distant consumerscontext
4. Inputs
| Controlled | Uncontrolled | |
|---|---|---|
| Source of truth | React state | The DOM |
| Wiring | value + onChange | ref / defaultValue |
Never switch modes
Initialise state to '', not undefined. Going from undefined to a value flips the input from uncontrolled to controlled.
Check yourself
Count is 0. One click runs setN(n + 1) three times. What is n after the re-render?
- 1, not 3
- State is a snapshot: all three calls set 0 + 1
setN(c => c + 1)three times gives 3
todos.push(t); setTodos(todos) never updates the list. Why?
- Same array reference, so
Object.issees no change - Fix:
setTodos([...todos, t])
A child does useState(props.value). The parent later passes a new value. What does the child show?
- The old value
- The initial value is used only on the first render, so the copy goes stale
- Use the prop directly, or lift the state