Notifications

No notifications

/Phase 3

State & Props

State & Props — The Data Flow of React

React follows a unidirectional data flow: data moves from parent to child via props, and components manage their own local data via state.

Props vs State

AspectPropsState
OwnerParent componentThe component itself
Mutable?Read-onlyUpdated via setter
DirectionTop → DownLocal
Re-render?When parent re-rendersWhen setter is called

useState Hook

const [count, setCount] = useState(0);
// count: current value
// setCount: updater function
// 0: initial value

The updater can accept a new value or a callback that receives the previous state:

setCount(prev => prev + 1); // Functional update (safe for batching)

Lifting State Up

When two sibling components need to share data, lift the state to their closest common parent and pass it down as props.

Prop Drilling Problem

Passing props through many layers of components that don't use them is called prop drilling. Solutions include:

  • Context API (built-in)
  • State management libraries (Zustand, Redux)
  • Component composition (children prop)

Component Communication Patterns

PatternDirectionExample
PropsParent → Child
Callback propsChild → Parent
Lifted stateSibling ↔ SiblingShared parent holds state
ContextAncestor → DescendantsTheme, auth, locale

On this page

Detailed Theory

State vs Props in One Sentence

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.

Your First useState

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:

  • Call them at the top level of a component — never inside loops, conditions, or nested functions.
  • Call them only from React functions (components or custom hooks).

State Is a Snapshot

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 => ...).

Updating Objects & Arrays — Immutably

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] });

Common Patterns

// 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" } });

Passing Props Down

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.

Letting a Child Talk Back: Callback Props

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.

Lifting State Up

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.

Controlled vs Uncontrolled Inputs

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.

Beginner Mistakes to Skip

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.

Intermediate: Derived State (Don't Store What You Can Compute)

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).

Intermediate: State Batching

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.

Intermediate: Resetting State with the key Prop

To 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.

Intermediate: Where Should State Live?

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.

Advanced: When to Use useReducer Instead

If 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.

Advanced: Stale Closures

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).

Advanced: useState's Lazy Initialiser

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));

Advanced: useState vs URL/Storage

Some "state" doesn't belong in React at all:

Where to put itWhy
URL search paramsShareable, back-button works (e.g. filters)
localStoragePersists across reloads (e.g. theme)
Server / DBSource of truth for any user data
React stateEphemeral UI (open/closed, hovered, current input)

Practice Path

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.