jsguides

Understanding React Props and State Management

Introduction

Understanding React starts with props and state — the two ways components handle data. Props flow down from parent to child and configure how a component behaves. State lives inside the component and tracks information that changes over time. Knowing which one to reach for, and how they work together, is the foundation of every React UI.

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, and it keeps the function body clean of repeated props. prefixes. When a component takes more than three or four props, destructuring goes from nice-to-have to essential for readability.

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, meaning 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. It guarantees that each update is computed from the most recent state value, regardless of whether React has flushed the previous update yet.

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.

Deriving state from props

Sometimes a component receives a prop that helps it derive local state, such as a starting value or a selected item. Treat that prop as an initial hint rather than a second source of truth. Once the component owns the state, the prop should not keep fighting it on every render. That separation keeps the UI predictable and avoids subtle reset bugs.

Update state with care

When state changes depend on the previous value, use the updater form of setState. It keeps the update logic tied to the latest value rather than whatever happened to be in scope when the handler was created. That matters most when several updates can land close together. Small state changes are easy to write; safe state changes are the ones that stay correct under pressure.

Pick the right owner

If data is shared, move it to the nearest parent that can coordinate both readers and writers. If data only matters to one component, keep it local. That choice keeps props focused on configuration and state focused on behavior. A clean ownership line also makes it easier to decide where to add tests when the UI changes later.

Local state should feel local

State belongs where the interaction happens. If a checkbox only affects one component, keep the value there instead of moving it up the tree. That keeps the parent from carrying details it does not need and makes the component easier to reuse in another screen.

Shared data needs an owner

When several components care about the same value, choose one owner and pass the current value and update function down from there. That avoids drifting copies of the same data and keeps the update path obvious. A clear owner is also the easiest place to add logging or debugging later.

Avoid duplicate sources of truth

If two places can both edit the same value, bugs show up quickly. Keep one authoritative state value and derive everything else from it. That way the UI does not have to guess which copy is current, and the user gets a single, predictable response to each change.

Props are for coordination

Use props to tell a child what it should render or how it should behave. That keeps the parent responsible for the broader flow and gives the child a simpler job. The cleaner that split stays, the easier it is to reuse the component in a different part of the app.

Next steps

Props and state form the foundation of every React component. To see how they interact in a full application, check out React Components and JSX for hands-on component patterns. For the async side of state management, the Building Custom Hooks tutorial covers how to extract and reuse stateful logic across components.

See Also