Module 3: State With Usestate

State vs props: borrowed vs own

Overview

Now that you know a component can have state —a memory of its own that persists and, when it changes, repaints—, the question you have to resolve before any other appears immediately: when you have a piece of data, does it go in props or in state? Choosing wrong is the beginner's most frequent mistake, and it produces slippery bugs: screens that don't update when they should, data that falls out of sync, components impossible to reuse. This lesson gives you the compass so you don't go wrong.

The distinction, in one sentence: props are borrowed data; state is own data. Let's break it down, because each half has consequences.

Props come from the parent. They flow down the tree (you saw it in module 1: data flows downward), and from the child's point of view they're read-only: the child uses them, but never changes them. If ProductCard receives name via props, it shows that name, but it doesn't belong to it —it's the catalog's that App owns—. Props are like a piece of data entrusted to you to use in your task; you read it, you don't cross it out.

State is own to the component. It doesn't come from outside: the component declares it with useState, it lives in its notebook, it persists between renders, and —this is the key operational difference— the component changes it itself, with its setter. If the SearchBar has a query state, that text is its own: no one passes it to it, it stores it and updates it when the user types.

Connection with the module. This lesson is the compass of the whole module. State (everything coming: snapshot, re-render, updater, immutability) only makes sense once you know what should be state. And it leans directly on what you learned before: props and their unidirectional flow are from module 1; here we contrast them with state so you see that they don't compete, they complement each other. A typical component receives some props (what it's given) and has some state (what's its own and changes), and produces its UI from the two. Knowing which is which is what lets you build without getting tangled.

An analogy: the printed form and your notes

Imagine you arrive at an office to do some paperwork and they hand you a printed form. At the top, already written in ink, come your name, your file number, the appointment date: data the office put there. You didn't write it nor can you change it —it's part of the form as it was given to you—. If your name is misprinted, you don't cross it out and rewrite it on your own; you tell the office, which is the owner of that data. Those printed data are the props: they reach you from above (the office, the parent), you use them, but they're not yours to change.

Now, that same form has blank fields that you fill: your signature, the option you choose, a note you add. That you write yourself, it's yours, and you can change it while filling out —cross out your own note and put another—. Those fields you fill and control are the state: own data, that you change, that are part of your interaction with the form.

The complete paperwork —what the office finally processes— is the combination of the two things: what was printed (props) plus what you filled in (state). That's how a component is: it produces its UI by combining what it was given (props) with what's its own and changes (state). And the golden rule of the analogy is the one you must not forget: don't rewrite the printed part (don't mutate the props), and what you fill is only yours (your state isn't seen or changed by another; it's private to your form). Keep the image: props = what someone else printed; state = what you fill in.

Worked example: the SearchBar with a fixed prop and a state that changes

We're going to see the distinction executed. We'll build a SearchBar that receives a prop (placeholder, the gray "Search products..." text) and has a state (query, what the user types). We'll run several renders and observe the central asymmetry: the prop doesn't change between renders (it comes from the parent, which always passes the same one), while the state does change (it's own, and we update it with its setter). And at the end we'll verify, with a real error, that the prop is read-only.

Here's how it's written in real React. Notice the two data sources: props.placeholder (borrowed) and query (own, from the useState):

import { useState } from 'react';

function SearchBar(props) {
  const [query, setQuery] = useState(''); // STATE: own, starts empty
  return (
    <input
      className="search-bar"
      placeholder={props.placeholder}      // PROP: comes from the parent
      value={query}                        // STATE: the component's own
    />
  );
}

function App() {
  return <SearchBar placeholder="Search products..." />; // the parent passes the prop
}

Read the casting of roles: App (the parent) passes SearchBar the prop placeholder. Inside SearchBar, props.placeholder is read-only —the gray text the parent decided—, while query is own state that the bar stores and changes. (The onChange that would fire setQuery when the user types is module 4; here we call the setter "by hand" to see the mechanic.)

Now the executable version in Node. We model the state with a cell that persists (as in lesson 1) and make the parent pass a frozen prop, so it's visible that the child can't change it:

'use strict';

// ── The state lives in a cell that PERSISTS between renders ──
// (In React, this cell is managed by useState; here we model it by hand
//  to see the difference with props.)
let queryCell = ''; // SearchBar's initial state: empty string

function setQuery(next) {
  // the SETTER: the only legitimate way to change the own state
  queryCell = next;
}

// The component receives PROPS (from outside, read-only) and reads its STATE (own).
function SearchBar(props) {
  const query = queryCell; // READS the state from its cell
  return {
    placeholder: props.placeholder, // PROP: comes from the parent
    value: query, // STATE: the component's own
  };
}

// The parent passes the props. They're frozen to make it visible that the child
// can't change them.
const propsFromParent = Object.freeze({ placeholder: 'Search products...' });

console.log("=== PROPS don't change; STATE does, between renders ===");
console.log('render 1:', JSON.stringify(SearchBar(propsFromParent)));

// The user "types": the component changes ITS state with the setter.
setQuery('mo');
console.log('render 2:', JSON.stringify(SearchBar(propsFromParent)));

setQuery('mouse');
console.log('render 3:', JSON.stringify(SearchBar(propsFromParent)));

// A child can NOT change its props: they belong to the parent.
console.log('\n=== a child tries to change a PROP ===');
try {
  propsFromParent.placeholder = 'something else'; // mutate frozen prop
} catch (e) {
  console.log(`  X ${e.constructor.name}: ${e.message}`);
}
console.log('the prop is unchanged:', JSON.stringify(propsFromParent.placeholder));

What to expect. When you run the file, the output is exactly this:

=== PROPS don't change; STATE does, between renders ===
render 1: {"placeholder":"Search products...","value":""}
render 2: {"placeholder":"Search products...","value":"mo"}
render 3: {"placeholder":"Search products...","value":"mouse"}

=== a child tries to change a PROP ===
  X TypeError: Cannot assign to read only property 'placeholder' of object '#<Object>'
the prop is unchanged: "Search products..."

This output is the whole distinction, measured. Read it in the two columns of each render.

Look at the placeholder column across the three renders: "Search products...", "Search products...", "Search products...". It never changes. And it doesn't change for a concrete reason: it's a prop, and the parent always passes the same one. For it to change, the parent would have to change it (calling SearchBar with another placeholder), not the bar. From inside SearchBar, placeholder is a fixed, borrowed piece of data that's only read.

Now look at the value column (the query state): "", "mo", "mouse". It changes on each render, and it changes because the bar itself changed it with setQuery. It's its own data, stored in the cell that persists, updated by its setter. No one from outside touched it; the bar manages it on its own. That's the underlying asymmetry: the prop is constant because it doesn't belong to the component; the state varies because it does.

And the last part nails down why props aren't changed from the child. SearchBar (or any child) that tried props.placeholder = 'something else' clashes with a TypeError: Cannot assign to read only property 'placeholder', because the prop is frozen (Object.freeze) —just as, in React, props are read-only by discipline—. The prop is still worth "Search products..." after the attempt: it couldn't be changed. This is the "props are borrowed" half turned into a visible error: the data is the parent's, and the child doesn't rewrite it. (You saw it already in module 1; here we bring it back to contrast it with state, which is changed —but with its setter, not with an assignment—.)

The table that resolves almost all doubts

When you don't know whether a piece of data goes in props or in state, this comparison resolves most cases:

                   PROPS                        STATE
─────────────────  ───────────────────────────  ───────────────────────────
Where from         from the parent (outside)     from the component itself
Who changes it     the parent (never the child)  the component, with its setter
From the child     read-only                     own, editable (via setter)
Persists?          while the parent passes it    yes, between renders (in the notebook)
What it's for      configure/feed the child      remember what changes over time
Example (Mercado)  product.name, placeholder     query, expanded, cartCount

Notice the pair of central rows: who changes it and from the child. There is the heart of the distinction. If the data is changed by the component itself in response to something (the user types, clicks), it's state. If it's changed (or fixed) by whoever is above and the component only receives it, it's props.

They don't compete: a single component has both

A framing mistake is thinking a component is "props-based" or "state-based". Almost all have both. The SearchBar of the example receives placeholder via props (configuration the parent gives it) and has query in state (what the user types, which is its own). A ProductCard receives the product via props (name, priceCents, from App's catalog) and can have expanded in state (whether the user expanded that card). The UI is described by combining both:

flowchart TD
    Parent["App (the parent)"] -->|"props: placeholder<br/>(borrowed, read-only)"| SB["SearchBar"]
    State["useState('') -> query<br/>(own, changes with setQuery)"] --> SB
    SB --> UI["UI = f(props, state)<br/>&lt;input placeholder=... value=query&gt;"]

Props enter from above (from the parent); state is born inside; and the UI is a function of the two. There's no competition: there's a casting of roles.

Common mistakes

Copying a prop into state "to be able to change it". What happens: a component receives a piece of data via props and, since it wants to modify it, copies it to state at the start: const [name, setName] = useState(props.name). Why it happens: you run into props being read-only and "solve" it by duplicating them into something editable. How to spot it: you have a state initialized with a prop, and you expect it to update when the prop changes —but it doesn't, because the initial value of useState is only used the first time (lesson 3)—. The result is a state that falls out of sync with the prop: the parent changes the data, but your copy in state stays with the old value. How to fix it: ask yourself why you want to "change" the prop. If it's to show a transformed version, compute a derived value without state (const displayName = props.name.toUpperCase()). If it really is a piece of data the user controls and that differs from the prop, then that data is legitimate state from the start —but don't call it "a copy of the prop"; it's something else—. The rule: don't duplicate in state something that's already a prop.

Putting in state something that can be derived. What happens: a piece of data that's actually computed from another state or from props —the formatted price, the cart total, the filtered list— is stored in state. Why it happens: it seems practical to "have it stored". How to spot it: you have two pieces of state and one is always a function of the other; to keep them consistent you have to remember to update both together, and sooner or later you forget and they fall out of sync. How to fix it: store in state only the source that changes by itself, and compute the rest in the render (const total = items.reduce(...)). One piece of state per thing that changes; the rest is derived. We formalize the rule in lesson 7 and module 5 develops it; for now, suspect any state that's "the result of" another piece of data.

Treating as fixed a prop that's actually state (or vice versa). What happens: someone puts in props something the component itself must change (and then it can't change it), or puts in state something that actually comes from the parent (and then it falls out of sync). Why it happens: the question "who changes this data?" wasn't asked. How to spot it: you try to update a prop from the child (impossible, it's read-only), or you have a state that "should" follow a piece of the parent's data but doesn't. How to fix it: for each piece of data, answer who changes it? If the component changes it in response to an interaction, it's state. If whoever is above fixes it or changes it, it's props. And if a piece of data is own to a component but another one also needs it, don't duplicate it: lift the state to the common ancestor (module 7) and flow it down via props.

Exercises

Exercise 1 — Classify each piece of data. For a Mercado ProductCard that shows a product and lets you expand it to see details, classify each piece of data as props or state and justify: (a) name; (b) priceCents; (c) expanded (whether the card shows the long details); (d) onAddToCart (a function the parent passes it to notify when it's added to the cart).

See solution
  • (a) name: props. It comes from the catalog, which App owns; the ProductCard only shows it, doesn't change it. Borrowed data.
  • (b) priceCents: props. Same as the name: part of the product, borrowed from above, read-only.
  • (c) expanded: state. It's a thing that changes over time by the user's action (they click to expand/collapse), it's own to that card, and it has to be remembered. It lives as const [expanded, setExpanded] = useState(false).
  • (d) onAddToCart: props. Even though it's a function and not a piece of data "to show", it still comes from the parent (App, which owns the cart). The ProductCard receives it and calls it, but doesn't define it or change it. The functions the parent flows down are props like any other (we study them as callbacks in module 4).

The pattern: what the ProductCard receives from above (product data, functions to notify) is props; what's its own and changes on its own (whether it's expanded) is state.

Exercise 2 — The copy bug. This component tries to show the product name in uppercase by copying the prop to state. Explain why it's going to fall out of sync and rewrite it correctly:

function ProductCard(props) {
  const [name, setName] = useState(props.name.toUpperCase());
  return <h3>{name}</h3>;
}
See solution

Why it falls out of sync: the initial value of useState (here props.name.toUpperCase()) is only used on the first render; in the following renders, useState ignores that argument and returns what's already in the notebook (this is lesson 3). So if the parent changes props.name —because the catalog was updated, or because this same card is reused for another product—, the name state stays with the old uppercase value. The card shows a name that no longer corresponds to the product. The state got "stuck" in the photo of the first render.

Why it's wrong at the root: the uppercase name isn't state. It's not a piece of data the user changes nor that the component should remember; it's simply a transformation of a prop, which can be recomputed on each render.

Rewritten, as a derived value without state:

function ProductCard(props) {
  const displayName = props.name.toUpperCase(); // recomputed each render
  return <h3>{displayName}</h3>;
}

Now displayName is computed from props.name on each render. If the parent changes the name, the card recomputes it and it's always in sync. There's no state to maintain nor that can fall behind. The rule: don't copy a prop into state; if you need a transformed version, derive it in the render.

Exercise 3 — Why don't they compete? A colleague asks: "if state is 'better' because the component controls it, why don't I do everything with state and forget about props?". Explain to them why props and state fulfill different roles and why an app needs both, with a storefront example.

See solution

Because they solve different problems, and confusing them breaks things. State is for what a component owns and changes on its own; props are for passing data from one component to another, top to bottom. Without props, components couldn't communicate: each would be an island unable to receive information from its parent.

Storefront example: the product catalog is owned by App (it's its state, or it reaches it from a fetch). How does each product reach its ProductCard? Via props: App flows the list down to ProductList, which flows each product down to a ProductCard. If you tried to have each ProductCard have "its own state" with the product, where would it get it from? The data lives above; it has to flow down via props. Props are the distribution mechanism.

And the other way around: the text the user types in the SearchBar is the bar's; it makes no sense for the parent to pass it, because it's born from the interaction inside the bar. That's state.

In a real app, data usually is born as state in some component (whoever owns it) and travels as props to those that need it further down. State to own and change; props to distribute downward. They don't compete: they're the two halves of how data flows in React. (When a piece of data is a component's state but several need it, it's lifted to the common ancestor and flows down via props —module 7—; there you'll see the two halves working together.)

Summary and next step

In this lesson you sharpened the module's compass: state vs props. Props are borrowed data —they come from the parent, flow down the tree, and from the child they're read-only—; state is own data —it lives in the component, persists between renders, and the component itself changes it with its setter—. You measured it by executing a SearchBar: its placeholder prop was constant across the three renders (the parent fixes it), while its query state changed from "" to "mo" to "mouse" (it changes it with setQuery); and you saw the TypeError when trying to rewrite the frozen prop. You take away the question that resolves almost everything: who changes this data? If the component, it's state; if whoever is above, it's props. And the warning not to duplicate in state what's a prop (it falls out of sync) nor store what can be derived.

Before moving on you should be able to: say where each one comes from (props from the parent, state from the component itself) and who changes it; classify a piece of storefront data as props or state, justifying it; detect the bug of copying a prop into state; and understand that a typical component has both and they don't compete.

Lesson 3 goes down to the detail of the tool with which you declare state: the useState hook. You're going to see, executed, the real API —const [query, setQuery] = useState('')—: what it returns exactly (a pair: the value and the setter), why it's destructured with brackets, what the initial value is and why it's only used on the first render, and the rules you have to respect for hooks to work. You'll go from "I know what state is" to "I know how to write it".

Resources