▶️ Live demo
Try it yourself — interact with the example below.
Loading demo…
🎯 What Is Sorting with Derived State?
Imagine you have a deck of cards and want to sort them different ways:
WITHOUT SORTING (Fixed Order):
┌─────────────────────────────────────────┐
│ 🎴 YOUR CARDS (Items Array) │
│ │
│ 7♠ 3♥ K♦ 2♣ A♠ │
│ │
│ "I want to see them by number..." │
│ "I want to see all red cards first..." │
│ "I can't! They're stuck in this order!"│
│ │
│ ❌ Array order is fixed! │
│ ❌ Can't reorder without changing state│
│ ❌ No way to view different orders! │
└─────────────────────────────────────────┘
WITH SORTING (Derived Order):
┌─────────────────────────────────────────┐
│ 🎴 YOUR CARDS (Items Array - Unchanged)│
│ │
│ Original: 7♠ 3♥ K♦ 2♣ A♠ │
│ │
│ Sort by Number: A♠ 2♣ 3♥ 7♠ K♦ │
│ Sort by Color: 3♥ K♦ 7♠ 2♣ A♠ │
│ Sort by Suit: 2♣ 3♥ 7♠ A♠ K♦ │
│ │
│ ✅ Original deck NEVER changes! │
│ ✅ Display order is CALCULATED! │
│ ✅ Switch views instantly! │
│ ✅ One source of truth! │
└─────────────────────────────────────────┘
THE SORTING DECISION FLOWCHART:
┌─────────────────────────────────────────┐
│ Need to sort a list? │
│ ↓ │
│ Should the original order be preserved? │
│ → YES → Use DERIVED STATE! │
│ → Create sortBy state (input, desc, │
│ packed) │
│ → Use slice().sort() to create NEW │
│ sorted array │
│ → Render sorted array, keep original │
│ untouched! │
│ → Switch sortBy → New sorted array! │
└─────────────────────────────────────────┘
⚠️ The Big Problem: "Mutating the Original Array"
// ==========================================
// THE "SORTED MY ORIGINAL DATA" TRAP
// ==========================================
// ❌ WRONG: Mutating the original items array!
function BadPackingList({ items }) {
const [sortBy, setSortBy] = useState("input");
// ❌ DANGER: items.sort() MUTATES the original array!
const sortedItems = items.sort((a, b) =>
a.description.localeCompare(b.description)
);
// items is now CHANGED in parent! 😱
// React might not detect the mutation!
// Parent and child are out of sync!
return (
<div className="list">
<ul>
{sortedItems.map(item => <Item key={item.id} item={item} />)}
</ul>
</div>
);
}
// Problems:
// 1. items.sort() MUTATES the original array!
// 2. Parent's state is corrupted!
// 3. React can't detect the mutation (same reference)!
// 4. Other components using items see wrong order!
// 5. Toggle/delete might break because IDs moved!
// ==========================================
// THE SOLUTION: slice().sort() (Immutable Sort)
// ==========================================
// ✅ CORRECT: Create a COPY before sorting!
function GoodPackingList({ items, onDeleteItem, onToggleItem }) {
const [sortBy, setSortBy] = useState("input");
// ✅ DERIVED STATE: Create NEW sorted array!
let sortedItems;
if (sortBy === "input") {
// Default: keep original order
sortedItems = items;
} else if (sortBy === "description") {
// Alphabetical: slice() first, then sort()
sortedItems = items.slice().sort((a, b) =>
a.description.localeCompare(b.description)
);
} else if (sortBy === "packed") {
// By packed status: slice() first, then sort()
sortedItems = items.slice().sort((a, b) =>
Number(a.packed) - Number(b.packed)
);
}
return (
<div className="list">
{/* ✅ Controlled select for sorting */}
<div className="actions">
<select
className="form-select"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="input">Sort by input order</option>
<option value="description">Sort by description</option>
<option value="packed">Sort by packed status</option>
</select>
</div>
<ul className="list-group">
{/* ✅ Render the SORTED items, original is SAFE! */}
{sortedItems.map(item => (
<Item
key={item.id}
item={item}
onDeleteItem={onDeleteItem}
onToggleItem={onToggleItem}
/>
))}
</ul>
</div>
);
}
// What happens when user selects "description":
//
// 1. User clicks select, chooses "description"
// 2. onChange fires: setSortBy("description")
// 3. PackingList re-renders
// 4. sortBy is now "description"
// 5. sortedItems = items.slice().sort(...)
// - items.slice() creates COPY: [Passports, Socks, Charger]
// - .sort() sorts the COPY alphabetically: [Charger, Passports, Socks]
// - Original items is UNCHANGED! ✓
// 6. Renders sorted items: C, P, S
//
// User selects "packed":
// 1. setSortBy("packed")
// 2. sortedItems = items.slice().sort(...)
// - items.slice() creates COPY
// - Number(false) - Number(true) = 0 - 1 = -1
// - false comes BEFORE true
// - Unpacked items first, packed items last!
// 3. Renders: Unpacked items first, then packed!
//
// Why this works:
// → Original items array NEVER changes! ✓
// → slice() creates a SHALLOW COPY! ✓
// → sort() mutates the COPY, not original! ✓
// → sortBy state controls which sort to use! ✓
// → Derived state = no extra state needed! ✓
📋 Complete Visual Examples
Create file: sorting-items.js
// ==========================================
// SORTING ITEMS - Complete Guide
// ==========================================
/*
THE SORTING DECISION FLOWCHART:
┌─────────────────────────────────────────┐
│ Need to sort a list? │
│ ↓ │
│ Is the original order important? │
│ → YES → Use DERIVED STATE! │
│ → Create sortBy state │
│ → Use slice().sort() for copy │
│ → NEVER mutate original array! │
│ → NO → Sort once, store in state │
│ │
│ THE THREE SORTING CRITERIA: │
│ ┌─────────────────────────────────┐ │
│ │ 1. INPUT ORDER (default) │ │
│ │ sortedItems = items │ │
│ │ (no sorting needed) │ │
│ │ │ │
│ │ 2. DESCRIPTION (alphabetical) │ │
│ │ items.slice().sort((a,b) => │ │
│ │ a.description.localeCompare │ │
│ │ (b.description)) │ │
│ │ │ │
│ │ 3. PACKED STATUS (boolean) │ │
│ │ items.slice().sort((a,b) => │ │
│ │ Number(a.packed) - │ │
│ │ Number(b.packed)) │ │
│ │ false (0) comes before │ │
│ │ true (1) → unpacked first! │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
slice().sort() EXPLAINED:
┌─────────────────────────────────────────┐
│ items.slice() = COPY of array │
│ .sort() = sorts the COPY │
│ │
│ Original: [A, B, C] │
│ Copy: [A, B, C] ← slice() │
│ Sorted: [B, A, C] ← sort() on copy │
│ Original: [A, B, C] ← UNCHANGED! ✓ │
│ │
│ ❌ items.sort() → MUTATES original! │
│ ✅ items.slice().sort() → SAFE! │
└─────────────────────────────────────────┘
*/
// ==========================================
// EXAMPLE 1: The Mutation Trap
// ==========================================
import { useState } from 'react';
function MutationTrapDemo() {
const [items, setItems] = useState([
{ id: 1, name: "Banana" },
{ id: 2, name: "Apple" },
{ id: 3, name: "Cherry" },
]);
function badSort() {
// ❌ WRONG: Mutates original!
const sorted = items.sort((a, b) =>
a.name.localeCompare(b.name)
);
// items is now [Apple, Banana, Cherry]!
// But React doesn't know it changed!
// Same reference = no re-render!
console.log("Original mutated:", items.map(i => i.name));
}
function goodSort() {
// ✅ CORRECT: Creates copy!
const sorted = items.slice().sort((a, b) =>
a.name.localeCompare(b.name)
);
// items is still [Banana, Apple, Cherry]!
// sorted is [Apple, Banana, Cherry]!
console.log("Original safe:", items.map(i => i.name));
console.log("Sorted copy:", sorted.map(i => i.name));
}
return (
<div className="container">
<button onClick={badSort} className="btn btn-danger">Bad Sort (Mutates)</button>
<button onClick={goodSort} className="btn btn-success">Good Sort (Copy)</button>
</div>
);
}
// Visual Flow:
// Original: [Banana, Apple, Cherry]
//
// ❌ badSort():
// items.sort() → MUTATES original!
// items becomes: [Apple, Banana, Cherry]
// React: "Same array reference, no re-render needed"
// UI: Still shows old order! BUG! 🐛
//
// ✅ goodSort():
// items.slice() → [Banana, Apple, Cherry] (copy)
// copy.sort() → [Apple, Banana, Cherry]
// items → [Banana, Apple, Cherry] (UNCHANGED!)
// React: "New array reference, re-render!"
// UI: Shows sorted order! ✓
// ==========================================
// EXAMPLE 2: The Three Sort Types
// ==========================================
function SortTypesDemo() {
const items = [
{ id: 1, description: "Socks", packed: false },
{ id: 2, description: "Passports", packed: true },
{ id: 3, description: "Charger", packed: false },
];
// 1. INPUT ORDER (default)
const byInput = items;
// Result: [Socks, Passports, Charger] (original order)
// 2. DESCRIPTION (alphabetical)
const byDescription = items.slice().sort((a, b) =>
a.description.localeCompare(b.description)
);
// How localeCompare works:
// "Charger".localeCompare("Passports") → negative (-1)
// "Charger" comes BEFORE "Passports"
// "Passports".localeCompare("Socks") → negative (-1)
// "Passports" comes BEFORE "Socks"
// Result: [Charger, Passports, Socks]
// 3. PACKED STATUS (boolean to number)
const byPacked = items.slice().sort((a, b) =>
Number(a.packed) - Number(b.packed)
);
// How boolean to number works:
// Number(false) = 0
// Number(true) = 1
// false - true = 0 - 1 = -1 (false comes first)
// true - false = 1 - 0 = 1 (true comes last)
// Result: [Socks(false), Charger(false), Passports(true)]
return (
<div className="container">
<h3>Sort Types</h3>
<p>By Input: {byInput.map(i => i.description).join(", ")}</p>
<p>By Description: {byDescription.map(i => i.description).join(", ")}</p>
<p>By Packed: {byPacked.map(i => `${i.description}(${i.packed})`).join(", ")}</p>
</div>
);
}
// ==========================================
// EXAMPLE 3: Controlled Select for Sorting
// ==========================================
function SortSelectDemo() {
const [sortBy, setSortBy] = useState("input");
// Three steps for controlled element:
// 1. State: const [sortBy, setSortBy] = useState("input")
// 2. Value: value={sortBy}
// 3. onChange: onChange={(e) => setSortBy(e.target.value)}
return (
<div className="container">
<select
className="form-select"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="input">Sort by input order</option>
<option value="description">Sort by description</option>
<option value="packed">Sort by packed status</option>
</select>
<p>Current sort: {sortBy}</p>
</div>
);
}
// Visual Flow:
// Initial: sortBy = "input"
// select shows: "Sort by input order"
//
// User selects "description":
// onChange fires
// e.target.value = "description"
// setSortBy("description")
// React re-renders
// sortBy = "description"
// select shows: "Sort by description"
// ==========================================
// EXAMPLE 4: Complete Sorting Logic
// ==========================================
function PackingList({ items, onDeleteItem, onToggleItem }) {
const [sortBy, setSortBy] = useState("input");
// ✅ DERIVED STATE: sortedItems based on sortBy
let sortedItems;
if (sortBy === "input") {
// Default: use original order
sortedItems = items;
} else if (sortBy === "description") {
// Alphabetical: slice + localeCompare
sortedItems = items.slice().sort((a, b) =>
a.description.localeCompare(b.description)
);
} else if (sortBy === "packed") {
// Packed status: slice + Number conversion
sortedItems = items.slice().sort((a, b) =>
Number(a.packed) - Number(b.packed)
);
}
return (
<div className="list">
{/* ✅ Controlled select */}
<div className="actions mb-3">
<select
className="form-select"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="input">Sort by input order</option>
<option value="description">Sort by description</option>
<option value="packed">Sort by packed status</option>
</select>
</div>
{/* ✅ Render sorted items */}
<ul className="list-group">
{sortedItems.map(item => (
<Item
key={item.id}
item={item}
onDeleteItem={onDeleteItem}
onToggleItem={onToggleItem}
/>
))}
</ul>
</div>
);
}
// Visual Flow of Sorting:
// 1. User loads page
// sortBy = "input"
// sortedItems = items (no sorting)
// Shows: [Passports, Socks, Charger] (original order)
//
// 2. User selects "description"
// setSortBy("description")
// sortedItems = items.slice().sort(...)
// Shows: [Charger, Passports, Socks] (A-Z)
//
// 3. User selects "packed"
// setSortBy("packed")
// sortedItems = items.slice().sort(...)
// Shows: [Socks(false), Charger(false), Passports(true)]
// Unpacked first, packed last!
//
// 4. User packs "Socks"
// onToggleItem fires
// items updates (Socks now packed: true)
// PackingList re-renders
// sortBy still "packed"
// sortedItems = items.slice().sort(...)
// Shows: [Charger(false), Socks(true), Passports(true)]
// Charger still unpacked, Socks moved to packed!
//
// Why this works:
// → Original items NEVER changes order! ✓
// → slice().sort() creates NEW array! ✓
// → sortBy controls which sort to apply! ✓
// → Derived state = no extra state! ✓
// → Toggle/delete still work on original IDs! ✓
// ==========================================
// EXAMPLE 5: The Decision Flowchart in Code
// ==========================================
function DecisionFlowchartDemo() {
const [items, setItems] = useState([]);
const [sortBy, setSortBy] = useState("input");
// Question 1: Need to sort items?
// → YES! User wants different views!
// Question 2: Should original order change?
// → NO! Keep original for toggle/delete!
// Question 3: Use derived state?
// → YES! Create sortBy state only!
// Question 4: How to sort immutably?
// → slice().sort()! Copy then sort!
// Question 5: What criteria?
// → input (none), description (localeCompare), packed (Number)
let sortedItems;
if (sortBy === "input") sortedItems = items;
else if (sortBy === "description") sortedItems = items.slice().sort((a, b) => a.description.localeCompare(b.description));
else if (sortBy === "packed") sortedItems = items.slice().sort((a, b) => Number(a.packed) - Number(b.packed));
return (
<div className="container">
<select className="form-select" value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="input">Sort by input order</option>
<option value="description">Sort by description</option>
<option value="packed">Sort by packed status</option>
</select>
</div>
);
}
🚀 Interactive React Usage Examples
Complete React File: SortingItemsMasterClass.jsx
import React, { useState } from 'react';
// ==========================================
// INTERACTIVE SORTING ITEMS DEMO
// ==========================================
function SortingItemsMasterClass() {
const [activeDemo, setActiveDemo] = useState('flowchart');
const demos = {
flowchart: { title: 'Decision Flowchart', component: <FlowchartDemo /> },
mutation: { title: 'Mutation Trap', component: <MutationTrapDemo /> },
sorttypes: { title: '3 Sort Types', component: <SortTypesDemo /> },
select: { title: 'Controlled Select', component: <SelectDemo /> },
complete: { title: 'Complete App', component: <CompleteAppDemo /> }
};
return (
<div className="container py-4">
<header className="bg-dark text-white p-4 rounded mb-4">
<h1 className="mb-0">🔄 Sorting Items</h1>
<p className="mb-0 opacity-75">Derived State with slice().sort()</p>
</header>
<nav className="d-flex gap-2 flex-wrap mb-4">
{Object.entries(demos).map(([key, { title }]) => (
<button
key={key}
onClick={() => setActiveDemo(key)}
className={`btn ${activeDemo === key ? 'btn-primary' : 'btn-outline-secondary'}`}
>
{title}
</button>
))}
</nav>
<main className="bg-light p-4 rounded min-vh-50">
{demos[activeDemo].component}
</main>
</div>
);
}
// ==========================================
// DEMO 1: The Decision Flowchart
// ==========================================
function FlowchartDemo() {
const [step, setStep] = useState(1);
const steps = [
{ num: 1, title: 'Need to Sort?', question: 'Do you need to sort a list?', code: '// User wants different views', visual: '🔄', desc: 'Input order, alphabetical, by status...' },
{ num: 2, title: 'Preserve Original?', question: 'Is original order important?', code: '// For toggle, delete, add', visual: '💾', desc: 'YES! Never mutate original!' },
{ num: 3, title: 'Create sortBy State', question: 'What sorting criteria?', code: 'const [sortBy, setSortBy] = useState("input")', visual: '🎛️', desc: 'Only ONE piece of state needed!' },
{ num: 4, title: 'slice().sort()', question: 'How to sort immutably?', code: 'items.slice().sort((a,b) => ...)', visual: '📋', desc: 'Copy first, then sort the copy!' },
{ num: 5, title: 'Derive sortedItems', question: 'Calculate based on sortBy?', code: 'if (sortBy === "desc") items.slice().sort(...)', visual: '🧮', desc: 'Derived state! No extra useState!' }
];
const current = steps[step - 1];
return (
<div>
<h2>The Sorting Flowchart 🧭</h2>
<div className="d-flex gap-2 justify-content-center mb-4">
{steps.map(s => (
<button
key={s.num}
onClick={() => setStep(s.num)}
className={`btn rounded-circle ${step >= s.num ? 'btn-primary' : 'btn-outline-secondary'}`}
style={{ width: '60px', height: '60px', fontSize: '24px' }}
>
{s.num}
</button>
))}
</div>
<div className="card border-primary">
<div className="card-body">
<h3 className="card-title text-primary">Step {current.num}: {current.title}</h3>
<p className="fs-5 fw-bold">{current.question}</p>
<div className="row">
<div className="col-md-8">
<pre className="bg-dark text-light p-3 rounded">{current.code}</pre>
<p className="fs-5">{current.desc}</p>
</div>
<div className="col-md-4 d-flex align-items-center justify-content-center">
<span className="display-1">{current.visual}</span>
</div>
</div>
</div>
</div>
<div className="alert alert-success mt-3">
<h4>🧠 Memory Trick:</h4>
<p>"slice() makes a photocopy, sort() shuffles the copy, original stays safe in the drawer!"</p>
</div>
</div>
);
}
// ==========================================
// DEMO 2: Mutation Trap
// ==========================================
function MutationTrapDemo() {
const [items, setItems] = useState([
{ id: 1, name: "Banana" },
{ id: 2, name: "Apple" },
{ id: 3, name: "Cherry" },
]);
const [logs, setLogs] = useState([]);
function addLog(message) {
setLogs(prev => [...prev, message]);
}
function badSort() {
const originalOrder = items.map(i => i.name).join(", ");
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
const newOrder = items.map(i => i.name).join(", ");
addLog(`❌ BAD SORT:`);
addLog(`Before: ${originalOrder}`);
addLog(`After: ${newOrder}`);
addLog(`Original mutated? ${originalOrder !== newOrder ? 'YES! BUG!' : 'No'}`);
}
function goodSort() {
const originalOrder = items.map(i => i.name).join(", ");
const sorted = items.slice().sort((a, b) => a.name.localeCompare(b.name));
const newOrder = items.map(i => i.name).join(", ");
addLog(`✅ GOOD SORT:`);
addLog(`Before: ${originalOrder}`);
addLog(`After: ${newOrder}`);
addLog(`Original mutated? ${originalOrder !== newOrder ? 'YES! BUG!' : 'No, safe!'}`);
}
function reset() {
setItems([
{ id: 1, name: "Banana" },
{ id: 2, name: "Apple" },
{ id: 3, name: "Cherry" },
]);
setLogs([]);
}
return (
<div>
<h2>The Mutation Trap 🪤</h2>
<div className="alert alert-danger">
<h4>items.sort() MUTATES the original array! slice().sort() is safe!</h4>
</div>
<div className="btn-group mb-3">
<button onClick={badSort} className="btn btn-danger">❌ items.sort()</button>
<button onClick={goodSort} className="btn btn-success">✅ items.slice().sort()</button>
<button onClick={reset} className="btn btn-secondary">Reset</button>
</div>
<div className="card">
<div className="card-body">
<h5>Current items order: {items.map(i => i.name).join(", ")}</h5>
</div>
</div>
<div className="mt-3">
<h5>Logs:</h5>
{logs.map((log, i) => (
<div key={i} className={`alert ${log.includes('❌') ? 'alert-danger' : log.includes('✅') ? 'alert-success' : 'alert-light'}`}>
{log}
</div>
))}
</div>
</div>
);
}
// ==========================================
// DEMO 3: Three Sort Types
// ==========================================
function SortTypesDemo() {
const items = [
{ id: 1, description: "Socks", packed: false },
{ id: 2, description: "Passports", packed: true },
{ id: 3, description: "Charger", packed: false },
];
const byInput = items;
const byDescription = items.slice().sort((a, b) => a.description.localeCompare(b.description));
const byPacked = items.slice().sort((a, b) => Number(a.packed) - Number(b.packed));
return (
<div>
<h2>Three Sort Types 🔄</h2>
<div className="alert alert-info">
<h4>Original items NEVER change! Each sort creates a new view!</h4>
</div>
<div className="row">
<div className="col-md-4">
<div className="card mb-3">
<div className="card-header bg-secondary text-white">1. Input Order</div>
<div className="card-body">
<code>sortedItems = items</code>
<ul className="list-group mt-2">
{byInput.map(item => (
<li key={item.id} className="list-group-item">
{item.description} {item.packed ? '✅' : '⬜'}
</li>
))}
</ul>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card mb-3">
<div className="card-header bg-primary text-white">2. Description</div>
<div className="card-body">
<code>items.slice().sort((a,b) => a.description.localeCompare(b.description))</code>
<ul className="list-group mt-2">
{byDescription.map(item => (
<li key={item.id} className="list-group-item">
{item.description} {item.packed ? '✅' : '⬜'}
</li>
))}
</ul>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card mb-3">
<div className="card-header bg-success text-white">3. Packed Status</div>
<div className="card-body">
<code>items.slice().sort((a,b) => Number(a.packed) - Number(b.packed))</code>
<ul className="list-group mt-2">
{byPacked.map(item => (
<li key={item.id} className="list-group-item">
{item.description} {item.packed ? '✅' : '⬜'}
</li>
))}
</ul>
</div>
</div>
</div>
</div>
<div className="alert alert-warning">
<h5>How Number(boolean) works:</h5>
<p className="mb-0">Number(false) = 0, Number(true) = 1</p>
<p className="mb-0">false - true = 0 - 1 = -1 → false comes FIRST</p>
<p className="mb-0">true - false = 1 - 0 = 1 → true comes LAST</p>
<p className="mb-0 fw-bold">Result: Unpacked items first, packed items last!</p>
</div>
</div>
);
}
// ==========================================
// DEMO 4: Controlled Select
// ==========================================
function SelectDemo() {
const [sortBy, setSortBy] = useState("input");
return (
<div>
<h2>Controlled Select 🎛️</h2>
<div className="alert alert-info">
<h4>Three steps: useState, value, onChange</h4>
</div>
<div className="card">
<div className="card-body">
<select
className="form-select"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="input">Sort by input order</option>
<option value="description">Sort by description</option>
<option value="packed">Sort by packed status</option>
</select>
<div className="mt-3">
<p><strong>Current sortBy state:</strong> <code>{sortBy}</code></p>
<p><strong>Visual:</strong></p>
<div className="progress">
<div
className="progress-bar"
role="progressbar"
style={{ width: sortBy === "input" ? '33%' : sortBy === "description" ? '66%' : '100%' }}
>
{sortBy}
</div>
</div>
</div>
</div>
</div>
<div className="alert alert-success mt-3">
<h5>The Recipe:</h5>
<ol className="mb-0">
<li><code>const [sortBy, setSortBy] = useState("input")</code></li>
<li><code>value={'{sortBy}'}</code> — React controls the select</li>
<li><code>onChange={'{(e) => setSortBy(e.target.value)}'}</code> — Updates state</li>
</ol>
</div>
</div>
);
}
// ==========================================
// DEMO 5: Complete Far Away App
// ==========================================
function CompleteAppDemo() {
const [items, setItems] = useState([
{ id: 1, description: "Passports", quantity: 2, packed: false },
{ id: 2, description: "Socks", quantity: 6, packed: false },
{ id: 3, description: "Charger", quantity: 1, packed: true },
]);
const [sortBy, setSortBy] = useState("input");
function handleToggle(id) {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, packed: !item.packed } : item
));
}
function handleDelete(id) {
setItems(prev => prev.filter(item => item.id !== id));
}
function handleAdd() {
const descriptions = ["Toothbrush", "Shampoo", "Towel", "Book", "Snacks"];
const random = descriptions[Math.floor(Math.random() * descriptions.length)];
setItems(prev => [...prev, {
id: Date.now(),
description: random,
quantity: Math.floor(Math.random() * 3) + 1,
packed: false
}]);
}
// ✅ DERIVED: Sort items based on sortBy
let sortedItems;
if (sortBy === "input") {
sortedItems = items;
} else if (sortBy === "description") {
sortedItems = items.slice().sort((a, b) => a.description.localeCompare(b.description));
} else if (sortBy === "packed") {
sortedItems = items.slice().sort((a, b) => Number(a.packed) - Number(b.packed));
}
const numItems = items.length;
const numPacked = items.filter(i => i.packed).length;
const percentage = numItems > 0 ? Math.round((numPacked / numItems) * 100) : 0;
return (
<div>
<h2>Complete Far Away App 🌴</h2>
<div className="card shadow">
<div className="card-header bg-dark text-white text-center">
<h1 className="h3 mb-0">🌴 Far Away 💼</h1>
</div>
<div className="card-body">
<div className="d-flex gap-2 mb-3">
<button onClick={handleAdd} className="btn btn-success">➕ Add Random Item</button>
<select
className="form-select w-auto"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="input">Sort by input order</option>
<option value="description">Sort by description</option>
<option value="packed">Sort by packed status</option>
</select>
</div>
<ul className="list-group">
{sortedItems.map(item => (
<li key={item.id} className={`list-group-item d-flex align-items-center ${item.packed ? 'list-group-item-success' : ''}`}>
<input
className="form-check-input me-3"
type="checkbox"
checked={item.packed}
onChange={() => handleToggle(item.id)}
/>
<span className={`flex-grow-1 ${item.packed ? 'text-decoration-line-through text-muted' : ''}`}>
{item.quantity} {item.description}
</span>
<button onClick={() => handleDelete(item.id)} className="btn btn-sm btn-danger">
❌
</button>
</li>
))}
</ul>
</div>
<div className="card-footer bg-primary text-white text-center">
{numItems === 0 ? (
<em>Start adding some items to your packing list 🚀</em>
) : percentage === 100 ? (
<em>You got everything! Ready to go! ✈️</em>
) : (
<em>💼 You have {numItems} items, packed {numPacked} ({percentage}%)</em>
)}
</div>
</div>
</div>
);
}
export default SortingItemsMasterClass;
🧠 Memory Aids for Poor Logic Thinking
The "Library Books" Analogy
┌─────────────────────────────────────────────────┐
│ SORTING = REARRANGING LIBRARY BOOKS │
│ │
│ You have books on a shelf (original array): │
│ ┌─────────────────────────────────────────┐ │
│ │ 📚 Shelf Order (Original) │ │
│ │ 1. Harry Potter │ │
│ │ 2. Lord of the Rings │ │
│ │ 3. The Hobbit │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ❌ WRONG: Rearranging the actual shelf! │
│ ┌─────────────────────────────────────────┐ │
│ │ 🔧 You physically move books! │ │
│ │ ❌ Shelf is changed forever! │ │
│ │ ❌ Other people looking for "Harry │ │
│ │ Potter" can't find it! │ │
│ │ ❌ Original order LOST! │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ✅ CORRECT: Making a catalog list! │
│ ┌─────────────────────────────────────────┐ │
│ │ 📋 Catalog List (Derived/Sorted) │ │
│ │ │ │
│ │ By Title (A-Z): │ │
│ │ 1. Harry Potter │ │
│ │ 2. The Hobbit │ │
│ │ 3. Lord of the Rings │ │
│ │ │ │
│ │ By Read Status: │ │
│ │ 1. The Hobbit (unread) │ │
│ │ 2. Harry Potter (read) │ │
│ │ 3. Lord of the Rings (read) │ │
│ │ │ │
│ │ ✅ Shelf is UNCHANGED! │ │
│ │ ✅ Multiple catalogs possible! │ │
│ │ ✅ Original order preserved! │ │
│ └─────────────────────────────────────────┘ │
│ │
│ slice() = Photocopy the shelf list │
│ sort() = Rearrange the photocopy │
│ Original shelf list stays in the drawer! │
│ │
│ Memory Trick: │
│ "Don't rearrange the shelf, make a sorted │
│ catalog! The books stay where they are!" │
└─────────────────────────────────────────────────┘
The "Photocopy and Highlighter" Analogy
┌─────────────────────────────────────────────────┐
│ slice().sort() = PHOTOCOPY + HIGHLIGHTER │
│ │
│ You have an important document: │
│ ┌─────────────────────────────────────────┐ │
│ │ 📄 ORIGINAL DOCUMENT (items array) │ │
│ │ • Alice - Not packed │ │
│ │ • Bob - Packed │ │
│ │ • Charlie - Not packed │ │
│ │ │ │
│ │ This is the LEGAL ORIGINAL! │ │
│ │ NEVER write on this! │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ❌ WRONG: Writing on the original! │
│ ┌─────────────────────────────────────────┐ │
│ │ ✏️ You crossed out and rewrote! │ │
│ │ ❌ Original is DESTROYED! │ │
│ │ ❌ Can't prove what it originally said! │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ✅ CORRECT: Photocopy, then mark up! │
│ ┌─────────────────────────────────────────┐ │
│ │ 📠 STEP 1: slice() = PHOTOCOPY │ │
│ │ • Alice - Not packed │ │
│ │ • Bob - Packed │ │
│ │ • Charlie - Not packed │ │
│ │ (Exact copy, original safe!) │ │
│ │ │ │
│ │ 🖍️ STEP 2: sort() = REARRANGE COPY │ │
│ │ • Alice - Not packed │ │
│ │ • Charlie - Not packed │ │
│ │ • Bob - Packed │ │
│ │ (Sorted by packed status!) │ │
│ │ │ │
│ │ 📄 ORIGINAL STILL SAYS: │ │
│ │ • Alice - Not packed │ │
│ │ • Bob - Packed │ │
│ │ • Charlie - Not packed │ │
│ │ (Unchanged! Safe in filing cabinet!) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Memory Trick: │
│ "slice() is the photocopier, sort() is your │
│ pen. Write on the copy, never the original!" │
└─────────────────────────────────────────────────┘
The "Train Cars" Analogy for Boolean Sort
┌─────────────────────────────────────────────────┐
│ BOOLEAN SORT = ORDERING TRAIN CARS │
│ │
│ You have train cars with flags: │
│ ┌─────────────────────────────────────────┐ │
│ │ 🚃 Car 1: EMPTY (packed: false) │ │
│ │ 🚃 Car 2: FULL (packed: true) │ │
│ │ 🚃 Car 3: EMPTY (packed: false) │ │
│ │ 🚃 Car 4: FULL (packed: true) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ You want EMPTY cars first, FULL cars last: │
│ │
│ How Number(boolean) works: │
│ ┌─────────────────────────────────────────┐ │
│ │ Number(false) = 0 ← EMPTY car weight │ │
│ │ Number(true) = 1 ← FULL car weight │ │
│ │ │ │
│ │ Comparing two cars: │ │
│ │ EMPTY(0) - FULL(1) = -1 │ │
│ │ → Negative = EMPTY comes FIRST! ✓ │ │
│ │ │ │
│ │ FULL(1) - EMPTY(0) = 1 │ │
│ │ → Positive = FULL comes LAST! ✓ │ │
│ │ │ │
│ │ EMPTY(0) - EMPTY(0) = 0 │ │
│ │ → Zero = Same weight, keep order! │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Result: [EMPTY, EMPTY, FULL, FULL] │
│ Unpacked items first, packed items last! │
│ │
│ Memory Trick: │
│ "false is light (0), true is heavy (1). │
│ Light things float to the top!" │
└─────────────────────────────────────────────────┘
The "Recipe" Analogy for Controlled Select
┌─────────────────────────────────────────────────┐
│ CONTROLLED SELECT = FOLLOWING A RECIPE │
│ │
│ You want to bake a cake: │
│ │
│ ❌ UNCONTROLLED (Winging it): │
│ ┌─────────────────────────────────────────┐ │
│ │ 🎂 "I'll just throw things in!" │ │
│ │ ❌ No measuring! │ │
│ │ ❌ No recipe! │ │
│ │ ❌ Cake might be terrible! │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ✅ CONTROLLED (Following recipe): │
│ ┌─────────────────────────────────────────┐ │
│ │ 📖 RECIPE (useState): │ │
│ │ Step 1: Preheat to 350°F │ │
│ │ │ │
│ │ 🌡️ OVEN (value prop): │ │
│ │ Currently set to: 350°F │ │
│ │ │ │
│ │ 👨🍳 CHEF (onChange): │ │
│ │ "I want to change to 375°F" │ │
│ │ → Updates recipe! │ │
│ │ → Oven adjusts! │ │
│ │ │ │
│ │ ✅ Recipe and oven ALWAYS match! │ │
│ └─────────────────────────────────────────┘ │
│ │
│ In React: │
│ const [sortBy, setSortBy] = useState("input") │
│ <select value={sortBy} onChange={...}> │
│ │
│ Recipe (state) controls the oven (select)! │
│ Chef (user) updates the recipe! │
│ │
│ Memory Trick: │
│ "The recipe (state) is the boss. The oven │
│ (select) follows the recipe. The chef (user) │
│ changes the recipe, not the oven directly!" │
└─────────────────────────────────────────────────┘
🎓 Practice Exercises
Exercise 1: Identify the Bug
Look at this code. Why is it dangerous?
function PackingList({ items }) {
const [sortBy, setSortBy] = useState("input");
// What's wrong here?
const sortedItems = items.sort((a, b) =>
a.description.localeCompare(b.description)
);
return (
<ul>
{sortedItems.map(item => <li key={item.id}>{item.description}</li>)}
</ul>
);
}
Questions:
- What does
items.sort()do? → Mutates the ORIGINAL array! - Who owns
items? → The parent component! - What happens to parent's state? → It's corrupted!
- How to fix? → Use
items.slice().sort(...)!
Solution:
function PackingList({ items }) {
const [sortBy, setSortBy] = useState("input");
let sortedItems;
if (sortBy === "input") {
sortedItems = items; // No sorting needed
} else {
sortedItems = items.slice().sort((a, b) => // ← COPY first!
a.description.localeCompare(b.description)
);
}
return (
<ul>
{sortedItems.map(item => <li key={item.id}>{item.description}</li>)}
</ul>
);
}
Exercise 2: Build a Sortable Table
Build a table that sorts by name, age, or active status:
function UserTable({ users }) {
const [sortBy, setSortBy] = useState("name");
// Fill in the sorting logic!
let sortedUsers = users;
// Your code here...
return (
<div>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="name">Sort by Name</option>
<option value="age">Sort by Age</option>
<option value="active">Sort by Active</option>
</select>
<table>
{/* Render sorted users */}
</table>
</div>
);
}
Solution:
function UserTable({ users }) {
const [sortBy, setSortBy] = useState("name");
let sortedUsers;
if (sortBy === "name") {
sortedUsers = users.slice().sort((a, b) =>
a.name.localeCompare(b.name)
);
} else if (sortBy === "age") {
sortedUsers = users.slice().sort((a, b) => a.age - b.age);
} else if (sortBy === "active") {
sortedUsers = users.slice().sort((a, b) =>
Number(b.active) - Number(a.active) // Active first!
);
}
return (
<div className="container">
<select
className="form-select mb-3"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="name">Sort by Name</option>
<option value="age">Sort by Age</option>
<option value="active">Sort by Active</option>
</select>
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>Active</th>
</tr>
</thead>
<tbody>
{sortedUsers.map(user => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.age}</td>
<td>{user.active ? '✅' : '❌'}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Exercise 3: The Complete Decision Flowchart
Walk through the flowchart for adding sorting:
Questions:
- Do we need to sort? → Yes! User wants different views!
- Should original order change? → No! Keep for toggle/delete!
- What state do we need? → Only
sortBy! - How to sort immutably? →
slice().sort()! - What are the criteria? → input, description, packed!
- How to render? → Use
sortedItemsinstead ofitems!
Solution:
function PackingList({ items, onToggleItem, onDeleteItem }) {
const [sortBy, setSortBy] = useState("input");
// ✅ DERIVED: sortedItems based on sortBy
let sortedItems;
if (sortBy === "input") {
sortedItems = items;
} else if (sortBy === "description") {
sortedItems = items.slice().sort((a, b) =>
a.description.localeCompare(b.description)
);
} else if (sortBy === "packed") {
sortedItems = items.slice().sort((a, b) =>
Number(a.packed) - Number(b.packed)
);
}
return (
<div className="list">
<div className="actions mb-3">
<select
className="form-select"
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
>
<option value="input">Sort by input order</option>
<option value="description">Sort by description</option>
<option value="packed">Sort by packed status</option>
</select>
</div>
<ul className="list-group">
{sortedItems.map(item => (
<li key={item.id} className="list-group-item">
<input
type="checkbox"
checked={item.packed}
onChange={() => onToggleItem(item.id)}
/>
<span className={item.packed ? 'text-decoration-line-through' : ''}>
{item.description}
</span>
<button onClick={() => onDeleteItem(item.id)}>❌</button>
</li>
))}
</ul>
</div>
);
}
💡 Key Takeaways
| Concept | What It Means | Example |
|---|---|---|
| Derived State | Calculate from existing state | sortedItems from items + sortBy |
| slice().sort() | Immutable sort pattern | items.slice().sort((a,b) => ...) |
| Mutable Sort | ❌ DANGER! Changes original | items.sort() — NEVER use! |
| localeCompare | Alphabetical string comparison | "Apple".localeCompare("Banana") |
| Number(boolean) | Convert true/false to 1/0 | Number(false) = 0, Number(true) = 1 |
| Controlled Select | React owns the dropdown | value={sortBy} onChange={...} |
| sortBy State | Only state needed for sorting | const [sortBy, setSortBy] = useState("input") |
The Sorting Pattern:
function SortingPattern() {
const [items, setItems] = useState([]);
const [sortBy, setSortBy] = useState("input"); // ← Only state needed!
// Step 1: Derive sorted array
let sortedItems;
if (sortBy === "input") {
sortedItems = items; // No sort
} else if (sortBy === "description") {
sortedItems = items.slice().sort((a, b) => // ← slice() first!
a.description.localeCompare(b.description)
);
} else if (sortBy === "packed") {
sortedItems = items.slice().sort((a, b) =>
Number(a.packed) - Number(b.packed)
);
}
// Step 2: Controlled select
// Step 3: Render sortedItems instead of items
}
Golden Rules:
- Never mutate the original array! — Always
slice()first! - Use derived state for sorting — Only
sortByneedsuseState - Keep original array unchanged — For toggle, delete, add to work
- Controlled select pattern —
useState+value+onChange localeComparefor strings — Handles alphabet correctlyNumber(boolean)for booleans —false(0)beforetrue(1)- Render
sortedItems, notitems— Show the sorted view
One Sentence Summary:
> "Sorting items in React using derived state involves creating a single sortBy state variable to track the selected sorting criteria, then deriving a new sorted array by conditionally applying slice().sort() with the appropriate comparison function — such as localeCompare for alphabetical sorting or Number() conversion for boolean sorting — while never mutating the original items array, ensuring that the source data remains intact for other operations like toggling and deleting, and rendering the derived sorted array instead of the original to display the reordered list to the user!"