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.
The problem
Section titled “The problem”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.
Moving state to the parent
Section titled “Moving state to the parent”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.
Receiving callbacks in children
Section titled “Receiving callbacks in children”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.
What to lift, what to keep local
Section titled “What to lift, what to keep local”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)
Exercise
Section titled “Exercise”- Move
incomeSourcesstate toApp.jsxwithuseState([]). - Write
addIncomeanddeleteIncomefunctions inAppand pass them toIncomeSectionasonAddandonDelete. - Update
IncomeSectionto use the props instead of managing its own list state. - 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.