Props and State

· 5 min read · Updated March 26, 2026 · intermediate
react props state hooks

What are Props?

Props (short for “properties”) are the mechanism for passing data from a parent component to a child component. They flow downward in the component tree — a parent passes props to its children, and children receive them as an argument.

// Parent passes props to Child
function Parent() {
  return <Child name="Alice" age={30} />;
}

// Child receives props as its first argument
function Child(props) {
  return <p>{props.name} is {props.age} years old</p>;
}
// # output: <p>Alice is 30 years old</p>

Props can be any JavaScript value: strings, numbers, arrays, objects, or even functions.

Props are Read-Only

A child component must never modify its props. Props represent the parent’s configuration of the child — mutating them would break the unidirectional data flow that React is built on.

function Child(props) {
  // This will throw an error in strict mode
  props.name = "Bob"; // TypeError: Cannot assign to read only property
  return <p>{props.name}</p>;
}

The rule is simple: props flow down, never up. If a child needs to affect the parent, the parent passes down a function as a prop instead.

Destructuring Props

Using props.name and props.age throughout a component gets verbose. Destructure props directly in the function signature:

// Destructuring in the function argument
function Child({ name, age }) {
  return <p>{name} is {age} years old</p>;
}

// Equivalent verbose form
function Child(props) {
  const { name, age } = props;
  return <p>{name} is {age} years old</p>;
}

Destructuring in the argument signature is the idiomatic modern React pattern. It makes it immediately clear which props a component uses.

Default Prop Values

When a prop might be undefined, provide a fallback value using ES6 default parameters:

function Greeting({ name = "Guest", greeting = "Hello" }) {
  return <p>{greeting}, {name}!</p>;
}

// <Greeting /> renders: "Hello, Guest!"
// <Greeting name="Bob" /> renders: "Hello, Bob!"
// # output: <p>Hello, Guest!</p>

Destructuring defaults only apply when the value is undefined — passing null will NOT fall back to the default. The legacy defaultProps approach exists but is superseded by this pattern.

The children Prop

Props can include children — any JSX nested between the component’s opening and closing tags:

function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage:
<Card title="My Card">
  <p>This paragraph is passed as children.</p>
  <button>Click me</button>
</Card>

children is a special prop. React automatically provides whatever JSX appears between the component tags. It lets you build wrapper components without knowing what content they’ll contain.

What is State?

State is data that belongs to a specific component and can change over time. Unlike props, which come from the parent and are read-only, state is local, mutable, and managed entirely by the component itself.

The key difference: when state changes, React re-renders the component to reflect the new data in the UI.

useState Hook

useState is a React hook that adds state to a functional component. It returns an array with two elements: the current state value and a setter function.

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
// # output: initial render shows Count: 0, button click updates to Count: 1

Syntax breakdown:

  • useState(0) — the initial value. It is only used on the first render.
  • count — the current state value.
  • setCount — the setter function. Calling it with a new value schedules a re-render.

When the new state depends on the previous state, use the functional update form:

setCount(prev => prev + 1); // preferred
setCount(count + 1);        // can cause stale closure bugs

The functional form avoids stale closure issues and is the safe, recommended pattern.

State Updates are Asynchronous

State updates in React are not applied immediately. Calling setCount schedules a re-render, but React may batch multiple updates together.

function Demo() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  function handleClick() {
    setX(5);
    setY(10);
    // x and y still read as 0 here
    // Both updates are batched into a single re-render
  }

  return (
    <div>
      <p>x = {x}, y = {y}</p>
      <button onClick={handleClick}>Update both</button>
    </div>
  );
}
// # output: after click, both x and y update in a single render

React 18 introduced automatic batching, which means all state updates — including those inside async functions, setTimeout, Promises, and native event handlers — are batched into a single render pass. This reduces unnecessary re-renders and improves performance.

Lifting State Up

When two sibling components need to share data, you cannot pass props between siblings directly. Instead, lift state up to the nearest common parent:

function Parent() {
  const [temperature, setTemperature] = useState(20);

  return (
    <div>
      <TemperatureDisplay temp={temperature} />
      <TemperatureInput
        temp={temperature}
        onTempChange={setTemperature}
      />
    </div>
  );
}

function TemperatureDisplay({ temp }) {
  return <p>Current temperature: {temp}°C</p>;
}

function TemperatureInput({ temp, onTempChange }) {
  return (
    <input
      type="number"
      value={temp}
      onChange={e => onTempChange(Number(e.target.value))}
    />
  );
}
// # output: typing in input updates both display and input value simultaneously

The pattern: move shared state to the parent, then pass both the value and the setter down to children that need them via props. This is the fundamental way to synchronize sibling components in React.

Props vs State

AspectPropsState
OwnershipParent passes it downComponent owns it
MutabilityRead-only in the childMutable via setXxx
PurposeConfigure a child componentTrack data that changes over time
Triggers re-renderYes (when parent re-renders)Yes (when value changes)
Can be passed to children?Yes, as regular propsYes, as a prop value
Default values?Yes, via destructuring defaultsYes, via useState(initial)
For sibling communication?No — needs state liftedNo — needs state lifted

Use props when: a parent needs to configure a child, data flows from outside the component, or the child should not own that data.

Use state when: data changes due to user interaction, the component needs to remember something between renders, or changing the data should update the UI.

A useful heuristic: if removing the prop and re-rendering would produce different output, it is state. If the prop just configures how the component renders, it is a prop.

See Also