βΆοΈ Live demo
Try it yourself β interact with the example below.
Loading demoβ¦
π― What Is Lifting State Up?
Imagine you and your siblings sharing one TV remote:
WITHOUT LIFTING STATE UP (Everyone Has Their Own Remote):
βββββββββββββββββββββββββββββββββββββββββββ
β π THE HOUSE (Your App) β
β β
β βββββββββββββββ βββββββββββββββ β
β β Brother's β β Sister's β β
β β Room β β Room β β
β β β β β β
β β Remote: ON β β Remote: ON β β
β β TV is ON! β β TV is ON! β β
β β β β β β
β β Brother β β Sister β β
β β controls β β controls β β
β β his own TV! β β her own TV! β β
β βββββββββββββββ βββββββββββββββ β
β β
β β Two TVs! Two remotes! β
β β Not sharing! Not in sync! β
β β Each room has its OWN state! β
βββββββββββββββββββββββββββββββββββββββββββ
WITH LIFTING STATE UP (One Remote for Everyone):
βββββββββββββββββββββββββββββββββββββββββββ
β π THE HOUSE (Your App) β
β β
β βββββββββββββββββββββββββββββββββββ β
β β πΊ LIVING ROOM (Accordion) β β
β β One TV, one remote! β β
β β (State lives here!) β β
β βββββββββββββββββββββββββββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββ βββββββββββββββ β
β β Brother β β Sister β β
β β asks: β β asks: β β
β β "Can I β β "Can I β β
β β watch?" β β watch?" β β
β β β β β β
β β Brother β β Sister β β
β β borrows β β borrows β β
β β remote β β remote β β
β β (via props) β β (via props) β β
β βββββββββββββββ βββββββββββββββ β
β β
β β
Only ONE TV! β
β β
Only ONE can watch at a time! β
β β
Living room = Common Parent! β
β β
Everyone shares the same state! β
βββββββββββββββββββββββββββββββββββββββββββ
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 β
β β Everyone stays in sync! β β
βββββββββββββββββββββββββββββββββββββββββββ
β οΈ The Big Problem: "Each Item Has Its Own State"
// ==========================================
// THE "INDEPENDENT STATE" TRAP (Part 1)
// ==========================================
// β WRONG: Each item has its OWN state (can all be open)
function BadAccordion() {
return (
<div>
<AccordionItem num={0} title="Q1" text="A1" /> {/* Has isOpen */}
<AccordionItem num={1} title="Q2" text="A2" /> {/* Has isOpen */}
<AccordionItem num={2} title="Q3" text="A3" /> {/* Has isOpen */}
{/* All can be open at the same time! */}
</div>
);
}
function AccordionItem({ num, title, text }) {
const [isOpen, setIsOpen] = useState(false); // β Each item's OWN state!
return (
<div onClick={() => setIsOpen(!isOpen)}>
<h3>{num} {title} {isOpen ? '-' : '+'}</h3>
{isOpen && <p>{text}</p>}
</div>
);
}
// What happens:
// Click item 1 β item 1 opens
// Click item 2 β item 2 opens (item 1 STAYS open!)
// Click item 3 β item 3 opens (items 1 & 2 STAY open!)
// Result: ALL items can be open at once!
// Problems:
// 1. Multiple items open at the same time
// 2. User wants ONLY ONE open at a time
// 3. Items need to KNOW about each other
// 4. But siblings CANNOT talk to each other!
// ==========================================
// THE SOLUTION: Lift State Up to Parent
// ==========================================
// β
CORRECT: State in parent, passed down to children
function GoodAccordion() {
// π STATE LIVES IN PARENT (Accordion)
const [curOpen, setCurOpen] = useState(null); // null = none open
return (
<div>
<AccordionItem
num={0}
title="Q1"
text="A1"
curOpen={curOpen} // β Pass state DOWN
onOpen={setCurOpen} // β Pass setter DOWN
/>
<AccordionItem
num={1}
title="Q2"
text="A2"
curOpen={curOpen}
onOpen={setCurOpen}
/>
<AccordionItem
num={2}
title="Q3"
text="A3"
curOpen={curOpen}
onOpen={setCurOpen}
/>
{/* Only ONE can be open! */}
</div>
);
}
function AccordionItem({ num, title, text, curOpen, onOpen }) {
// Calculate isOpen based on parent's state!
const isOpen = num === curOpen; // β "Am I the open one?"
return (
<div onClick={() => onOpen(num)}> {/* β Tell parent to open ME */}
<h3>{num} {title} {isOpen ? '-' : '+'}</h3>
{isOpen && <p>{text}</p>}
</div>
);
}
// What happens now:
// 1. curOpen = null (none open)
// 2. Click item 1 β onOpen(1) β curOpen = 1 β item 1 opens
// 3. Click item 2 β onOpen(2) β curOpen = 2 β item 2 opens, item 1 closes!
// 4. Click item 2 again β onOpen(2) β curOpen = 2 β already open, stays open
// 5. To close: need special logic (set to null)
// Result: ONLY ONE item open at a time!
π Complete Visual Examples
Create file: react-accordion-lifted-state.js
// ==========================================
// ACCORDION WITH LIFTED STATE - Complete Guide
// ==========================================
/*
STATE LIFTING DECISION:
βββββββββββββββββββββββββββββββββββββββββββ
β BEFORE (Part 1): β
β β’ Each AccordionItem has its OWN state β
β β’ They are INDEPENDENT β
β β’ Multiple can be open at once β
β β
β AFTER (Part 2): β
β β’ Accordion (parent) has the state β
β β’ Items are DEPENDENT on parent β
β β’ Only ONE can be open at a time β
β β
β WHY? Because items need to KNOW β
β about each other! "Am I the open one?" β
β β
β SOLUTION: Lift state to common parent! β
βββββββββββββββββββββββββββββββββββββββββββ
DATA FLOW:
βββββββββββββββββββββββββββββββββββββββββββ
β Accordion (Parent) β
β const [curOpen, setCurOpen] = useState(null)β
β β
β β β
β ββββ curOpen={curOpen} ββββΊ Item 0 β
β β onOpen={setCurOpen} ββββΊ "Are you β
β β open? No." β
β β β
β ββββ curOpen={curOpen} ββββΊ Item 1 β
β β onOpen={setCurOpen} ββββΊ "Are you β
β β open? Yes!"β
β β (curOpen=1)β
β β β
β ββββ curOpen={curOpen} ββββΊ Item 2 β
β onOpen={setCurOpen} ββββΊ "Are you β
β open? No." β
β β
β Click Item 1: β
β 1. Item 1 calls onOpen(1) β
β 2. Parent's setCurOpen(1) runs β
β 3. Parent re-renders with curOpen=1 β
β 4. All items receive new curOpen prop β
β 5. Item 1: isOpen = (1 === 1) = true β
β 6. Items 0,2: isOpen = false β
βββββββββββββββββββββββββββββββββββββββββββ
THE TOGGLE LOGIC:
βββββββββββββββββββββββββββββββββββββββββββ
β Click open item β Should CLOSE it β
β Click closed item β Should OPEN it β
β β
β BEFORE (bug): β
β onClick={() => onOpen(num)} β
β β’ Click open item β onOpen(num) β stays open!β
β β
β AFTER (fix): β
β onClick={() => onOpen(isOpen ? null : num)}β
β β’ Click open item β isOpen is true β onOpen(null)β
β β’ Click closed item β isOpen is false β onOpen(num)β
β β
β null = "Nothing is open" β
βββββββββββββββββββββββββββββββββββββββββββ
*/
// ==========================================
// EXAMPLE 1: The Data Structure
// ==========================================
const faqs = [
{
title: "Where are these chairs assembled?",
text: "Lorem ipsum dolor sit amet consectetur, adipisicing elit..."
},
{
title: "How long do I have to return my chair?",
text: "Accusantium voluptatum..."
},
{
title: "Do you ship to countries outside the EU?",
text: "Excepturi velit..."
}
];
// ==========================================
// EXAMPLE 2: Accordion with Lifted State
// ==========================================
function Accordion({ data }) {
// π STATE LIVES IN PARENT (lifted up from items)
const [curOpen, setCurOpen] = useState(null);
// curOpen = the number of the currently open item
// null = nothing is open
return (
<div className="accordion">
{data.map((element, index) => (
<AccordionItem
key={element.title}
num={index}
title={element.title}
curOpen={curOpen} // β Pass state DOWN (read)
onOpen={setCurOpen} // β Pass setter DOWN (write)
>
{element.text} {/* β Children prop for content */}
</AccordionItem>
))}
{/* Bonus: Can add items with JSX children! */}
<AccordionItem
num={23}
title="Test Item"
curOpen={curOpen}
onOpen={setCurOpen}
>
<p>This is a test item with JSX content!</p>
<ul>
<li>Point 1</li>
<li>Point 2</li>
</ul>
</AccordionItem>
</div>
);
}
// ==========================================
// EXAMPLE 3: AccordionItem (No State, Just Props)
// ==========================================
function AccordionItem({ num, title, curOpen, onOpen, children }) {
// Calculate isOpen from parent's state!
const isOpen = num === curOpen;
// "Is my number the same as the currently open number?"
function handleToggle() {
// If already open β close it (set to null)
// If closed β open it (set to my number)
onOpen(isOpen ? null : num);
}
return (
<div
className={`item ${isOpen ? "open" : ""}`}
onClick={handleToggle}
>
<p className="number">{num < 9 ? `0${num + 1}` : num + 1}</p>
<p className="title">{title}</p>
<p className="icon">{isOpen ? "-" : "+"}</p>
{/* Content via children prop */}
{isOpen && <div className="content-box">{children}</div>}
</div>
);
}
// ==========================================
// EXAMPLE 4: Complete App
// ==========================================
function App() {
return (
<div>
<Accordion data={faqs} />
</div>
);
}
// ==========================================
// EXAMPLE 5: Visual Flow of Clicking
// ==========================================
// Initial State:
// curOpen = null
// All items: isOpen = false (closed)
// Click Item 1 (num = 1):
// 1. handleToggle runs
// 2. isOpen = false (item 1 is closed)
// 3. onOpen(isOpen ? null : 1) β onOpen(1)
// 4. Parent's setCurOpen(1) runs
// 5. Parent re-renders with curOpen = 1
// 6. All items re-render with new curOpen prop
// 7. Item 1: isOpen = (1 === 1) = true β OPEN!
// 8. Other items: isOpen = false β CLOSED!
// Click Item 1 Again (num = 1):
// 1. handleToggle runs
// 2. isOpen = true (item 1 is open)
// 3. onOpen(isOpen ? null : 1) β onOpen(null)
// 4. Parent's setCurOpen(null) runs
// 5. Parent re-renders with curOpen = null
// 6. All items re-render
// 7. All items: isOpen = false β ALL CLOSED!
// Click Item 2 (num = 2):
// 1. handleToggle runs
// 2. isOpen = false (item 2 is closed)
// 3. onOpen(isOpen ? null : 2) β onOpen(2)
// 4. Parent's setCurOpen(2) runs
// 5. Parent re-renders with curOpen = 2
// 6. Item 2: isOpen = (2 === 2) = true β OPEN!
// 7. Item 1: isOpen = (1 === 2) = false β CLOSED!
// 8. Other items: isOpen = false β CLOSED!
π Interactive React Usage Examples
Complete React File: AccordionLiftedStateMasterClass.jsx
import React, { useState } from 'react';
// ==========================================
// INTERACTIVE ACCORDION LIFTED STATE DEMO
// ==========================================
function AccordionLiftedStateMasterClass() {
const [activeDemo, setActiveDemo] = useState('problem');
const demos = {
problem: { title: 'The Problem', component: <ProblemDemo /> },
solution: { title: 'The Solution', component: <SolutionDemo /> },
flow: { title: 'Data Flow', component: <DataFlowDemo /> },
children: { title: 'Children Prop', component: <ChildrenPropDemo /> },
complete: { title: 'Complete App', component: <CompleteAppDemo /> }
};
return (
<div className="container py-5">
<header className="bg-warning text-dark p-4 rounded-3 mb-4">
<h1 className="m-0">β¬οΈ Lifting State Up</h1>
<p className="mb-0 mt-2 opacity-75">Moving State to the Common Parent</p>
</header>
<nav className="d-flex gap-2 mb-4 flex-wrap">
{Object.entries(demos).map(([key, { title }]) => (
<button
key={key}
onClick={() => setActiveDemo(key)}
className={`btn ${activeDemo === key ? 'btn-warning' : 'btn-outline-secondary'}`}
>
{title}
</button>
))}
</nav>
<main className="bg-light p-4 rounded-3" style={{ minHeight: '400px' }}>
{demos[activeDemo].component}
</main>
</div>
);
}
// ==========================================
// DEMO 1: The Problem (Independent State)
// ==========================================
function ProblemDemo() {
return (
<div>
<h2 className="mb-4">The Problem: Independent State π±</h2>
<div className="alert alert-danger">
<h5 className="alert-heading">Each item has its OWN state - they can ALL be open!</h5>
</div>
<div className="row g-4">
<div className="col-md-6">
<div className="card border-danger">
<div className="card-header bg-danger text-white">
<h5 className="mb-0">β Part 1: Independent State</h5>
</div>
<div className="card-body">
<pre className="bg-dark text-light p-3 rounded small">
{`function AccordionItem({ num, title, text }) {
const [isOpen, setIsOpen] = useState(false);
// β Each item has its OWN state!
return (
<div onClick={() => setIsOpen(!isOpen)}>
<h3>{title} {isOpen ? '-' : '+'}</h3>
{isOpen && <p>{text}</p>}
</div>
);
}`}
</pre>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card">
<div className="card-header bg-light">
<h5 className="mb-0">Result:</h5>
</div>
<div className="card-body">
<BadAccordionItem num={0} title="Question 1" text="Answer 1" />
<BadAccordionItem num={1} title="Question 2" text="Answer 2" />
<BadAccordionItem num={2} title="Question 3" text="Answer 3" />
</div>
</div>
</div>
</div>
<div className="alert alert-warning mt-4">
<h5 className="alert-heading">π§ The Problem:</h5>
<p className="mb-0">Click multiple items - they ALL stay open! But we want ONLY ONE open at a time!</p>
</div>
</div>
);
}
function BadAccordionItem({ num, title, text }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div
className={`card mb-2 ${isOpen ? 'border-success' : ''}`}
onClick={() => setIsOpen(!isOpen)}
style={{ cursor: 'pointer' }}
>
<div className={`card-body d-flex justify-content-between align-items-center ${isOpen ? 'bg-success-subtle' : ''}`}>
<span className="fw-bold">{title}</span>
<span className="badge bg-secondary">{isOpen ? '-' : '+'}</span>
</div>
{isOpen && (
<div className="card-footer">
<p className="mb-0">{text}</p>
</div>
)}
</div>
);
}
// ==========================================
// DEMO 2: The Solution (Lifted State)
// ==========================================
function SolutionDemo() {
return (
<div>
<h2 className="mb-4">The Solution: Lifted State β
</h2>
<div className="alert alert-success">
<h5 className="alert-heading">State moves to parent - only ONE can be open!</h5>
</div>
<div className="row g-4">
<div className="col-md-6">
<div className="card border-success">
<div className="card-header bg-success text-white">
<h5 className="mb-0">β
Part 2: Lifted State</h5>
</div>
<div className="card-body">
<pre className="bg-dark text-light p-3 rounded small">
{`function Accordion({ data }) {
const [curOpen, setCurOpen] = useState(null);
// β State in PARENT!
return (
<div>
{data.map((el, i) => (
<AccordionItem
key={el.title}
num={i}
title={el.title}
curOpen={curOpen} // β Pass DOWN
onOpen={setCurOpen} // β Pass DOWN
>
{el.text}
</AccordionItem>
))}
</div>
);
}`}
</pre>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card">
<div className="card-header bg-light">
<h5 className="mb-0">Result:</h5>
</div>
<div className="card-body">
<GoodAccordion />
</div>
</div>
</div>
</div>
<div className="alert alert-success mt-4">
<h5 className="alert-heading">β
How It Works:</h5>
<ol className="mb-0">
<li>State lives in Accordion (parent)</li>
<li>Each item receives curOpen (which one is open)</li>
<li>Each item receives onOpen (function to change it)</li>
<li>Item calculates: "Am I the open one?"</li>
<li>Clicking tells parent: "Make ME the open one!"</li>
</ol>
</div>
</div>
);
}
function GoodAccordion() {
const [curOpen, setCurOpen] = useState(null);
const faqs = [
{ title: "What is React?", text: "A JavaScript library for building UIs." },
{ title: "Why use React?", text: "It's fast, efficient, and component-based." },
{ title: "How to start?", text: "Install Node.js and create-react-app." }
];
return (
<div>
{faqs.map((el, i) => (
<GoodAccordionItem
key={el.title}
num={i}
title={el.title}
curOpen={curOpen}
onOpen={setCurOpen}
>
{el.text}
</GoodAccordionItem>
))}
</div>
);
}
function GoodAccordionItem({ num, title, curOpen, onOpen, children }) {
const isOpen = num === curOpen;
function handleToggle() {
onOpen(isOpen ? null : num);
}
return (
<div
className={`card mb-2 ${isOpen ? 'border-success' : ''}`}
onClick={handleToggle}
style={{ cursor: 'pointer' }}
>
<div className={`card-body d-flex justify-content-between align-items-center ${isOpen ? 'bg-success-subtle' : ''}`}>
<span className="fw-bold">{title}</span>
<span className={`badge ${isOpen ? 'bg-success' : 'bg-secondary'}`}>
{isOpen ? '-' : '+'}
</span>
</div>
{isOpen && (
<div className="card-footer">
<p className="mb-0">{children}</p>
</div>
)}
</div>
);
}
// ==========================================
// DEMO 3: Data Flow Visualization
// ==========================================
function DataFlowDemo() {
const [curOpen, setCurOpen] = useState(null);
const [lastAction, setLastAction] = useState('None');
const items = [
{ num: 0, title: 'Item 1' },
{ num: 1, title: 'Item 2' },
{ num: 2, title: 'Item 3' }
];
function handleOpen(num) {
const newOpen = curOpen === num ? null : num;
setCurOpen(newOpen);
setLastAction(`Clicked item ${num + 1} β curOpen = ${newOpen === null ? 'null' : newOpen + 1}`);
}
return (
<div>
<h2 className="mb-4">Data Flow Visualization π</h2>
<div className="alert alert-info">
<h5 className="alert-heading">Watch how data flows from child β parent β all children!</h5>
</div>
<div className="card mb-4 border-primary">
<div className="card-header bg-primary text-white">
<h5 className="mb-0">π Accordion (Parent) - State: curOpen = {curOpen === null ? 'null' : curOpen + 1}</h5>
</div>
<div className="card-body">
<div className="alert alert-warning">
<strong>Last Action:</strong> {lastAction}
</div>
{items.map(item => (
<FlowItem
key={item.num}
num={item.num}
title={item.title}
curOpen={curOpen}
onOpen={handleOpen}
/>
))}
</div>
</div>
<div className="card border-info">
<div className="card-header bg-info text-white">
<h5 className="mb-0">π State Table</h5>
</div>
<div className="card-body">
<table className="table table-sm">
<thead>
<tr>
<th>Item</th>
<th>num</th>
<th>curOpen</th>
<th>isOpen = (num === curOpen)</th>
</tr>
</thead>
<tbody>
{items.map(item => (
<tr key={item.num} className={item.num === curOpen ? 'table-success' : ''}>
<td>{item.title}</td>
<td>{item.num + 1}</td>
<td>{curOpen === null ? 'null' : curOpen + 1}</td>
<td>{item.num === curOpen ? 'β
TRUE (Open)' : 'β FALSE (Closed)'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
function FlowItem({ num, title, curOpen, onOpen }) {
const isOpen = num === curOpen;
return (
<div
className={`card mb-2 ${isOpen ? 'border-success' : 'border-light'}`}
onClick={() => onOpen(num)}
style={{ cursor: 'pointer' }}
>
<div className={`card-body py-2 d-flex justify-content-between align-items-center ${isOpen ? 'bg-success-subtle' : ''}`}>
<div>
<span className="badge bg-secondary me-2">{num + 1}</span>
<span className="fw-bold">{title}</span>
</div>
<div className="small text-muted">
isOpen = {num} === {curOpen === null ? 'null' : curOpen} β {isOpen ? 'true' : 'false'}
</div>
</div>
</div>
);
}
// ==========================================
// DEMO 4: Children Prop with JSX
// ==========================================
function ChildrenPropDemo() {
const [curOpen, setCurOpen] = useState(null);
return (
<div>
<h2 className="mb-4">Children Prop + Lifted State π¨</h2>
<div className="alert alert-info">
<h5 className="alert-heading">Using children prop for flexible content!</h5>
</div>
<div className="row g-4">
<div className="col-md-6">
<JSXAccordionItem
num={0}
title="Simple Text"
curOpen={curOpen}
onOpen={setCurOpen}
>
<p className="mb-0">Just plain text content here.</p>
</JSXAccordionItem>
</div>
<div className="col-md-6">
<JSXAccordionItem
num={1}
title="With List"
curOpen={curOpen}
onOpen={setCurOpen}
>
<p className="mb-2">Features:</p>
<ul className="mb-0">
<li>Fast</li>
<li>Reliable</li>
<li>Easy to use</li>
</ul>
</JSXAccordionItem>
</div>
<div className="col-md-6">
<JSXAccordionItem
num={2}
title="With Button"
curOpen={curOpen}
onOpen={setCurOpen}
>
<p className="mb-2">Click the button below:</p>
<button className="btn btn-sm btn-primary">Learn More</button>
</JSXAccordionItem>
</div>
<div className="col-md-6">
<JSXAccordionItem
num={3}
title="With Image"
curOpen={curOpen}
onOpen={setCurOpen}
>
<div className="bg-secondary text-white p-3 rounded text-center mb-2">
π· Image Placeholder
</div>
<p className="mb-0 small">A beautiful image description.</p>
</JSXAccordionItem>
</div>
</div>
</div>
);
}
function JSXAccordionItem({ num, title, curOpen, onOpen, children }) {
const isOpen = num === curOpen;
function handleToggle() {
onOpen(isOpen ? null : num);
}
return (
<div
className={`card mb-3 ${isOpen ? 'border-info border-2' : ''}`}
onClick={handleToggle}
style={{ cursor: 'pointer' }}
>
<div className={`card-body d-flex justify-content-between align-items-center ${isOpen ? 'bg-info-subtle' : ''}`}>
<span className="fw-bold">{title}</span>
<span className={`badge ${isOpen ? 'bg-info' : 'bg-secondary'}`}>
{isOpen ? '-' : '+'}
</span>
</div>
{isOpen && (
<div className="card-footer bg-light">
{children}
</div>
)}
</div>
);
}
// ==========================================
// DEMO 5: Complete App
// ==========================================
function CompleteAppDemo() {
const [curOpen, setCurOpen] = useState(null);
const faqs = [
{ title: "Where are these chairs assembled?", text: "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Accusantium, quaerat temporibus quas dolore provident nisi ut aliquid." },
{ title: "How long do I have to return my chair?", text: "Pariatur recusandae dignissimos fuga voluptas unde optio nesciunt commodi beatae, explicabo natus." },
{ title: "Do you ship to countries outside the EU?", text: "Excepturi velit numquam, fugit quos, debitis, maxime quaerat sed nisi placeat tempore tenetur." }
];
return (
<div>
<h2 className="mb-4">Complete Accordion App π</h2>
<div className="alert alert-info">
<h5 className="alert-heading">Full implementation with lifted state + children prop!</h5>
</div>
<div className="card shadow-lg mx-auto" style={{ maxWidth: '600px' }}>
<div className="card-header bg-warning text-dark">
<h5 className="mb-0">π Frequently Asked Questions</h5>
</div>
<div className="card-body">
{faqs.map((el, i) => (
<FinalAccordionItem
key={el.title}
num={i}
title={el.title}
curOpen={curOpen}
onOpen={setCurOpen}
>
{el.text}
</FinalAccordionItem>
))}
{/* Bonus item with JSX children! */}
<FinalAccordionItem
num={23}
title="Custom JSX Content"
curOpen={curOpen}
onOpen={setCurOpen}
>
<p className="mb-2">This item uses the <strong>children prop</strong> for complex content!</p>
<ul className="mb-2">
<li>Point 1</li>
<li>Point 2</li>
</ul>
<button className="btn btn-sm btn-success">Action Button</button>
</FinalAccordionItem>
</div>
</div>
<div className="alert alert-success mt-4">
<h5 className="alert-heading">β
What We Built:</h5>
<ol className="mb-0">
<li>State lifted from items to Accordion parent</li>
<li>curOpen tracks which item is open (null = none)</li>
<li>onOpen function passed down to each item</li>
<li>Items calculate isOpen from props (not state!)</li>
<li>Toggle logic: open β close, closed β open</li>
<li>Children prop for flexible content</li>
<li>Only ONE item open at a time!</li>
</ol>
</div>
</div>
);
}
function FinalAccordionItem({ num, title, curOpen, onOpen, children }) {
const isOpen = num === curOpen;
function handleToggle() {
onOpen(isOpen ? null : num);
}
return (
<div
className={`card mb-2 ${isOpen ? 'border-warning border-2' : ''}`}
onClick={handleToggle}
style={{ cursor: 'pointer' }}
>
<div className={`card-body d-flex justify-content-between align-items-center ${isOpen ? 'bg-warning-subtle' : ''}`}>
<div className="d-flex align-items-center gap-3">
<span className={`badge ${isOpen ? 'bg-warning text-dark' : 'bg-secondary'}`}>
{num < 9 ? `0${num + 1}` : num + 1}
</span>
<span className={`fw-bold ${isOpen ? 'text-dark' : ''}`}>{title}</span>
</div>
<span className={`badge ${isOpen ? 'bg-warning text-dark' : 'bg-secondary'}`}>
{isOpen ? '-' : '+'}
</span>
</div>
{isOpen && (
<div className="card-footer bg-light">
<div className="mb-0">{children}</div>
</div>
)}
</div>
);
}
export default AccordionLiftedStateMasterClass;
π§ Memory Aids for Poor Logic Thinking
The "TV Remote" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β LIFTED STATE = ONE TV REMOTE FOR THE HOUSE β
β β
β BEFORE (Each room has its own TV): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Room 1: TV ON πΊ β β
β β Room 2: TV ON πΊ β β
β β Room 3: TV ON πΊ β β
β β β β
β β Each room controls its own TV! β β
β β All TVs can be on at once! β β
β β No coordination! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β AFTER (One remote in living room): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β π LIVING ROOM (Accordion) β β
β β One remote controls ALL TVs! β β
β β curOpen = which TV is on β β
β β β β
β β Remote: "Only TV 2 can be on!" β β
β β β β
β β Room 1: TV OFF β β β
β β Room 2: TV ON πΊ β Only this one! β β
β β Room 3: TV OFF β β β
β β β β
β β Click Room 3: β β
β β Room 1: TV OFF β β β
β β Room 2: TV OFF β β β
β β Room 3: TV ON πΊ β Now this one! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β The remote (state) is in the living room! β
β Each room asks: "Am I the one with TV on?" β
β (isOpen = num === curOpen) β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Classroom" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β LIFTED STATE = TEACHER CALLING ON STUDENTS β
β β
β BEFORE (Students raise hands independently): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Student 1: Hand UP β β β
β β Student 2: Hand UP β β β
β β Student 3: Hand UP β β β
β β β β
β β Teacher: "Too many hands! Confusing!" β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β AFTER (Teacher controls who speaks): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β π¨βπ« TEACHER (Accordion - has state) β β
β β curOpen = which student speaks β β
β β β β
β β Student 1: "Can I speak?" β β
β β Teacher: "No." β Hand DOWN π β β
β β β β
β β Student 2: "Can I speak?" β β
β β Teacher: "Yes!" β Hand UP β β β
β β (curOpen = 2) β β
β β β β
β β Student 3: "Can I speak?" β β
β β Teacher: "No, Student 2 is speaking!" β β
β β β Hand DOWN π β β
β β β β
β β Student 2 again: "I'm done!" β β
β β Teacher: "OK, no one speaks now." β β
β β (curOpen = null) β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β Teacher = Accordion (parent with state) β
β Students = AccordionItems (children with props)β
β "Can I speak?" = isOpen = (num === curOpen) β
β Teacher's decision = setCurOpen(num) β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Parking Spot" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β LIFTED STATE = ONE PARKING SPOT β
β β
β Parking lot with ONE spot: β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β π
ΏοΈ PARKING LOT (Accordion) β β
β β Only ONE car can park! β β
β β curOpen = which car is parked β β
β β β β
β β Car 1 arrives: "Can I park?" β β
β β Lot: "Yes!" β Car 1 parks π β β
β β curOpen = 1 β β
β β β β
β β Car 2 arrives: "Can I park?" β β
β β Lot: "No! Car 1 is there!" β β
β β Car 1 must LEAVE first! β β
β β Lot: "OK Car 1, leave!" β β
β β Car 1 leaves πβ β β
β β curOpen = null β β
β β Lot: "Car 2, you can park now!" β β
β β Car 2 parks π β β
β β curOpen = 2 β β
β β β β
β β Car 2 leaves: "I'm leaving!" β β
β β Lot: "OK, spot is empty." β β
β β curOpen = null β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β Click item = Car arrives β
β curOpen = which car has the spot β
β null = spot is empty (no item open) β
β Toggle logic: if I'm parked, I leave β
β if spot empty, I park β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
The "Toggle Logic" Analogy
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β TOGGLE LOGIC = LIGHT SWITCH WITH MEMORY β
β β
β Simple switch (no memory): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Click β Always turns ON β β
β β Click again β Still ON! (Bug!) β β
β β β β
β β onClick={() => onOpen(num)} β β
β β β’ Click open item β stays open! β β
β β β’ Can't close by clicking again! β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β Smart switch (with memory): β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Is light ON? β β
β β β YES β Turn OFF β β
β β β NO β Turn ON β β
β β β β
β β onClick={() => onOpen(isOpen ? null : num)}β
β β β β
β β isOpen = true (I'm open): β β
β β β onOpen(null) β Close me! β β
β β β β
β β isOpen = false (I'm closed): β β
β β β onOpen(num) β Open me! β β
β β β β
β β null = "Nobody is open" β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β
β isOpen ? null : num β
β Translation: "If I'm open, close me. β
β If I'm closed, open me." β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
π Practice Exercises
Exercise 1: Identify Where State Should Live
Look at this component tree. Where should selected state live?
App
βββ Tabs
β βββ TabItem
β βββ TabItem
β βββ TabItem
βββ Content
Questions:
- Which component needs to know which tab is selected? β
TabsandContent - Are
TabItemcomponents siblings? β Yes! - What's their common parent? β
Tabs - Should each tab have its own state? β No! Only ONE can be selected!
Solution:
function Tabs({ data }) {
// π STATE LIVES IN PARENT (Tabs)
const [selected, setSelected] = useState(0);
return (
<div>
<div className="tab-list">
{data.map((item, index) => (
<TabItem
key={item.id}
num={index}
title={item.title}
selected={selected} // β Pass DOWN
onSelect={setSelected} // β Pass DOWN
/>
))}
</div>
<Content>{data[selected].content}</Content>
</div>
);
}
function TabItem({ num, title, selected, onSelect }) {
const isSelected = num === selected;
return (
<button
className={isSelected ? 'active' : ''}
onClick={() => onSelect(num)}
>
{title}
</button>
);
}
Exercise 2: Fix the Broken Accordion
This code has a bug - clicking an open item doesn't close it. Fix it!
function AccordionItem({ num, title, curOpen, onOpen, children }) {
const isOpen = num === curOpen;
// TODO: Fix the toggle logic
function handleToggle() {
onOpen(num); // β BUG: Always opens, never closes!
}
return (
<div onClick={handleToggle}>
<h3>{title} {isOpen ? '-' : '+'}</h3>
{isOpen && <p>{children}</p>}
</div>
);
}
Solution:
function AccordionItem({ num, title, curOpen, onOpen, children }) {
const isOpen = num === curOpen;
// β
FIXED: Toggle logic
function handleToggle() {
// If already open β close it (null)
// If closed β open it (num)
onOpen(isOpen ? null : num);
}
return (
<div onClick={handleToggle}>
<h3>{title} {isOpen ? '-' : '+'}</h3>
{isOpen && <p>{children}</p>}
</div>
);
}
Exercise 3: Build a Tab Component
Build a Tabs component where only ONE tab can be active:
function Tabs({ tabs }) {
// TODO: Add state for selected tab
// TODO: Render tab buttons
// TODO: Show content for selected tab
return (
<div>
{/* Your code here */}
</div>
);
}
// Usage:
const tabs = [
{ title: 'Home', content: 'Welcome home!' },
{ title: 'About', content: 'About us...' },
{ title: 'Contact', content: 'Contact us...' }
];
<Tabs tabs={tabs} />
Solution:
import { useState } from 'react';
function Tabs({ tabs }) {
const [selected, setSelected] = useState(0);
return (
<div className="card">
<div className="card-header">
<div className="btn-group">
{tabs.map((tab, index) => (
<button
key={tab.title}
className={`btn ${selected === index ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => setSelected(index)}
>
{tab.title}
</button>
))}
</div>
</div>
<div className="card-body">
<p>{tabs[selected].content}</p>
</div>
</div>
);
}
Exercise 4: Build a Modal with Children
Create a Modal component that shows/hides with children content:
function Modal({ isOpen, onClose, children }) {
// TODO: Return null if not open
// TODO: Show overlay with content
// TODO: Close button calls onClose
}
// Usage:
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<h2>Title</h2>
<p>Any content here!</p>
</Modal>
Solution:
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
return (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button className="btn-close" onClick={onClose}></button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>
</div>
);
}
π‘ Key Takeaways
| Concept | What It Means | Example |
|---|---|---|
| Lifting State Up | Moving state from child to parent | isOpen moved from AccordionItem to Accordion |
| Common Parent | First ancestor shared by all components that need the state | Accordion is parent of all AccordionItems |
| Props Down | Parent passes data to children | curOpen={curOpen} |
| Functions Down | Parent passes updater functions to children | onOpen={setCurOpen} |
| Derived State | Calculate local state from props | const isOpen = num === curOpen |
| Toggle Logic | If open β close, if closed β open | onOpen(isOpen ? null : num) |
| null | Represents "nothing selected" | curOpen = null means none open |
| Children Prop | Flexible content between tags | <AccordionItem>Any content</AccordionItem> |
The Lifting State Up Pattern:
function Parent() {
const [sharedState, setSharedState] = useState(null);
return (
<div>
<Child
id={1}
sharedState={sharedState}
onUpdate={setSharedState}
/>
<Child
id={2}
sharedState={sharedState}
onUpdate={setSharedState}
/>
</div>
);
}
function Child({ id, sharedState, onUpdate }) {
const isActive = id === sharedState;
return (
<div onClick={() => onUpdate(isActive ? null : id)}>
{isActive ? 'Active' : 'Inactive'}
</div>
);
}
Golden Rules:
- Lift state to common parent β When siblings need to share
- Pass state down as props β For children to READ
- Pass functions down as props β For children to WRITE
- Calculate local state from props β
const isOpen = num === curOpen - Use null for "nothing selected" β Clean initial state
- Toggle with ternary β
isOpen ? null : num - Children prop for flexible content β Any JSX inside
- One source of truth β State lives in ONE place only
- Props flow down, events flow up β Parent owns state, child calls functions
- If siblings need to sync, lift it up β Move to common ancestor
One Sentence Summary: > "Lifting state up in React means moving a piece of state from child components to their closest common parent component when multiple sibling components need to access or modify that same state, then passing the state down as props for children to read and passing updater functions down as props for children to call, ensuring there is only one source of truth that flows downward through the component tree while allowing children to communicate changes back up through function calls, with special toggle logic using null to represent 'nothing selected' and the children prop for flexible content!"