Runnable lesson · press Try it on the examples to edit and run them here.
Props flow down from parent to child. To send information up, the parent passes a function, and the child calls it. This is how forms, lists and buttons talk to the rest of your app.
The pattern
- The parent defines a function that knows what to do (for example, update state).
- The parent passes it down as a prop (by convention named
onSomething). - The child calls it when something happens, and can pass data as arguments.
function AddButton({ onAdd }) {
return <button onClick={onAdd}>Add 1</button>;
}
function App() {
const [total, setTotal] = React.useState(0);
function handleAdd() {
setTotal(total + 1);
}
return (
<div>
<p>Total: {total}</p>
<AddButton onAdd={handleAdd} />
</div>
);
}
AddButton knows nothing about total. It only knows “when I am clicked, call onAdd“. That makes it reusable anywhere.
Sending data back to the parent
The child calls the function with arguments, and the parent receives them:
function NameList({ names, onSelect }) {
return (
<ul>
{names.map((name) => (
<li key={name}>
<button onClick={() => onSelect(name)}>{name}</button>
</li>
))}
</ul>
);
}
function App() {
const [chosen, setChosen] = React.useState("nobody yet");
return (
<div>
<p>Chosen: {chosen}</p>
<NameList names={["Ada", "Grace", "Linus"]} onSelect={setChosen} />
</div>
);
}
Here the parent even passes setChosen directly. Note onClick={() => onSelect(name)}: we wrap the call in an arrow function so it runs on click, not during rendering.
Pass the function, do not call it
This is the number one mistake with function props:
<button onClick={handleClick}>OK</button> // correct: passes the function
<button onClick={handleClick()}>OK</button> // wrong: calls it while rendering
<button onClick={() => handleClick(id)}>OK</button> // correct: wraps a call that needs arguments
Writing handleClick() runs the function immediately during rendering, and its return value (usually undefined) becomes the click handler. If it also sets state, you get an endless loop of re-renders and an error.
Naming
- The prop a component accepts starts with
on:onSave,onDelete,onSelect. - The function the parent writes starts with
handle:handleSave.
<ProductForm onSave={handleSave} />
Lifting state up
When two sibling components must share data, keep the state in their common parent and pass values down and functions down. That pattern is called lifting state up, and it is the subject of Lifting State Up.
A complete example: a deletable list
function Item({ name, onDelete }) {
return (
<li>
{name} <button aria-label={"Delete " + name} onClick={() => onDelete(name)}>Delete {name}</button>
</li>
);
}
function App() {
const [items, setItems] = React.useState(["Milk", "Eggs", "Bread"]);
function handleDelete(name) {
setItems(items.filter((item) => item !== name));
}
return (
<ul>
{items.map((item) => (
<Item key={item} name={item} onDelete={handleDelete} />
))}
</ul>
);
}
App owns the list. Item just reports “delete me” and App decides what that means.
Key points
- To communicate from child to parent, pass a function as a prop and call it in the child.
- Pass the function itself (
onClick={handle}), not its result (onClick={handle()}). - Use
onXfor props andhandleXfor the functions you define.