Props and State
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
| Aspect | Props | State |
|---|---|---|
| Ownership | Parent passes it down | Component owns it |
| Mutability | Read-only in the child | Mutable via setXxx |
| Purpose | Configure a child component | Track data that changes over time |
| Triggers re-render | Yes (when parent re-renders) | Yes (when value changes) |
| Can be passed to children? | Yes, as regular props | Yes, as a prop value |
| Default values? | Yes, via destructuring defaults | Yes, via useState(initial) |
| For sibling communication? | No — needs state lifted | No — 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
- React Intro and Setup — install React and build your first component
- React Components and JSX — write your first React component from scratch