Last 30 Days
No notifications
React follows a unidirectional data flow: data moves from parent to child via props, and components manage their own local data via state.
| Aspect | Props | State |
| Owner | Parent component | The component itself |
| Mutable? | Read-only | Updated via setter |
| Direction | Top → Down | Local |
| Re-render? | When parent re-renders | When setter is called |
const [count, setCount] = useState(0);
// count: current value
// setCount: updater function
// 0: initial valueThe updater can accept a new value or a callback that receives the previous state:
setCount(prev => prev + 1); // Functional update (safe for batching)When two sibling components need to share data, lift the state to their closest common parent and pass it down as props.
Passing props through many layers of components that don't use them is called prop drilling. Solutions include:
| Pattern | Direction | Example |
| Props | Parent → Child | |
| Callback props | Child → Parent | |
| Lifted state | Sibling ↔ Sibling | Shared parent holds state |
| Context | Ancestor → Descendants | Theme, auth, locale |
Props are data a component receives from above. State is data a component owns and can change. Props are inputs (read-only); state is memory (mutable through a setter).
If two components need the same value, the closest shared parent owns it as state and passes it down as props.
useState gives a component a piece of memory that survives between renders.
import { useState } from "react";function Counter() {
const [count, setCount] = useState(0); // initial value: 0
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
What's happening:
1. useState(0) returns a pair: the current value and a setter.
2. Calling setCount(...) schedules a re-render with the new value.
3. On the next render, count will be the new value.
Two iron rules of hooks:
This trips up everyone once. Consider:
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// count was 5 → ends as 6, NOT 8
}Why? Each call uses the same count from this render's snapshot. To "stack" updates, use the function form:
function handleClick() {
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
// 5 → 8, because each callback gets the latest pending value
}Rule: if the new state depends on the previous state, use setX(prev => ...).
React compares state by reference. Mutating an object in place looks fine but won't re-render:
// ❌ Mutation — same reference, no re-render
state.items.push(newItem);
setState(state);// ✅ New reference — re-renders correctly
setState({ ...state, items: [...state.items, newItem] });
// Add to array
setItems([...items, newItem]);// Remove by id
setItems(items.filter((it) => it.id !== id));
// Update one item
setItems(items.map((it) => it.id === id ? { ...it, done: true } : it));
// Update one field of an object
setUser({ ...user, name: "New" });
// Update a nested field
setUser({ ...user, address: { ...user.address, city: "Pune" } });
function Parent() {
const [name, setName] = useState("Asha");
return <Child name={name} />;
}function Child({ name }) {
return <p>Hello, {name}</p>;
}
Props always flow down — parents to children. There's no way for a child to write to a prop directly.
To let a child change parent state, the parent passes a function down:
function Parent() {
const [count, setCount] = useState(0);
return <IncrementButton onIncrement={() => setCount(c => c + 1)} />;
}function IncrementButton({ onIncrement }) {
return <button onClick={onIncrement}>+1</button>;
}
The child doesn't know what onIncrement does — it just calls it. Clean separation.
When two siblings need to share data, lift the state to the closest common parent:
function Parent() {
const [query, setQuery] = useState(""); return (
<>
<SearchBox query={query} onChange={setQuery} />
<Results query={query} />
</>
);
}
The parent now owns query. SearchBox updates it; Results reads it.
A controlled input ties its value to React state:
const [email, setEmail] = useState("");<input value={email} onChange={(e) => setEmail(e.target.value)} />
You always know the current value. This is the default choice in React.
An uncontrolled input lets the DOM hold the value; you read it via a ref. Useful for huge forms or when integrating with non-React code.
1. Mutating state instead of replacing it — no re-render happens.
2. Calling the setter inside the render body — infinite loop.
3. Stacking setters that depend on previous state without the function form.
4. Storing derived values in state — see "Derived State" below.
5. Reading state right after setX(...) — it won't have updated yet; the new value lands on the next render.
If a value can be calculated from existing state or props, don't put it in state:
// ❌ Two sources of truth that can drift
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);// ✅ One source of truth
const [items, setItems] = useState([]);
const count = items.length; // recalculated on each render
If the calculation is expensive, wrap it in useMemo (covered in Hooks).
React groups multiple state updates inside one event handler into a single re-render. In React 18+, this also applies inside Promises, timers, and async/await:
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
setName("Asha");
// → ONE render, not three
}This is why state changes feel "atomic" within a handler.
key PropTo wipe a child's internal state, change its key:
<Profile key={userId} userId={userId} />When userId changes, React unmounts the old Profile and mounts a fresh one — its useState starts from scratch.
Heuristic:
1. Find every component that needs to read it. 2. Find their closest common ancestor. 3. Put the state there. Pass down via props. 4. If you find yourself drilling props through 5 layers, consider Context (covered in Hooks).
Keep state as close to where it's used as possible — global state forces unrelated components to re-render.
useReducer InsteadIf your component manages a state object with several related fields, or transitions follow rules ("can only go from idle → loading → success"), useReducer is cleaner than several useStates:
function reducer(state, action) {
switch (action.type) {
case "load": return { status: "loading" };
case "success": return { status: "ready", data: action.data };
case "error": return { status: "error", error: action.error };
default: return state;
}
}const [state, dispatch] = useReducer(reducer, { status: "idle" });
It also makes state transitions testable — reducer is a pure function.
Inside a handler defined in render, the values you "see" are frozen from that render. Combine with setTimeout and you get surprises:
function App() {
const [count, setCount] = useState(0);
function delayedShow() {
setTimeout(() => alert(count), 3000); // shows the count at click time, NOT now
}
return <button onClick={delayedShow}>Show in 3s</button>;
}If you need the *latest* value inside an async callback, store it in a ref (covered in Hooks).
Pass a function (not a value) when the initial state is expensive to compute:
// ❌ runs on every render (even though only first counts)
const [items, setItems] = useState(parseHugeJSON(input));// ✅ runs only on first render
const [items, setItems] = useState(() => parseHugeJSON(input));
useState vs URL/StorageSome "state" doesn't belong in React at all:
| Where to put it | Why |
| URL search params | Shareable, back-button works (e.g. filters) |
localStorage | Persists across reloads (e.g. theme) |
| Server / DB | Source of truth for any user data |
| React state | Ephemeral UI (open/closed, hovered, current input) |
1. Build a counter with +1, -1, and Reset using the function form of the setter.
2. Build a todo list with add / remove / toggle — all immutable updates.
3. Lift the search query out of a SearchBox so a sibling Results can read it.
4. Convert a useState-spaghetti component (4+ booleans) into a single useReducer.