React Props and State for Beginners
A practical beginner's guide to React props and state — how data flows down through props, how state holds data that changes over time, and the rules that keep your UI predictable.
What you'll learn
- ✓What props are and how data flows down through your component tree
- ✓How to destructure props and set sensible defaults
- ✓What state is, and how it differs from props
- ✓How useState works at a practical level
- ✓How a child component can talk back to its parent
- ✓The rules that keep state changes predictable
Prerequisites
- •A working understanding of components and JSX — see Components and JSX
- •Comfort with destructuring and arrow functions
Props and state are the two ways data enters a React component. Once you understand the difference, the rest of React is largely the same idea applied in different places.
Props: passing data into a component
A prop is an input to a component. You pass props the same way you pass HTML attributes — as named values on the tag.
function Greeting(props) {
return <h1>Hello, {props.name}</h1>;
}
<Greeting name="Ada" />
props is always an object. Inside the component, the value you passed is available as props.name.
Destructure your props
Almost every real codebase destructures props directly in the function signature. It reads better and removes a layer of indirection.
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}
You can destructure multiple props at once:
function UserCard({ name, role, avatarUrl }) {
return (
<div>
<img src={avatarUrl} alt="" />
<h2>{name}</h2>
<p>{role}</p>
</div>
);
}
<UserCard name="Ada" role="Engineer" avatarUrl="/ada.png" />
Default values
Give a prop a default by using destructuring defaults:
function Button({ label = "Click me", disabled = false }) {
return <button disabled={disabled}>{label}</button>;
}
Now <Button /> renders a button labelled “Click me” and <Button label="Save" /> overrides the default.
Props can be anything
A prop is just a JavaScript value. Strings, numbers, booleans, arrays, objects, and even functions are all valid.
<UserCard
name="Ada"
age={36}
isActive
tags={['react', 'web']}
onSelect={() => console.log('clicked')}
/>
A few notes on syntax:
- Wrap non-string values in
{ }.age=36is invalid;age={36}is correct. - A boolean prop with no value (
isActive) is shorthand forisActive={true}. - Functions are passed as props all the time — that is how children talk back to parents.
Props are read-only
Inside a component, you must never reassign a prop.
function Bad({ name }) {
name = name.toUpperCase(); // do not do this
return <h1>{name}</h1>;
}
If you need a derived value, compute it into a new variable:
function Good({ name }) {
const displayName = name.toUpperCase();
return <h1>{displayName}</h1>;
}
This rule keeps data flow one-directional and predictable. A child component never modifies its parent’s data — it asks the parent to do so, which we cover below.
Try it yourself. Build a Price component that accepts amount (number) and currency (string, default "USD"). Render it as $19.99 USD. Use destructuring with a default and one computed string. Render three different <Price> elements from App.jsx to confirm reuse.
State: data a component owns
Props come from outside. State is data the component owns and can change over time. State is the reason a counter can count, a form can hold typed text, a modal can open and close.
In modern React, state lives inside a hook called useState.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
Three things are going on.
useState(0)declares one piece of state with an initial value of0.- It returns an array of two things: the current value and a setter function. Destructuring gives them names — by convention,
countandsetCount. - Calling
setCount(...)tells React the value changed. React re-renders the component, and the new value shows up in the JSX.
You can have as many useState calls as you need:
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [agreed, setAgreed] = useState(false);
// ...
}
We cover useState more thoroughly in the next post. The summary above is enough for this one.
Props vs. state at a glance
A short comparison that resolves most beginner confusion:
| Props | State | |
|---|---|---|
| Set by | The parent | The component itself |
| Mutable | No — read-only | Yes — via the setter |
| Causes re-render | When the parent re-renders | When the setter is called |
| Lives in | The parent’s JSX | The component itself |
Rule of thumb: if a piece of data is passed in from above, it is a prop. If the component itself creates or changes it, it is state.
Lifting state up
Often, two sibling components need to share the same data. React’s answer is to lift the state up to their common parent and pass it back down as props.
Consider a temperature input that shows whether water boils:
import { useState } from 'react';
function TemperatureInput({ temperature, onChange }) {
return (
<input
type="number"
value={temperature}
onChange={(e) => onChange(e.target.value)}
/>
);
}
function BoilingVerdict({ celsius }) {
if (celsius >= 100) return <p>The water would boil.</p>;
return <p>The water would not boil.</p>;
}
function Calculator() {
const [temperature, setTemperature] = useState('');
return (
<div>
<TemperatureInput temperature={temperature} onChange={setTemperature} />
<BoilingVerdict celsius={Number(temperature)} />
</div>
);
}
Notice how the state lives in Calculator. Both children receive what they need — the input gets the current value plus a callback to change it, the verdict gets the derived number. This pattern — state up, props down, callbacks back up — is the spine of every React app.
Callbacks: how children talk back
A child cannot modify its parent’s state directly. Instead, the parent passes a function as a prop and the child calls it.
function Toggle({ onToggle }) {
return <button onClick={onToggle}>Toggle</button>;
}
function Panel() {
const [open, setOpen] = useState(false);
return (
<div>
<Toggle onToggle={() => setOpen(!open)} />
{open && <p>Panel is open.</p>}
</div>
);
}
By convention, callback props start with on (onClick, onSubmit, onSelect). It is a strong signal to readers that this prop is an event handler.
Rules that keep state predictable
A few rules that look pedantic until you violate them and watch the UI break.
1. Never mutate state directly
This is the most important rule in React.
// Wrong — React will not re-render
items.push(newItem);
setItems(items);
// Right — create a new array
setItems([...items, newItem]);
React compares values by reference. If you push into the same array, the reference does not change and React assumes nothing happened. Always create a new array or object.
Common patterns:
// Add to an array
setItems([...items, newItem]);
// Remove from an array
setItems(items.filter((item) => item.id !== id));
// Update an array item
setItems(items.map((item) =>
item.id === id ? { ...item, done: true } : item
));
// Update an object
setUser({ ...user, name: newName });
The spread operator (...) is your closest friend in React.
2. State updates are asynchronous
Calling the setter does not immediately change the variable in the surrounding code.
function bad() {
setCount(count + 1);
console.log(count); // still the old value!
}
The new value only shows up on the next render. If you need to update state based on the previous value, pass a function to the setter:
setCount((prev) => prev + 1);
This guarantees you read the most recent value. Always use the functional form when the new state depends on the old.
3. Group related state — within reason
Two related values can live in one state object:
const [position, setPosition] = useState({ x: 0, y: 0 });
setPosition({ ...position, x: 100 });
But two unrelated values should stay separate:
const [name, setName] = useState('');
const [open, setOpen] = useState(false);
The cost of a single useState is essentially zero, so favour clarity.
Try it yourself. Build a TodoList with two child components — TodoInput and TodoItems. Lift the list state into the parent. The input adds a new item on submit. Each rendered item has a delete button that removes it. Use the immutable patterns from the rule above.
A small worked example
A working sign-up form combining props, state, and lifting:
import { useState } from 'react';
function Field({ label, value, onChange, type = 'text' }) {
return (
<label style={{ display: 'block', marginBottom: '0.5rem' }}>
{label}
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</label>
);
}
function SignUp() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const canSubmit = name.length > 0 && email.includes('@');
function handleSubmit(event) {
event.preventDefault();
console.log({ name, email });
}
return (
<form onSubmit={handleSubmit}>
<Field label="Name" value={name} onChange={setName} />
<Field label="Email" value={email} onChange={setEmail} type="email" />
<button type="submit" disabled={!canSubmit}>Sign up</button>
</form>
);
}
export default SignUp;
Every idea in this post appears in those twenty-five lines. Field receives label, value, onChange, and type as props. SignUp owns the state. The button’s disabled prop is computed directly from state on every render. The setter for each field is passed down as a callback prop.
Recap
You now know:
- Props pass data from parent to child and are read-only
- State is data a component owns and can change with a setter
useStatereturns[value, setter]; calling the setter triggers a re-render- Sibling components share data by lifting state up to a common parent
- Children talk back to parents by calling a callback prop
- Never mutate state — always create new arrays and objects
- Use the functional setter when the new state depends on the previous one
Next steps
The next post drills into the two hooks every React developer uses constantly — useState for state you have now seen, and useEffect for things that happen on the side: data fetching, subscriptions, and integration with the outside world.
→ Next: The Most Important React Hooks: useState and useEffect
Questions or feedback? Email codeloomdevv@gmail.com.