Skip to content

Lifting State Up

State lives inside a component. When two components need to share the same data, that state must live in their nearest common ancestor. Moving state up the tree to share it is called lifting state up — the fundamental pattern for coordinating components in React.

IncomeSection has a form to add income. LeftToAssign needs to know the total income to display the unassigned amount. These two components are siblings — they both render inside App. Neither can read the other’s state.

The solution: move the incomeSources array up to App. Both components receive what they need as props.

function App() {
const [incomeSources, setIncomeSources] = useState([]);
function addIncome(source) {
setIncomeSources(prev => [...prev, { ...source, id: crypto.randomUUID() }]);
}
function deleteIncome(id) {
setIncomeSources(prev => prev.filter(s => s.id !== id));
}
const totalIncome = incomeSources.reduce((sum, s) => sum + Number(s.amount), 0);
return (
<>
<IncomeSection
incomeSources={incomeSources}
onAdd={addIncome}
onDelete={deleteIncome}
/>
<LeftToAssign leftToAssign={totalIncome - totalBudgeted} />
</>
);
}

App owns the data. IncomeSection receives the list and two callback props: onAdd and onDelete. When the user submits the form inside IncomeSection, it calls onAdd — which is actually addIncome in App — and the state update happens at the top.

IncomeSection does not manage the list anymore. It receives it and calls the parent’s functions:

export default function IncomeSection({ incomeSources, onAdd, onDelete }) {
const [name, setName] = useState('');
const [amount, setAmount] = useState('');
function handleSubmit(e) {
e.preventDefault();
if (!name.trim() || !amount) return;
onAdd({ name: name.trim(), amount: parseFloat(amount) });
setName('');
setAmount('');
}
return (
<section className="income-section card">
<h2>Income</h2>
<ul>
{incomeSources.map(src => (
<li key={src.id}>
{src.name} — ${src.amount}
<button onClick={() => onDelete(src.id)}>×</button>
</li>
))}
</ul>
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} placeholder="Source name" />
<input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="Amount" />
<button type="submit">Add</button>
</form>
</section>
);
}

IncomeSection still owns its own local form state (name, amount). That state is private — only the form cares about it. But the list of income sources belongs to App because multiple parts of the UI depend on it.

Keep state local when only one component uses it. Lift it when two or more components need to read or change it.

  • Form field values → local (only the form uses them)
  • The income sources array → lifted (both IncomeSection and LeftToAssign need it)
  • The categories array → lifted (CategoryCard, LeftToAssign, and TransactionForm all need it)
  • Whether an inline edit is open → local (only the row that is editing needs it)
  1. Move incomeSources state to App.jsx with useState([]).
  2. Write addIncome and deleteIncome functions in App and pass them to IncomeSection as onAdd and onDelete.
  3. Update IncomeSection to use the props instead of managing its own list state.
  4. Confirm that adding and deleting income sources in the form updates the list correctly.
  • State that is shared between siblings must live in their nearest common ancestor.
  • The parent owns the data and passes it down as props; children call callback props to request changes.
  • Keep state local when only one component needs it. Lift it when multiple components need to share it.
  • The parent’s data flows down; child actions flow back up through callbacks.