βΆοΈ Live demo
Try it yourself β interact with the example below.
Loading demoβ¦
π― What Is Lifting State Up?
Imagine you and your friend at a restaurant splitting a bill:
WITHOUT LIFTING STATE UP (Everyone Tracks Their Own):
βββββββββββββββββββββββββββββββββββββββββββ
β π½οΈ THE RESTAURANT (Your App) β
β β
β βββββββββββββββ βββββββββββββββ β
β β You β β Your Friend β β
β β β β β β
β β Bill: $100 β β Bill: ??? β β
β β Tip: 10% β β Tip: 0% β β
β β β β β β
β β You can't β β Friend can'tβ β
β β see friend'sβ β see yours! β β
β β tip! β β β β
β βββββββββββββββ βββββββββββββββ β
β β
β β No one can calculate the total! β
β β Each person has their own data! β
β β Components can't talk to siblings! β
βββββββββββββββββββββββββββββββββββββββββββ
WITH LIFTING STATE UP (One Person Holds the Bill):
βββββββββββββββββββββββββββββββββββββββββββ
β π½οΈ THE RESTAURANT (Your App) β
β β
β βββββββββββββββββββββββββββββββββββ β
β β π§Ύ THE WAITER (TipCalculator) β β
β β Holds the bill & all tips! β β
β β (State lives here!) β β
β βββββββββββββββββββββββββββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββ βββββββββββββββ β
β β You β β Your Friend β β
β β says: β β says: β β
β β "I tip 10%" β β "I tip 0%" β β
β β β β β β
β β You tell β β Friend tellsβ β
β β waiter β β waiter β β
β β (via props) β β (via props) β β
β βββββββββββββββ βββββββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββββββββββββββββββββββ β
β β π§Ύ THE WAITER CALCULATES: β β
β β Average tip = (10+0)/2 = 5% β β
β β Total = $100 + $5 = $105 β β
β β (Derived state!) β β
β βββββββββββββββββββββββββββββββββββ β
β β
β β
Only ONE source of truth! β
β β
Everyone reports to the waiter! β
β β
Calculation happens in ONE place! β
β β
All components stay in sync! β
βββββββββββββββββββββββββββββββββββββββββββ
THE LIFTING STATE UP DECISION FLOWCHART:
βββββββββββββββββββββββββββββββββββββββββββ
β Do multiple components need this state? β
β β β
β Are they siblings? (same level) β
β β NO β Keep it where it is! β
β β YES β Find closest common parent! β
β β Move state there! β
β β Pass state down to children β
β that need to READ it β
β β Pass updater function down to β
β children that need to CHANGE it β
β β Parent calculates derived values! β
β β Everyone stays in sync! β β
βββββββββββββββββββββββββββββββββββββββββββ
β οΈ The Big Problem: "State Trapped in Children"
// ==========================================
// THE "TRAPPED STATE" TRAP (Wrong Way)
// ==========================================
// β WRONG: Each input has its OWN state
function BadTipCalculator() {
return (
<div>
<BadBillInput /> {/* Has bill state */}
<BadSelectPercentage /> {/* Has percentage1 state */}
<BadSelectPercentage /> {/* Has percentage2 state */}
{/* No one can calculate the total! */}
</div>
);
}
function BadBillInput() {
const [bill, setBill] = useState(''); // β Trapped here!
return (
<div>
<label>How much was the bill?</label>
<input
value={bill}
onChange={(e) => setBill(e.target.value)}
/>
</div>
);
}
function BadSelectPercentage() {
const [percentage, setPercentage] = useState(0); // β Trapped here!
return (
<div>
<label>How did you like the service?</label>
<select
value={percentage}
onChange={(e) => setPercentage(Number(e.target.value))}
>
<option value="0">Dissatisfied (0%)</option>
<option value="5">It was okay (5%)</option>
<option value="10">It was good (10%)</option>
<option value="20">Absolutely amazing (20%)</option>
</select>
</div>
);
}
// What happens:
// 1. User types $100 in BadBillInput
// 2. $100 is trapped in BadBillInput's state
// 3. BadTipCalculator (parent) has NO IDEA what the bill is!
// 4. User selects 10% in first BadSelectPercentage
// 5. 10% is trapped in that component's state
// 6. User selects 0% in second BadSelectPercentage
// 7. 0% is trapped in that component's state
// 8. NO ONE can calculate: $100 + average tip!
// 9. Output component can't display anything!
// Problems:
// 1. State is trapped in children
// 2. Parent can't calculate the total
// 3. Siblings can't share data
// 4. No single source of truth
// ==========================================
// THE SOLUTION: Lift State Up to Parent
// ==========================================
// β
CORRECT: State in parent, passed down to children
function GoodTipCalculator() {
// π STATE LIVES IN PARENT (TipCalculator)
const [bill, setBill] = useState(''); // Bill amount
const [percentage1, setPercentage1] = useState(0); // My tip %
const [percentage2, setPercentage2] = useState(0); // Friend's tip %
// Derived state - calculated from existing state!
const tip = bill * ((percentage1 + percentage2) / 2 / 100);
const total = Number(bill) + tip;
return (
<div>
<BillInput
bill={bill} // β Pass state DOWN (read)
onSetBill={setBill} // β Pass setter DOWN (write)
/>
<SelectPercentage
percentage={percentage1} // β Pass state DOWN
onSelect={setPercentage1} // β Pass setter DOWN
>
How did you like the service?
</SelectPercentage>
<SelectPercentage
percentage={percentage2} // β Pass state DOWN
onSelect={setPercentage2} // β Pass setter DOWN
>
How did your friend like the service?
</SelectPercentage>
{/* Output reads from parent's derived state */}
<Output bill={bill} tip={tip} total={total} />
{/* Reset calls parent's state setters */}
<Reset onReset={handleReset} />
</div>
);
}
π Complete Visual Examples
The Data Flow:
βββββββββββββββββββββββββββββββββββββββββββ
β TipCalculator (Parent) β
β const [bill, setBill] = useState('') β
β const [p1, setP1] = useState(0) β
β const [p2, setP2] = useState(0) β
β β
β const tip = bill * ((p1+p2)/2/100) β
β const total = bill + tip β
β β
β β β
β ββββ bill={bill} ββββββββΊ BillInput β
β β onSetBill={setBill} ββββΊ "Show $100β
β β User types β
β β Tell parentβ
β β β
β ββββ percentage={p1} ββββΊ Select% β
β β onSelect={setP1} βββββΊ "Show 10% β
β β User picks β
β β Tell parentβ
β β β
β ββββ percentage={p2} ββββΊ Select% β
β β onSelect={setP2} βββββΊ "Show 0% β
β β User picks β
β β Tell parentβ
β β β
β ββββ bill={bill} ββββββββΊ Output β
β β tip={tip} βββββββββββΊ "Show $105 β
β β total={total} β
β β β
β ββββ onReset={handleReset} βββΊ Reset β
β "Clear everything!" β
β β
β User types $100: β
β 1. BillInput calls onSetBill(100) β
β 2. Parent's setBill(100) runs β
β 3. Parent re-renders with bill=100 β
β 4. All children receive new bill prop β
β 5. Output recalculates: tip = $5 β
β 6. Output shows: "You pay $105" β
βββββββββββββββββββββββββββββββββββββββββββ
π Complete Working Code
import { useState } from 'react';
// ==========================================
// PARENT COMPONENT - The "Brain"
// ==========================================
function TipCalculator() {
// π ALL STATE LIVES IN PARENT (lifted up from children)
const [bill, setBill] = useState(''); // Bill amount (starts empty)
const [percentage1, setPercentage1] = useState(0); // My tip % (starts 0)
const [percentage2, setPercentage2] = useState(0); // Friend's tip % (starts 0)
// Derived state - calculated from existing state (NOT stored separately!)
const tip = bill * ((percentage1 + percentage2) / 2 / 100);
const total = Number(bill) + tip;
// Reset all values back to initial state
function handleReset() {
setBill('');
setPercentage1(0);
setPercentage2(0);
}
return (
<div className="container py-5" style={{ maxWidth: '500px' }}>
<h2 className="mb-4">π§Ύ Tip Calculator</h2>
{/* Bill Input - receives value and updater */}
<BillInput bill={bill} onSetBill={setBill} />
{/* Select Percentage - reused twice with different text! */}
<SelectPercentage
percentage={percentage1}
onSelect={setPercentage1}
>
How did you like the service?
</SelectPercentage>
<SelectPercentage
percentage={percentage2}
onSelect={setPercentage2}
>
How did your friend like the service?
</SelectPercentage>
{/* Only show output and reset if bill > 0 */}
{bill > 0 && (
<>
<Output bill={Number(bill)} tip={tip} total={total} />
<Reset onReset={handleReset} />
</>
)}
</div>
);
}
// ==========================================
// CHILD COMPONENT 1: Bill Input
// ==========================================
function BillInput({ bill, onSetBill }) {
return (
<div className="mb-3">
<label className="form-label fw-bold">How much was the bill?</label>
<input
type="text"
className="form-control"
placeholder="Bill value"
value={bill} // Controlled by parent
onChange={(e) => onSetBill(Number(e.target.value))} // Tell parent to update
/>
</div>
);
}
// ==========================================
// CHILD COMPONENT 2: Select Percentage (REUSABLE!)
// ==========================================
function SelectPercentage({ children, percentage, onSelect }) {
return (
<div className="mb-3">
<label className="form-label fw-bold">{children}</label>
<select
className="form-select"
value={percentage} // Controlled by parent
onChange={(e) => onSelect(Number(e.target.value))} // Tell parent to update
>
<option value="0">Dissatisfied (0%)</option>
<option value="5">It was okay (5%)</option>
<option value="10">It was good (10%)</option>
<option value="20">Absolutely amazing (20%)</option>
</select>
</div>
);
}
// ==========================================
// CHILD COMPONENT 3: Output Display
// ==========================================
function Output({ bill, tip, total }) {
return (
<div className="alert alert-info mb-3">
<h5 className="mb-0">
You pay ${total} (${bill} + ${tip} tip)
</h5>
</div>
);
}
// ==========================================
// CHILD COMPONENT 4: Reset Button
// ==========================================
function Reset({ onReset }) {
return (
<button className="btn btn-warning w-100" onClick={onReset}>
Reset
</button>
);
}
export default TipCalculator;
π§ Memory Aids for Poor Logic Thinking
The "Restaurant Waiter" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β LIFTED STATE = THE WAITER HOLDS THE CHECK β
β β
β BEFORE (Everyone has their own receipt): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β You: "I owe $100" π β β
β β Friend: "I owe... wait, what?" β β
β β β β
β β You: "I want to tip 10%" β β
β β Friend: "I want to tip 0%" β β
β β β β
β β NO ONE knows the total! β β
β β Can't calculate tip! β β
β β Can't split the bill! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β AFTER (Waiter holds everything): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β π§Ύ WAITER (TipCalculator) β β
β β Holds: bill, tip1, tip2 β β
β β β β
β β You tell waiter: "Bill is $100" β β
β β β Waiter writes it down (setBill) β β
β β β β
β β You tell waiter: "I tip 10%" β β
β β β Waiter writes it down (setP1) β β
β β β β
β β Friend tells waiter: "I tip 0%" β β
β β β Waiter writes it down (setP2) β β
β β β β
β β Waiter calculates: β β
β β Average = (10 + 0) / 2 = 5% β β
β β Tip = $100 Γ 5% = $5 β β
β β Total = $100 + $5 = $105 β β
β β β β
β β Waiter announces: "You pay $105!" β β
β β β Everyone hears the same number! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β You = BillInput + SelectPercentage β
β Friend = SelectPercentage β
β Waiter = TipCalculator (parent with state) β
β Announcement = Output component β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Bank Account" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β CONTROLLED COMPONENTS = BANK TELLER SYSTEM β
β β
β You (the input) want to withdraw money: β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β π° THE BANK (React State) β β
β β balance = $1000 β β
β β β β
β β You fill out form: "I want $500" β β
β β β Form shows $500 (value={balance}) β β
β β β β
β β BUT the bank hasn't approved it yet! β β
β β β onChange tells bank: "User wants $500"β β
β β β β
β β Bank updates: balance = $500 β β
β β β Form now shows $500 (new value prop) β β
β β β β
β β The form CANNOT change by itself! β β
β β It MUST ask the bank (state) first! β β
β β β β
β β value={state} + onChange={setState} β β
β β = CONTROLLED COMPONENT β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β Uncontrolled = You keep cash under mattress β
β Controlled = Money in bank, you use debit card β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Derived State" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β DERIVED STATE = RECIPE INGREDIENTS β
β β
β You have ingredients (state): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β π₯ INGREDIENTS (State) β β
β β flour = 2 cups β β
β β eggs = 3 β β
β β sugar = 1 cup β β
β β β β
β β You DON'T store "cake" separately! β β
β β You bake it when needed! β β
β β β β
β β const cake = bake(flour, eggs, sugar) β β
β β β Calculate on the fly! β β
β β β β
β β If flour changes β cake recalculates β β
β β If eggs change β cake recalculates β β
β β ALWAYS correct! NEVER out of sync! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β In React: β
β const [bill, setBill] = useState(100) β
β const [p1, setP1] = useState(10) β
β const [p2, setP2] = useState(0) β
β β
β const tip = bill * ((p1 + p2) / 2 / 100) β
β β tip is "baked" from bill, p1, p2 β
β β No need to store tip in state! β
β β Automatically updates when inputs change! β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Props Passing" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β PROPS = PASSING NOTES IN CLASS β
β β
β Teacher (Parent) writes a note: β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β π¨βπ« TEACHER (TipCalculator) β β
β β β β
β β Note to Student A: "The answer is 42" β β
β β β bill={42} β β
β β β β
β β Note to Student B: "Tell me if you β β
β β change your mind" β β
β β β onSelect={setPercentage1} β β
β β β β
β β Student A reads note: "Oh, answer is 42"β β
β β β const { bill } = props β β
β β β β
β β Student B has idea: "I choose 10%!" β β
β β β Writes back to teacher: onSelect(10) β β
β β β β
β β Teacher updates grade book: setP1(10) β β
β β β Re-calculates class average! β β
β β β Sends new notes to ALL students! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β Props = One-way street (Parent β Child) β
β Functions = Return street (Child β Parent) β
β Re-render = Teacher sends updated notes! β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Children Prop" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β CHILDREN PROP = ENVELOPE WITH CUSTOM LETTER β
β β
β You buy a greeting card (component): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β π¨ THE ENVELOPE (SelectPercentage) β β
β β Same envelope for everyone! β β
β β Same structure, same stamps! β β
β β β β
β β But the LETTER inside is different! β β
β β β β
β β Envelope 1: β β
β β <SelectPercentage> β β
β β How did you like the service? β β
β β </SelectPercentage> β β
β β β children = "How did you like..." β β
β β β β
β β Envelope 2: β β
β β <SelectPercentage> β β
β β How did your friend like... β β
β β </SelectPercentage> β β
β β β children = "How did your friend..." β β
β β β β
β β Same component, different content! β β
β β Reusable! Flexible! Powerful! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β Whatever is between opening and closing tags β
β becomes the "children" prop automatically! β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
π Practice Exercises
Exercise 1: Identify Where State Should Live
Look at this component tree. Where should each piece of state live?
App
βββ TipCalculator
β βββ BillInput
β βββ SelectPercentage (You)
β βββ SelectPercentage (Friend)
β βββ Output
β βββ Reset
Questions:
- Which component needs to know the bill amount? β
Outputneeds to display it - Are
BillInputandOutputsiblings? β Yes! - What's their common parent? β
TipCalculator - Should
BillInputhave its own bill state? β No! Lift it up!
Solution:
function TipCalculator() {
// π STATE LIVES IN PARENT (TipCalculator)
const [bill, setBill] = useState('');
const [percentage1, setPercentage1] = useState(0);
const [percentage2, setPercentage2] = useState(0);
const tip = bill * ((percentage1 + percentage2) / 2 / 100);
return (
<div>
<BillInput bill={bill} onSetBill={setBill} />
<SelectPercentage percentage={percentage1} onSelect={setPercentage1}>
How did you like the service?
</SelectPercentage>
<SelectPercentage percentage={percentage2} onSelect={setPercentage2}>
How did your friend like the service?
</SelectPercentage>
<Output bill={bill} tip={tip} />
<Reset onReset={handleReset} />
</div>
);
}
Exercise 2: Fix the Broken Controlled Input
This code has a bug - the input doesn't update when you type. Fix it!
function BillInput({ bill, onSetBill }) {
return (
<div>
<label>How much was the bill?</label>
<input
type="text"
placeholder="Bill value"
// TODO: Add value and onChange
/>
</div>
);
}
Solution:
function BillInput({ bill, onSetBill }) {
return (
<div>
<label>How much was the bill?</label>
<input
type="text"
placeholder="Bill value"
value={bill} // β
Controlled by parent
onChange={(e) => onSetBill(Number(e.target.value))} // β
Tell parent
/>
</div>
);
}
Exercise 3: Build a Simple Counter with Lifted State
Build a counter where two buttons share the same count:
function App() {
// TODO: Add state for count
// TODO: Pass count and setter to both buttons
return (
<div>
{/* Your code here */}
</div>
);
}
// Two CounterButton components that share state
function CounterButton({ label, count, onIncrement }) {
return (
<button onClick={onIncrement}>
{label}: {count}
</button>
);
}
Solution:
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
function handleIncrement() {
setCount(count + 1);
}
return (
<div>
<CounterButton
label="Button A"
count={count}
onIncrement={handleIncrement}
/>
<CounterButton
label="Button B"
count={count}
onIncrement={handleIncrement}
/>
<p>Total clicks: {count}</p>
</div>
);
}
function CounterButton({ label, count, onIncrement }) {
return (
<button className="btn btn-primary m-2" onClick={onIncrement}>
{label}: {count}
</button>
);
}
Exercise 4: Add a Third Person to the Tip Calculator
Add a third SelectPercentage for a third friend:
function TipCalculator() {
const [bill, setBill] = useState('');
const [percentage1, setPercentage1] = useState(0);
const [percentage2, setPercentage2] = useState(0);
// TODO: Add percentage3 state
// TODO: Update tip calculation for 3 people
const tip = bill * ((percentage1 + percentage2) / 2 / 100);
return (
<div>
<BillInput bill={bill} onSetBill={setBill} />
<SelectPercentage percentage={percentage1} onSelect={setPercentage1}>
How did you like the service?
</SelectPercentage>
<SelectPercentage percentage={percentage2} onSelect={setPercentage2}>
How did your friend like the service?
</SelectPercentage>
{/* TODO: Add third SelectPercentage */}
<Output bill={Number(bill)} tip={tip} />
</div>
);
}
Solution:
function TipCalculator() {
const [bill, setBill] = useState('');
const [percentage1, setPercentage1] = useState(0);
const [percentage2, setPercentage2] = useState(0);
const [percentage3, setPercentage3] = useState(0); // β
Added
// β
Updated for 3 people: divide by 3
const tip = bill * ((percentage1 + percentage2 + percentage3) / 3 / 100);
const total = Number(bill) + tip;
return (
<div>
<BillInput bill={bill} onSetBill={setBill} />
<SelectPercentage percentage={percentage1} onSelect={setPercentage1}>
How did you like the service?
</SelectPercentage>
<SelectPercentage percentage={percentage2} onSelect={setPercentage2}>
How did your friend like the service?
</SelectPercentage>
<SelectPercentage percentage={percentage3} onSelect={setPercentage3}>
How did your other friend like the service?
</SelectPercentage>
<Output bill={Number(bill)} tip={tip} total={total} />
</div>
);
}
π‘ Key Takeaways
| Concept | What It Means | Example |
|---|---|---|
| Components | Reusable building blocks | BillInput, SelectPercentage, Output, Reset |
| Props | Pass data down (parent β child) | bill={bill}, percentage={p1} |
| State | Component memory | const [bill, setBill] = useState('') |
| Controlled Components | React owns the input completely | value={bill} + onChange={onSetBill} |
| Lifting State Up | Move state to common parent | bill moved from BillInput to TipCalculator |
| Derived State | Calculate from existing state | const tip = bill * ((p1+p2)/2/100) |
| Conditional Rendering | Show/hide with && | {bill > 0 && <><Output /><Reset /></>} |
| Children Prop | Content between tags | <SelectPercentage>Text here</SelectPercentage> |
| Reusability | Same component, different props | SelectPercentage used twice |
Fragment <></> | Invisible wrapper for multiple elements | <>...</> wraps Output + Reset |
The Lifting State Up Pattern:
function Parent() {
const [sharedState, setSharedState] = useState(initialValue);
return (
<div>
<ChildA
value={sharedState}
onUpdate={setSharedState}
/>
<ChildB
value={sharedState}
onUpdate={setSharedState}
/>
<Display derivedValue={calculate(sharedState)} />
</div>
);
}
Golden Rules:
- One component per piece β
BillInput,SelectPercentage,Output,Reset - Lift state to common parent β When siblings need to share data
- Pass state down as props β For children to READ
- Pass functions down as props β For children to WRITE (update)
- Use controlled elements β Always pair
valuewithonChange - Use derived state β Calculate
tipfrombillandpercentages - Use
Number()conversion β Inputs are strings, math needs numbers - Use
&&for conditional rendering β Show output only whenbill > 0 - Use
<></>fragments β Wrap multiple elements without extra HTML - Reuse components β Same
SelectPercentagewith differentchildren
One Sentence Summary: > "The Tip Calculator teaches you to build small reusable components, lift their state up to a common parent so all siblings can stay in sync, pass data down through props and updater functions down through props, use controlled inputs so React manages every keystroke, calculate derived values like tip instead of storing them in state, conditionally render output only when there's a bill to calculate, and use the children prop to make components flexible and reusable β all while keeping one single source of truth in the parent component!"