Intermediate
This intermediate tutorial covers production-ready React + TypeScript patterns through 25 heavily annotated examples. Each example maintains 1-2.25 comment lines per code line to ensure deep understanding. You’ll master advanced hooks, custom hooks, Context API, React Query, routing, and error handling.
Prerequisites
Before starting, ensure you understand:
- React fundamentals (components, props, state, basic hooks)
- TypeScript basics (types, interfaces, generics)
- Asynchronous JavaScript (Promises, async/await)
- Form handling and controlled components
- Event handling patterns
If you need to review fundamentals, see Beginner.
Group 1: Advanced Hooks (5 examples)
Example 1: useReducer for Complex State
useReducer manages complex state with multiple related values. It accepts reducer function and initial state, returns current state and dispatch function.
import { useReducer } from 'react';
// => Define action types as discriminated union
// => Ensures type safety for all actions
type Action =
| { type: 'increment'; payload: number }
| { type: 'decrement'; payload: number }
| { type: 'reset' }
| { type: 'set'; payload: number };
// => State interface
interface CounterState {
count: number; // => Current count value
history: number[]; // => Array of previous values
}
// => Reducer function: (state, action) => newState
// => Pure function - no side effects
function counterReducer(state: CounterState, action: Action): CounterState {
switch (action.type) {
case 'increment':
// => Create new state with incremented count
return {
count: state.count + action.payload,
history: [...state.history, state.count + action.payload]
// => Spread existing history, append new count
};
case 'decrement':
return {
count: state.count - action.payload,
history: [...state.history, state.count - action.payload]
};
case 'reset':
// => Reset to initial state
return {
count: 0,
history: [0]
// => Start fresh history with 0
};
case 'set':
// => Set to specific value
return {
count: action.payload,
history: [...state.history, action.payload]
};
default:
// => TypeScript ensures exhaustive checking
// => All action types must be handled
return state;
}
}
function DonationCounter() {
// => useReducer(reducer, initialState) returns [state, dispatch]
const [state, dispatch] = useReducer(counterReducer, {
count: 0, // => Initial count is 0
history: [0] // => History starts with [0]
});
// => dispatch triggers state transition via reducer
// => TypeScript validates action types
// => Dispatch actions with type and optional payload
const handleDonate10 = () => {
dispatch({ type: 'increment', payload: 10 });
// => Calls reducer with current state and action
// => Reducer returns new state
// => Component re-renders with updated state
};
const handleUndo = () => {
if (state.history.length > 1) {
// => Get previous value from history
const previousValue = state.history[state.history.length - 2];
dispatch({ type: 'set', payload: previousValue });
}
};
return (
<div>
<h2>Total Donations: ${state.count}</h2>
{/* => Displays current count from state */}
<div>
<button onClick={handleDonate10}>Donate \$10</button>
<button onClick={() => dispatch({ type: 'increment', payload: 25 })}>
Donate \$25
</button>
{/* => Inline dispatch with literal action object */}
<button onClick={() => dispatch({ type: 'decrement', payload: 5 })}>
Refund \$5
</button>
<button onClick={handleUndo}>
Undo Last
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
</div>
<div>
<h3>History</h3>
<ul>
{state.history.map((value, index) => (
<li key={index}>
{/* => Using index as key (OK since history only appends) */}
Step {index}: ${value}
</li>
))}
</ul>
</div>
</div>
);
}
export default DonationCounter;Key Takeaway: Use useReducer for complex state with multiple related values and transitions. Reducer function centralizes state logic. Discriminated unions provide type-safe actions.
Expected Output: Donation counter starting at $0. Buttons add/subtract amounts and update history list. Undo button reverts to previous value. Reset clears to $0.
Common Pitfalls: Mutating state in reducer (must return new state), forgetting to handle all action types (TypeScript helps), or using useReducer for simple state (useState simpler).
Example 2: useCallback for Function Memoization
useCallback memoizes function references to prevent unnecessary child re-renders. Essential when passing callbacks to optimized child components.
import { useState, useCallback, memo } from 'react';
// => Memoized child component using React.memo
// => Only re-renders when props change by reference
interface TodoItemProps {
todo: { id: number; text: string };
onDelete: (id: number) => void; // => Callback prop
}
const TodoItem = memo(({ todo, onDelete }: TodoItemProps) => {
console.log('TodoItem rendered:', todo.text);
// => Logs on every render
// => Without useCallback, logs every time parent re-renders
return (
<li>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});
// => React.memo wraps component
// => Shallow comparison of props prevents unnecessary renders
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Read Quran' },
{ id: 2, text: 'Pray Fajr' }
]);
const [count, setCount] = useState(0); // => Unrelated state
// => WITHOUT useCallback (inefficient)
// => New function created on every render
const handleDeleteWrong = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// => Even if todos don't change, function reference changes
// => TodoItem re-renders unnecessarily
// => WITH useCallback (efficient)
// => Function reference stays same unless dependencies change
const handleDelete = useCallback((id: number) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
// => Functional update avoids dependency on todos
// => Dependencies: [] (empty) - function never recreated
}, []);
// => Empty dependency array: function memoized permanently
// => Same function reference on every render
// => useCallback with dependencies
const handleDeleteWithDep = useCallback((id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
// => Uses todos from closure
// => Must include todos in dependencies
}, [todos]);
// => Function recreated only when todos changes
// => New reference triggers TodoItem re-render
return (
<div>
<h2>Todos</h2>
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete}
{/* => Passes memoized callback */}
{/* => Same reference on every render */}
{/* => TodoItem doesn't re-render unless todo changes */}
/>
))}
</ul>
<div>
<p>Unrelated Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment Counter
</button>
{/* => Clicking this re-renders TodoList */}
{/* => Without useCallback, all TodoItems re-render */}
{/* => With useCallback, TodoItems don't re-render */}
</div>
</div>
);
}
export default TodoList;Key Takeaway: Use useCallback to memoize functions passed to optimized child components. Prevents child re-renders when parent re-renders. Use functional state updates to avoid dependencies.
Expected Output: Todo list with two items and counter. Clicking “Increment Counter” re-renders parent but not todo items (check console logs). Delete button removes todos.
Common Pitfalls: Overusing useCallback (premature optimization), including unnecessary dependencies (defeats memoization), or forgetting React.memo on child (useCallback has no effect).
Example 3: useMemo for Value Memoization
useMemo memoizes expensive computations. Recomputes only when dependencies change.
import { useState, useMemo } from 'react';
// => Type for Murabaha contract (Islamic financing)
interface MurabahaContract {
id: number;
principalAmount: number; // => Original amount
profitRate: number; // => Profit rate (percentage)
termMonths: number; // => Loan term in months
}
function MurabahaCalculator() {
const [contracts, setContracts] = useState<MurabahaContract[]>([
{ id: 1, principalAmount: 100000, profitRate: 5, termMonths: 12 },
{ id: 2, principalAmount: 50000, profitRate: 4, termMonths: 24 },
{ id: 3, principalAmount: 200000, profitRate: 6, termMonths: 36 }
]);
const [filterTerm, setFilterTerm] = useState<number>(0);
const [count, setCount] = useState(0); // => Unrelated state
// => EXPENSIVE computation WITHOUT useMemo
// => Recalculates on EVERY render (even when contracts unchanged)
const totalWithoutMemo = contracts.reduce((sum, contract) => {
console.log('Calculating without memo for contract:', contract.id);
// => This logs even when only count changes!
const totalPayment = contract.principalAmount * (1 + contract.profitRate / 100);
return sum + totalPayment;
}, 0);
// => EXPENSIVE computation WITH useMemo
// => Recalculates ONLY when contracts changes
const totalWithMemo = useMemo(() => {
console.log('Calculating WITH memo (only when contracts change)');
// => This logs only when contracts changes
// => Not logged when count or filterTerm changes
return contracts.reduce((sum, contract) => {
const totalPayment = contract.principalAmount * (1 + contract.profitRate / 100);
// => Principal + profit
return sum + totalPayment;
}, 0);
}, [contracts]);
// => Dependency array: [contracts]
// => Memoized result returned if contracts unchanged
// => Filtered contracts (also expensive)
const filteredContracts = useMemo(() => {
console.log('Filtering contracts (memoized)');
// => Logs only when contracts or filterTerm changes
if (filterTerm === 0) return contracts;
return contracts.filter(c => c.termMonths === filterTerm);
// => Filter by term length
}, [contracts, filterTerm]);
// => Dependencies: contracts AND filterTerm
// => Recomputes if either changes
// => Calculate statistics from memoized result
const averageProfit = useMemo(() => {
if (filteredContracts.length === 0) return 0;
const totalProfit = filteredContracts.reduce((sum, contract) => {
const profit = contract.principalAmount * (contract.profitRate / 100);
return sum + profit;
}, 0);
return totalProfit / filteredContracts.length;
// => Average profit per contract
}, [filteredContracts]);
// => Depends on filtered result (also memoized)
// => Chained memoization
return (
<div>
<h2>Murabaha Contract Calculator</h2>
<div>
<label>Filter by term (months): </label>
<select
value={filterTerm}
onChange={(e) => setFilterTerm(Number(e.target.value))}
>
<option value={0}>All</option>
<option value={12}>12 months</option>
<option value={24}>24 months</option>
<option value={36}>36 months</option>
</select>
{/* => Changing filter triggers filteredContracts recalculation */}
</div>
<div>
<h3>Statistics</h3>
<p>Total Contracts: {filteredContracts.length}</p>
<p>Total Payment (with profit): ${totalWithMemo.toFixed(2)}</p>
<p>Average Profit per Contract: ${averageProfit.toFixed(2)}</p>
</div>
<div>
<h3>Contracts</h3>
<ul>
{filteredContracts.map(contract => (
<li key={contract.id}>
ID: {contract.id} - Principal: ${contract.principalAmount} -
Profit Rate: {contract.profitRate}% - Term: {contract.termMonths} months
</li>
))}
</ul>
</div>
<div>
<p>Unrelated Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment Counter
</button>
{/* => Clicking this re-renders component */}
{/* => Without useMemo, calculations re-run */}
{/* => With useMemo, calculations use cached result */}
{/* => Check console logs to see difference */}
</div>
</div>
);
}
export default MurabahaCalculator;Key Takeaway: Use useMemo to memoize expensive computations. Prevents recalculation when dependencies unchanged. Essential for derived state from large datasets or complex calculations.
Expected Output: Murabaha calculator showing 3 contracts. Filter dropdown updates displayed contracts. Statistics recalculate only when contracts or filter changes, not when counter increments (check console).
Common Pitfalls: Overusing useMemo (premature optimization), memoizing cheap operations (adds overhead), or missing dependencies (stale results).
Example 4: useRef for DOM Access and Mutable Values
useRef creates mutable reference that persists across renders. Use for DOM access or storing values without triggering re-renders.
import { useState, useRef, useEffect } from 'react';
function DonationFormWithFocus() {
const [amount, setAmount] = useState<number>(0);
const [donations, setDonations] = useState<number[]>([]);
// => Ref for DOM element access
// => useRef<HTMLInputElement>(null) creates ref for input element
const inputRef = useRef<HTMLInputElement>(null);
// => inputRef.current is null initially
// => After render, React assigns DOM element to inputRef.current
// => Ref for mutable value (doesn't trigger re-render)
const renderCount = useRef<number>(0);
// => Persists across renders, unlike let variable
// => Changing renderCount.current doesn't trigger re-render
// => Ref for interval ID (cleanup reference)
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// => Effect to count renders
useEffect(() => {
renderCount.current += 1; // => Increment on every render
// => Doesn't trigger re-render (unlike setState)
console.log('Component rendered', renderCount.current, 'times');
});
// => No dependency array: runs after every render
// => Focus input on mount
useEffect(() => {
if (inputRef.current) {
// => Type guard: check if ref assigned
inputRef.current.focus(); // => Focus the input element
// => Directly manipulates DOM
}
}, []);
// => Empty dependency array: runs once on mount
// => Handle donation submission
const handleDonate = () => {
if (amount > 0) {
setDonations(prev => [...prev, amount]);
setAmount(0); // => Reset amount
// => Focus input after submission
if (inputRef.current) {
inputRef.current.focus(); // => Return focus to input
}
}
};
// => Start auto-increment timer
const startAutoIncrement = () => {
if (intervalRef.current) return; // => Already running
// => Create interval and store ID in ref
intervalRef.current = setInterval(() => {
setAmount(prev => prev + 10); // => Increment by \$10 every second
}, 1000);
// => Store interval ID for cleanup
};
// => Stop auto-increment timer
const stopAutoIncrement = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current); // => Clear interval
intervalRef.current = null; // => Reset ref
}
};
// => Cleanup on unmount
useEffect(() => {
return () => {
// => Cleanup function runs on unmount
if (intervalRef.current) {
clearInterval(intervalRef.current); // => Clear any active interval
}
};
}, []);
// => Get input element properties directly
const handleCheckInput = () => {
if (inputRef.current) {
console.log('Input value:', inputRef.current.value);
console.log('Input focused:', document.activeElement === inputRef.current);
// => Direct DOM access via ref
}
};
return (
<div>
<h2>Donation Form</h2>
<p>Render count: {renderCount.current}</p>
{/* => Display render count (doesn't trigger re-render when updated) */}
<div>
<label>Amount ($): </label>
<input
ref={inputRef}
{/* => Attach ref to input element */}
{/* => React assigns DOM node to inputRef.current */}
type="number"
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
min="0"
/>
</div>
<div>
<button onClick={handleDonate}>Donate</button>
<button onClick={handleCheckInput}>Check Input Properties</button>
</div>
<div>
<button onClick={startAutoIncrement}>Start Auto-Increment (\$10/sec)</button>
<button onClick={stopAutoIncrement}>Stop Auto-Increment</button>
{/* => Interval ID stored in ref persists across renders */}
</div>
<div>
<h3>Donations History</h3>
<ul>
{donations.map((donation, index) => (
<li key={index}>${donation}</li>
))}
</ul>
<p>Total: ${donations.reduce((sum, d) => sum + d, 0)}</p>
</div>
</div>
);
}
export default DonationFormWithFocus;Key Takeaway: Use useRef for DOM access (focus, scroll, measure) or storing mutable values that don’t trigger re-renders (interval IDs, previous values, render counts).
Expected Output: Donation form with auto-focused input. “Donate” button adds amount to history and refocuses input. Auto-increment buttons demonstrate interval management with refs. Render count displays without causing re-renders.
Common Pitfalls: Using refs for state (won’t trigger re-render), accessing ref.current in render (use useEffect), or forgetting type parameter (TypeScript errors).
Example 5: useImperativeHandle for Custom Refs
useImperativeHandle customizes ref value exposed to parent components. Use with forwardRef for controlled component APIs.
import { useRef, useImperativeHandle, forwardRef, useState } from 'react';
// => Define methods exposed via ref
interface CounterRef {
increment: () => void;
decrement: () => void;
reset: () => void;
getValue: () => number;
}
// => Props for counter component
interface CounterProps {
initialValue?: number;
}
// => forwardRef allows component to receive ref
// => Second parameter is the forwarded ref
const Counter = forwardRef<CounterRef, CounterProps>((props, ref) => {
const [count, setCount] = useState(props.initialValue || 0);
// => useImperativeHandle customizes ref value
// => First parameter: forwarded ref
// => Second parameter: function returning exposed methods
useImperativeHandle(ref, () => ({
// => Return object with methods to expose
// => Parent can call these methods via ref.current
increment: () => {
setCount(prev => prev + 1); // => Increment internal state
// => Parent can trigger this without props
},
decrement: () => {
setCount(prev => prev - 1);
},
reset: () => {
setCount(props.initialValue || 0); // => Reset to initial value
},
getValue: () => {
return count; // => Return current count
// => Parent can read state without props
}
}), [count, props.initialValue]);
// => Dependencies: recreate methods when these change
// => Ensures closures capture latest values
return (
<div style={{ padding: '16px', border: '1px solid #ccc', margin: '8px' }}>
<h3>Counter Component</h3>
<p>Count: {count}</p>
{/* => Component manages its own UI */}
{/* => Parent controls it via ref methods */}
</div>
);
});
// => forwardRef wraps component
// => Enables ref forwarding
// => Parent component using custom ref
function DonationDashboard() {
// => Create ref for Counter component
const counter1Ref = useRef<CounterRef>(null);
const counter2Ref = useRef<CounterRef>(null);
// => Typed with CounterRef interface
const [message, setMessage] = useState<string>('');
// => Call child method via ref
const handleIncrementBoth = () => {
counter1Ref.current?.increment(); // => Optional chaining (ref might be null)
counter2Ref.current?.increment();
// => Directly calls child's increment method
// => No props needed for this control
};
// => Read child state via ref
const handleGetTotal = () => {
const value1 = counter1Ref.current?.getValue() || 0;
const value2 = counter2Ref.current?.getValue() || 0;
// => Calls getValue method on both counters
// => Returns current count from each
const total = value1 + value2;
setMessage(`Total donations: $${total}`);
// => Display combined total
};
// => Reset all counters
const handleResetAll = () => {
counter1Ref.current?.reset();
counter2Ref.current?.reset();
setMessage('All counters reset');
};
return (
<div>
<h2>Donation Dashboard</h2>
<div style={{ display: 'flex', gap: '16px' }}>
<Counter ref={counter1Ref} initialValue={0} />
{/* => Pass ref to child component */}
{/* => forwardRef enables this */}
<Counter ref={counter2Ref} initialValue={10} />
</div>
<div style={{ marginTop: '16px' }}>
<button onClick={handleIncrementBoth}>
Increment Both Counters
</button>
<button onClick={() => counter1Ref.current?.decrement()}>
Decrement Counter 1
</button>
<button onClick={handleGetTotal}>
Get Total
</button>
<button onClick={handleResetAll}>
Reset All
</button>
</div>
{message && <p><strong>{message}</strong></p>}
</div>
);
}
export default DonationDashboard;Key Takeaway: Use useImperativeHandle with forwardRef to expose custom methods to parent via refs. Enables imperative control of child components while keeping encapsulation.
Expected Output: Dashboard with two counter components. Buttons control both counters via refs (increment, decrement, reset). “Get Total” reads values from both counters and displays sum.
Common Pitfalls: Overusing imperative refs (prefer props), forgetting forwardRef (ref won’t work), or not typing ref interface (TypeScript errors).
Group 2: Custom Hooks (5 examples)
Example 6: Creating Custom Hooks
Custom hooks extract reusable logic. Start with “use” prefix, can use other hooks inside.
import { useState, useEffect } from 'react';
// => Custom hook for window dimensions
// => Returns current window width and height
function useWindowSize() {
// => State for dimensions
const [size, setSize] = useState({
width: window.innerWidth, // => Initial width
height: window.innerHeight // => Initial height
});
useEffect(() => {
// => Event handler for resize
const handleResize = () => {
setSize({
width: window.innerWidth, // => Updated width
height: window.innerHeight // => Updated height
});
};
// => Add event listener
window.addEventListener('resize', handleResize);
// => Listens for window resize events
// => Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
// => Remove listener on unmount
};
}, []);
// => Empty dependency array: setup once
// => Return current size
return size;
// => Components using this hook get reactive size
}
// => Custom hook for local storage state
// => Syncs state with localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
// => Generic type T for value type
// => State initialized from localStorage or initial value
const [value, setValue] = useState<T>(() => {
try {
// => Try to get from localStorage
const item = window.localStorage.getItem(key);
// => Returns string or null
return item ? JSON.parse(item) : initialValue;
// => Parse JSON if exists, otherwise use initial value
} catch (error) {
console.error('Error reading from localStorage:', error);
return initialValue; // => Fallback on error
}
});
// => Lazy initialization (function runs only once)
// => Update localStorage when value changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
// => Stringify value and save to localStorage
} catch (error) {
console.error('Error writing to localStorage:', error);
}
}, [key, value]);
// => Dependencies: re-run when key or value changes
// => Return tuple like useState
return [value, setValue] as const;
// => as const prevents TypeScript from widening type
// => Maintains tuple structure [T, (value: T) => void]
}
// => Component using custom hooks
function ResponsiveDonationForm() {
// => Use window size hook
const { width, height } = useWindowSize();
// => Automatically updates when window resizes
// => Use localStorage hook for donation amount
const [amount, setAmount] = useLocalStorage<number>('donationAmount', 0);
// => Persists amount across page refreshes
// => Use localStorage hook for donor name
const [name, setName] = useLocalStorage<string>('donorName', '');
// => Responsive behavior based on width
const isMobile = width < 768; // => Mobile breakpoint
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert(`Donation submitted: $${amount} from ${name}`);
// => Clear form after submission
setAmount(0);
setName('');
// => Updates both state and localStorage
};
return (
<div style={{ padding: '20px' }}>
<h2>Donation Form</h2>
<p>
Window size: {width} x {height}
{/* => Displays current dimensions */}
</p>
<p>
Layout: {isMobile ? 'Mobile' : 'Desktop'}
{/* => Responsive indicator */}
</p>
<form onSubmit={handleSubmit} style={{
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
// => Stack vertically on mobile, horizontally on desktop
gap: '12px'
}}>
<input
type="text"
placeholder="Your Name"
value={name}
onChange={(e) => setName(e.target.value)}
style={{ padding: '8px', flex: 1 }}
/>
<input
type="number"
placeholder="Amount"
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
style={{ padding: '8px', flex: 1 }}
min="0"
/>
<button type="submit" style={{ padding: '8px 16px' }}>
Donate
</button>
</form>
<p style={{ fontSize: '0.875rem', color: '#666' }}>
Your form data is saved locally and persists across page refreshes.
</p>
</div>
);
}
export default ResponsiveDonationForm;Key Takeaway: Custom hooks extract reusable logic. Start with “use” prefix, can call other hooks. Return values or tuple. Enable code reuse across components without prop drilling.
Expected Output: Donation form showing window size. Resizing window updates dimensions and switches between mobile/desktop layouts. Form inputs persist across page refreshes via localStorage.
Common Pitfalls: Not starting with “use” prefix (linter errors), calling hooks conditionally inside custom hooks (violates rules of hooks), or forgetting to handle errors (localStorage can fail).
Example 7: useLocalStorage Hook (Detailed Implementation)
Enhanced localStorage hook with type safety, error handling, and synchronization across tabs.
import { useState, useEffect } from 'react';
// => Complete useLocalStorage implementation
function useLocalStorage<T>(key: string, initialValue: T) {
// => Get stored value or initial value
const readValue = (): T => {
// => Check if running in browser (not SSR)
if (typeof window === 'undefined') {
return initialValue; // => Return initial value during SSR
}
try {
const item = window.localStorage.getItem(key);
// => getItem returns string or null
return item ? JSON.parse(item) : initialValue;
// => Parse JSON string to original type
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue; // => Fallback on error
}
};
// => State with lazy initialization
const [storedValue, setStoredValue] = useState<T>(readValue);
// => Update localStorage and state
const setValue = (value: T | ((val: T) => T)) => {
try {
// => Allow value to be function like useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
// => If function, call with current value
// => Otherwise use value directly
// => Update state
setStoredValue(valueToStore);
// => Update localStorage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
// => Stringify and save
// => Dispatch custom event for cross-tab sync
window.dispatchEvent(new Event('local-storage'));
// => Other tabs can listen to this event
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
};
// => Listen for changes from other tabs
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
// => StorageEvent fires when localStorage changes in other tabs
if (e.key === key && e.newValue !== null) {
// => Check if our key changed
setStoredValue(JSON.parse(e.newValue));
// => Update state with new value from other tab
}
};
// => Listen to storage event
window.addEventListener('storage', handleStorageChange);
// => Native event for cross-tab communication
// => Listen to custom event (same tab updates)
window.addEventListener('local-storage', () => {
setStoredValue(readValue()); // => Re-read from localStorage
});
return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('local-storage', () => {});
};
}, [key]);
// => Dependency: key (re-setup if key changes)
return [storedValue, setValue] as const;
}
// => Component demonstrating localStorage hook
function SadaqahTracker() {
// => Track monthly Sadaqah (charity) goal and current amount
const [goal, setGoal] = useLocalStorage<number>('sadaqahGoal', 100);
const [current, setCurrent] = useLocalStorage<number>('sadaqahCurrent', 0);
// => Both persisted to localStorage
// => Derived state: progress percentage
const progress = goal > 0 ? (current / goal) * 100 : 0;
// => Calculate percentage (handle division by zero)
// => Handle donation
const handleDonate = (amount: number) => {
setCurrent(prev => prev + amount); // => Functional update
// => Updates localStorage automatically
};
// => Reset for new month
const handleReset = () => {
setCurrent(0);
// => Keeps goal, resets current donations
};
return (
<div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
<h2>Monthly Sadaqah Tracker</h2>
<div style={{ marginBottom: '16px' }}>
<label>Monthly Goal ($): </label>
<input
type="number"
value={goal}
onChange={(e) => setGoal(Number(e.target.value))}
min="0"
style={{ padding: '4px', width: '100px' }}
/>
</div>
<div style={{ marginBottom: '16px' }}>
<p><strong>Current Donations:</strong> ${current}</p>
<p><strong>Goal:</strong> ${goal}</p>
<p><strong>Progress:</strong> {progress.toFixed(1)}%</p>
{/* => Progress bar */}
<div style={{
width: '100%',
height: '24px',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: `${Math.min(progress, 100)}%`,
height: '100%',
backgroundColor: progress >= 100 ? '#029E73' : '#0173B2',
// => Green if goal reached, blue otherwise
transition: 'width 0.3s ease'
}} />
</div>
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button onClick={() => handleDonate(5)} style={{ padding: '8px' }}>
Donate \$5
</button>
<button onClick={() => handleDonate(10)} style={{ padding: '8px' }}>
Donate \$10
</button>
<button onClick={() => handleDonate(25)} style={{ padding: '8px' }}>
Donate \$25
</button>
</div>
<div style={{ marginTop: '16px' }}>
<button onClick={handleReset} style={{ padding: '8px' }}>
Reset for New Month
</button>
</div>
<p style={{ marginTop: '16px', fontSize: '0.875rem', color: '#666' }}>
Data persists across page refreshes and syncs across browser tabs.
Open this page in multiple tabs and try donating!
</p>
</div>
);
}
export default SadaqahTracker;Key Takeaway: useLocalStorage hook provides persistent state with automatic localStorage sync. Handles errors, supports functional updates, and syncs across browser tabs using storage events.
Expected Output: Sadaqah tracker with goal, current donations, and progress bar. Data persists across refreshes. Open multiple tabs to see cross-tab synchronization (donation in one tab updates others).
Common Pitfalls: Not handling JSON parse errors (can throw), forgetting SSR check (window undefined on server), or not using lazy initialization (reads localStorage on every render).
Example 8: useDebounce Hook
Debounce hook delays value updates to reduce expensive operations like API calls or filtering.
import { useState, useEffect } from 'react';
// => Debounce hook implementation
// => Returns debounced value that updates after delay
function useDebounce<T>(value: T, delay: number): T {
// => Generic type T for any value type
// => State for debounced value
const [debouncedValue, setDebouncedValue] = useState<T>(value);
// => Initially equals input value
useEffect(() => {
// => Set timeout to update debounced value
const handler = setTimeout(() => {
setDebouncedValue(value); // => Update after delay
// => Only runs if value stays stable for delay duration
}, delay);
// => handler is timeout ID for cleanup
// => Cleanup function
return () => {
clearTimeout(handler); // => Clear timeout on cleanup
// => Runs before next effect or on unmount
// => If value changes before timeout, old timeout cleared
};
}, [value, delay]);
// => Dependencies: re-run when value or delay changes
// => New timeout set on every value change
// => Previous timeout cleared, resetting delay
return debouncedValue;
// => Returns stable value (only updates after delay)
}
// => Component using debounce hook
function PrayerSearch() {
// => Search input state (updates immediately)
const [searchTerm, setSearchTerm] = useState<string>('');
// => Debounced search term (updates after 500ms delay)
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// => Only updates when searchTerm stable for 500ms
// => State for search results
const [results, setResults] = useState<string[]>([]);
const [isSearching, setIsSearching] = useState<boolean>(false);
// => Mock prayer database
const prayerDatabase = [
'Fajr (Dawn Prayer)',
'Dhuhr (Noon Prayer)',
'Asr (Afternoon Prayer)',
'Maghrib (Sunset Prayer)',
'Isha (Night Prayer)',
'Tahajjud (Night Prayer)',
'Duha (Forenoon Prayer)',
'Witr (Odd Prayer)',
'Taraweeh (Ramadan Prayer)'
];
// => Perform search when debounced term changes
useEffect(() => {
// => Only search if debounced term not empty
if (debouncedSearchTerm.trim() === '') {
setResults([]);
setIsSearching(false);
return;
}
// => Simulate search start
setIsSearching(true);
console.log('Searching for:', debouncedSearchTerm);
// => This logs only when typing stops for 500ms
// => Without debounce, would log on every keystroke
// => Simulate API call with timeout
const searchTimeout = setTimeout(() => {
// => Filter prayers matching search term
const filtered = prayerDatabase.filter(prayer =>
prayer.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
);
setResults(filtered);
setIsSearching(false);
console.log('Search complete. Results:', filtered.length);
}, 300);
// => Simulate 300ms API latency
return () => clearTimeout(searchTimeout);
// => Cleanup if debounced term changes again
}, [debouncedSearchTerm]);
// => Dependency: debouncedSearchTerm (not searchTerm!)
// => Effect only runs when debounced value changes
// => Prevents excessive API calls while typing
return (
<div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
<h2>Prayer Search</h2>
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
// => Updates immediately on every keystroke
placeholder="Search prayers (e.g., 'Fajr', 'Night')..."
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
<p style={{ fontSize: '0.875rem', color: '#666', marginTop: '8px' }}>
{searchTerm !== debouncedSearchTerm && 'Typing...'}
{/* => Show "Typing..." if values differ (debounce in progress) */}
{searchTerm === debouncedSearchTerm && searchTerm && !isSearching && `Found ${results.length} prayers`}
{/* => Show count when stable and not searching */}
{isSearching && 'Searching...'}
</p>
</div>
<div style={{ marginTop: '16px' }}>
{results.length > 0 ? (
<ul style={{ listStyle: 'none', padding: 0 }}>
{results.map((prayer, index) => (
<li key={index} style={{
padding: '12px',
backgroundColor: '#f5f5f5',
marginBottom: '8px',
borderRadius: '4px'
}}>
{prayer}
</li>
))}
</ul>
) : debouncedSearchTerm && !isSearching ? (
<p>No prayers found matching "{debouncedSearchTerm}"</p>
) : null}
</div>
<div style={{ marginTop: '16px', fontSize: '0.875rem', color: '#666' }}>
<p><strong>Debounce Demo:</strong></p>
<p>Current input: "{searchTerm}"</p>
<p>Debounced value: "{debouncedSearchTerm}"</p>
<p>Search only happens when you stop typing for 500ms.</p>
</div>
</div>
);
}
export default PrayerSearch;Key Takeaway: useDebounce hook delays value updates, preventing excessive operations. Essential for search inputs, API calls, and filtering. Reduces network requests and improves performance.
Expected Output: Prayer search with 500ms debounce. Typing updates input immediately but search waits until typing stops. Status indicators show debounce state. Console logs show search only fires after delay.
Common Pitfalls: Using original value instead of debounced in effect (defeats purpose), setting delay too short (no benefit) or too long (sluggish UX), or forgetting cleanup (memory leaks).
Example 9: useFetch Hook
Custom fetch hook with loading, error, and data states. Handles cleanup for cancelled requests.
import { useState, useEffect } from 'react';
// => Type for fetch hook return value
interface UseFetchResult<T> {
data: T | null; // => Fetched data or null
loading: boolean; // => Loading indicator
error: string | null; // => Error message or null
}
// => Generic fetch hook implementation
function useFetch<T>(url: string): UseFetchResult<T> {
// => State for data, loading, and error
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// => Reset states on URL change
setLoading(true);
setError(null);
setData(null);
// => AbortController for request cancellation
const controller = new AbortController();
// => Creates signal for fetch cancellation
const signal = controller.signal;
console.log('Fetching:', url);
// => Perform fetch
fetch(url, { signal })
// => Pass signal to enable cancellation
.then(response => {
// => Check HTTP status
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
// => Throw for 4xx, 5xx responses
}
return response.json(); // => Parse JSON body
})
.then((jsonData: T) => {
// => Success: update data
setData(jsonData);
setLoading(false);
console.log('Fetch successful');
})
.catch(err => {
// => Error handling
if (err.name === 'AbortError') {
// => Request was cancelled
console.log('Fetch aborted');
} else {
// => Actual error
console.error('Fetch error:', err);
setError(err.message);
setLoading(false);
}
});
// => Cleanup function
return () => {
controller.abort(); // => Cancel request on cleanup
// => Runs when URL changes or component unmounts
// => Prevents state updates on unmounted component
console.log('Aborting fetch');
};
}, [url]);
// => Dependency: url (re-fetch when URL changes)
// => Return current state
return { data, loading, error };
}
// => Type for user data
interface User {
id: number;
name: string;
email: string;
phone: string;
}
// => Component using fetch hook
function UserProfile() {
const [userId, setUserId] = useState<number>(1);
// => Use fetch hook with dynamic URL
const { data: user, loading, error } = useFetch<User>(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
// => Destructure with alias (data as user)
// => Re-fetches when userId changes
// => Loading state
if (loading) {
return (
<div style={{ padding: '20px' }}>
<p>Loading user data...</p>
{/* => Show loading indicator */}
</div>
);
}
// => Error state
if (error) {
return (
<div style={{ padding: '20px' }}>
<p style={{ color: 'red' }}>Error: {error}</p>
{/* => Display error message */}
<button onClick={() => setUserId(userId)}>
Retry
</button>
{/* => Re-trigger fetch by setting same userId */}
</div>
);
}
// => Success state
return (
<div style={{ padding: '20px', maxWidth: '500px', margin: '0 auto' }}>
<h2>User Profile</h2>
{user && (
<div style={{
padding: '16px',
backgroundColor: '#f5f5f5',
borderRadius: '8px'
}}>
<p><strong>ID:</strong> {user.id}</p>
<p><strong>Name:</strong> {user.name}</p>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Phone:</strong> {user.phone}</p>
</div>
)}
<div style={{ marginTop: '16px', display: 'flex', gap: '8px' }}>
<button
onClick={() => setUserId(prev => Math.max(1, prev - 1))}
disabled={userId === 1}
// => Disable at minimum ID
>
Previous User
</button>
<button
onClick={() => setUserId(prev => Math.min(10, prev + 1))}
disabled={userId === 10}
// => Disable at maximum ID
>
Next User
</button>
</div>
<p style={{ marginTop: '16px', fontSize: '0.875rem', color: '#666' }}>
Current user ID: {userId}. Click buttons to fetch different users.
Rapid clicking demonstrates request cancellation (check console).
</p>
</div>
);
}
export default UserProfile;Key Takeaway: useFetch hook encapsulates data fetching with loading, error, and data states. Uses AbortController for request cancellation. Re-fetches when dependencies change.
Expected Output: User profile with navigation buttons. Initially shows loading state, then displays user data. Clicking buttons fetches different users. Rapid clicking cancels previous requests (check console logs).
Common Pitfalls: Not cancelling requests (memory leaks, race conditions), updating state on unmounted component (React warning), or forgetting error handling (app crashes).
Example 10: useForm Hook for Form Management
Form hook simplifies form state management with validation and submission handling.
import { useState } from 'react';
// => Type for validation rules
interface ValidationRules {
required?: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
custom?: (value: any) => string | undefined;
}
// => Type for field configuration
interface FieldConfig {
initialValue: any;
validation?: ValidationRules;
}
// => Type for form configuration
interface FormConfig {
[key: string]: FieldConfig;
}
// => Type for form errors
interface FormErrors {
[key: string]: string | undefined;
}
// => useForm hook implementation
function useForm<T extends { [key: string]: any }>(config: FormConfig) {
// => State for form values
// => Stores current value for each form field
const [values, setValues] = useState<T>(() => {
// => Initialize from config using lazy initialization
// => Lazy init prevents recreation on every render
const initialValues: any = {}; // => Empty object to collect initial values
Object.keys(config).forEach(key => {
// => Iterate through field configurations
// => Extract initialValue for each field
initialValues[key] = config[key].initialValue;
// => Set field's starting value from config
});
return initialValues as T; // => Type assertion for TypeScript
});
// => State for errors
// => Maps field name to error message
// => Empty string or undefined means no error
const [errors, setErrors] = useState<FormErrors>({});
// => Example: { email: 'Invalid email format', phone: undefined }
// => State for touched fields
// => Tracks which fields user has interacted with
// => Only show errors for touched fields (better UX)
const [touched, setTouched] = useState<{ [key: string]: boolean }>({});
// => Example: { email: true, phone: false } - email touched, phone not
// => Validate single field against its rules
// => Returns error message string or undefined (valid)
// => Pure function - no side effects
const validateField = (name: string, value: any): string | undefined => {
const rules = config[name]?.validation;
// => Get validation rules for this field
// => Optional chaining (?.) handles missing config safely
if (!rules) return undefined; // => No rules means field is valid
// => Required validation
// => Checks if value exists (not empty, null, undefined)
if (rules.required && !value) {
return 'This field is required';
// => Fails for '', null, undefined, 0, false
}
// => Min length validation
// => Only applies to string/array values with .length property
if (rules.minLength && value.length < rules.minLength) {
return `Minimum length is ${rules.minLength}`;
// => Example: minLength: 3, value: 'ab' → error
}
// => Max length validation
// => Prevents excessive input (security + UX)
if (rules.maxLength && value.length > rules.maxLength) {
return `Maximum length is ${rules.maxLength}`;
// => Example: maxLength: 50, value: 'very long...' → error
}
// => Pattern validation
// => Tests value against regex pattern
// => Common for email, phone, zip code, etc.
if (rules.pattern && !rules.pattern.test(value)) {
return 'Invalid format';
// => Example: pattern: /^[0-9]+$/, value: 'abc' → error
}
// => Custom validation
// => Allows domain-specific validation logic
// => Function receives value, returns error message or undefined
if (rules.custom) {
return rules.custom(value);
// => Example: (val) => val.includes('@') ? undefined : 'Must contain @'
}
return undefined; // => All validations passed, field is valid
};
// => Handle field change (called on every keystroke/input)
// => Updates value and re-validates if field already touched
const handleChange = (name: string, value: any) => {
// => Update field value immutably
// => Functional update ensures latest state
setValues(prev => ({
...prev, // => Keep all other field values
[name]: value // => Override changed field
// => Computed property syntax: field name determines key
}));
// => Validate only if field has been touched
// => Avoids showing errors before user interaction (better UX)
if (touched[name]) {
const error = validateField(name, value);
// => Re-validate with new value
// => Returns error message or undefined
setErrors(prev => ({
...prev,
[name]: error // => Update or clear error for this field
// => If error is undefined, field becomes valid
}));
}
};
// => Handle field blur (when input loses focus)
// => Marks field as touched and validates current value
const handleBlur = (name: string) => {
// => Mark field as touched
// => Enables error display for this field
setTouched(prev => ({
...prev,
[name]: true // => Set touched flag
// => Once touched, errors will show on change
}));
// => Validate field with current value
// => Uses values[name] from current state
const error = validateField(name, values[name]);
// => Get current value from state
// => Validate against field's rules
setErrors(prev => ({
...prev,
[name]: error // => Set validation result
// => Error message or undefined (valid)
}));
};
// => Validate all fields at once
// => Called on form submission
// => Returns true if entire form is valid, false otherwise
const validateAll = (): boolean => {
const newErrors: FormErrors = {};
// => Collect all validation errors
// => Will contain only fields with errors
let isValid = true;
// => Track overall form validity
// => Flips to false if any field has error
Object.keys(config).forEach(name => {
// => Iterate through all configured fields
// => Validate each field regardless of touched state
const error = validateField(name, values[name]);
// => Run validation for current field value
// => Returns error message or undefined
if (error) {
// => Field has validation error
newErrors[name] = error;
// => Add error to collection
// => Example: { email: 'Invalid email', phone: 'Required' }
isValid = false;
// => Mark form as invalid
// => Even one error makes entire form invalid
}
});
setErrors(newErrors);
// => Update all errors at once
// => Replaces previous error state completely
// => Shows all validation errors simultaneously
setTouched(
Object.keys(config).reduce((acc, key) => ({ ...acc, [key]: true }), {})
// => Mark all fields as touched
// => reduce() builds object: { field1: true, field2: true, ... }
// => Ensures all errors are visible to user
);
return isValid;
// => Return validation result
// => true: form submittable, false: has errors
};
// => Reset form to initial state
// => Clears all values, errors, and touched flags
// => Useful after successful submission or user cancellation
const reset = () => {
const initialValues: any = {};
// => Recreate initial values object
Object.keys(config).forEach(key => {
initialValues[key] = config[key].initialValue;
// => Extract initial value from each field config
// => Restores form to starting state
});
setValues(initialValues as T);
// => Reset values to initial state
// => Type assertion for TypeScript safety
setErrors({});
// => Clear all validation errors
// => Empty object means no errors
setTouched({});
// => Clear all touched flags
// => Form appears untouched after reset
};
return {
// => Return form management API
// => Object destructured by component
values, // => Current field values
errors, // => Current validation errors
touched, // => Which fields have been interacted with
handleChange, // => Function to update field value
handleBlur, // => Function called when field loses focus
validateAll, // => Function to validate entire form
reset // => Function to reset form to initial state
// => Clean API hides internal state management complexity
};
}
// => Component using form hook
// => Demonstrates useForm hook in production scenario
function DonorRegistrationForm() {
// => Define form configuration
// => Generic type <{...}> ensures type safety for form.values
const form = useForm<{
name: string; // => Donor's full name
email: string; // => Donor's email address
phone: string; // => Donor's phone number
amount: number; // => Donation amount in dollars
}>({
// => Configuration object defines fields + validation rules
name: {
initialValue: '', // => Start with empty string
validation: {
required: true, // => Field is mandatory
minLength: 3, // => Minimum 3 characters
maxLength: 50 // => Maximum 50 characters
// => Prevents very short names ('ab') or excessively long input
}
},
email: {
initialValue: '',
validation: {
required: true, // => Email is mandatory
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
// => Basic email regex pattern
// => Checks for: localpart @ domain . tld
// => Prevents spaces, requires @ and dot
custom: (value) => {
// => Custom validation: block disposable emails
// => Domain-specific business logic
if (value.endsWith('@tempmail.com')) {
return 'Disposable emails not allowed';
// => Reject temporary/throwaway email services
}
return undefined; // => Valid email
}
}
},
phone: {
initialValue: '',
validation: {
required: true, // => Phone is mandatory
pattern: /^\d{10}$/, // => Exactly 10 digits
// => ^ = start, \d = digit, {10} = exactly 10, $ = end
custom: (value) => {
if (value && !/^\d{10}$/.test(value)) {
return 'Phone must be 10 digits';
// => Custom error message for clarity
// => Redundant with pattern but provides better UX message
}
return undefined;
}
}
},
amount: {
initialValue: 0, // => Start at 0
validation: {
required: true, // => Amount is mandatory
custom: (value) => {
if (value <= 0) {
return 'Amount must be greater than 0';
// => Business rule: no zero or negative donations
// => Ensures meaningful contribution
}
return undefined;
}
}
}
});
const [submitted, setSubmitted] = useState(false);
// => Track submission state
// => Shows success message after valid submission
// => Handle form submission
// => Validates entire form before processing
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// => Prevent default form submission
// => Stops page reload, allows custom handling
// => Validate all fields
// => Returns true if all valid, false if any errors
if (form.validateAll()) {
// => All fields valid - process submission
console.log('Form submitted:', form.values);
// => In production: send to API, save to database, etc.
setSubmitted(true);
// => Show success message
// => Triggers conditional render below
// => Reset after 2 seconds
// => Gives user time to see success message
setTimeout(() => {
form.reset();
// => Clear form to initial state
// => Ready for next submission
setSubmitted(false);
// => Hide success message
// => Return to form view
}, 2000); // => 2000ms = 2 seconds
} else {
// => Form has validation errors
console.log('Form has errors:', form.errors);
// => In production: show toast notification, focus first error, etc.
// => Errors already displayed by field-level validation
}
};
if (submitted) {
// => Conditional render: show success message after submission
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
{/* => Success message displayed for 2 seconds */}
<h2 style={{ color: '#029E73' }}>Thank you for your donation!</h2>
{/* => Accessible green color from palette */}
<p>Donor: {form.values.name}</p>
{/* => Display submitted donor name */}
<p>Amount: ${form.values.amount}</p>
{/* => Display submitted amount */}
{/* => After timeout, form resets and this view disappears */}
</div>
);
}
return (
<div style={{ maxWidth: '500px', margin: '0 auto', padding: '20px' }}>
{/* => Centered container with max width for readability */}
<h2>Donor Registration</h2>
<form onSubmit={handleSubmit}>
{/* => Form submission calls handleSubmit */}
{/* => Name field */}
<div style={{ marginBottom: '16px' }}>
{/* => Field container with bottom spacing */}
<label style={{ display: 'block', marginBottom: '4px' }}>
Name *
{/* => Asterisk indicates required field */}
</label>
<input
type="text"
value={form.values.name}
{/* => Controlled input: value from form state */}
onChange={(e) => form.handleChange('name', e.target.value)}
{/* => Update form value on every keystroke */}
{/* => Extract value from event target */}
onBlur={() => form.handleBlur('name')}
{/* => Mark field as touched when focus lost */}
{/* => Triggers validation */}
style={{
width: '100%',
padding: '8px',
border: form.errors.name && form.touched.name ? '2px solid red' : '1px solid #ccc',
{/* => Red border if error exists AND field touched */}
{/* => Gray border otherwise (normal state) */}
borderRadius: '4px'
}}
/>
{form.errors.name && form.touched.name && (
{/* => Conditional render: show error only if exists AND touched */}
{/* => Prevents showing errors before user interaction */}
<p style={{ color: 'red', fontSize: '0.875rem', margin: '4px 0 0' }}>
{form.errors.name}
{/* => Display error message from validation */}
</p>
)}
</div>
{/* => Email field */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>
Email *
</label>
<input
type="email"
value={form.values.email}
onChange={(e) => form.handleChange('email', e.target.value)}
onBlur={() => form.handleBlur('email')}
style={{
width: '100%',
padding: '8px',
border: form.errors.email && form.touched.email ? '2px solid red' : '1px solid #ccc',
borderRadius: '4px'
}}
/>
{form.errors.email && form.touched.email && (
<p style={{ color: 'red', fontSize: '0.875rem', margin: '4px 0 0' }}>
{form.errors.email}
</p>
)}
</div>
{/* => Phone field */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>
Phone (10 digits) *
</label>
<input
type="tel"
value={form.values.phone}
onChange={(e) => form.handleChange('phone', e.target.value)}
onBlur={() => form.handleBlur('phone')}
style={{
width: '100%',
padding: '8px',
border: form.errors.phone && form.touched.phone ? '2px solid red' : '1px solid #ccc',
borderRadius: '4px'
}}
/>
{form.errors.phone && form.touched.phone && (
<p style={{ color: 'red', fontSize: '0.875rem', margin: '4px 0 0' }}>
{form.errors.phone}
</p>
)}
</div>
{/* => Amount field */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>
Donation Amount ($) *
</label>
<input
type="number"
value={form.values.amount}
onChange={(e) => form.handleChange('amount', Number(e.target.value))}
onBlur={() => form.handleBlur('amount')}
min="0"
style={{
width: '100%',
padding: '8px',
border: form.errors.amount && form.touched.amount ? '2px solid red' : '1px solid #ccc',
borderRadius: '4px'
}}
/>
{form.errors.amount && form.touched.amount && (
<p style={{ color: 'red', fontSize: '0.875rem', margin: '4px 0 0' }}>
{form.errors.amount}
</p>
)}
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
type="submit"
style={{
padding: '12px 24px',
backgroundColor: '#0173B2',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
flex: 1
}}
>
Submit Donation
</button>
<button
type="button"
onClick={form.reset}
style={{
padding: '12px 24px',
backgroundColor: '#ccc',
color: '#000',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Reset
</button>
</div>
</form>
</div>
);
}
export default DonorRegistrationForm;Key Takeaway: useForm hook centralizes form state, validation, and error handling. Supports multiple validation rules, touched states, and form reset. Reduces boilerplate for complex forms.
Expected Output: Registration form with four fields. Real-time validation on blur. Submit validates all fields. Successful submission shows thank you message for 2 seconds. Reset button clears form.
Common Pitfalls: Validating on every keystroke (poor UX), not showing errors until touched, or overly complex validation logic (extract to separate functions).
Next Steps
Continue to Group 3: Context API and Global State, or explore specific topics:
- Advanced hooks patterns (useReducer with TypeScript, custom hook composition)
- Context API for global state management
- React Query for server state management
- React Router for navigation
- Error boundaries for error handling
Group 3: Context API and Global State (5 examples)
Example 11: Creating and Using Context
Context provides global state without prop drilling. Create context, provide value, consume with useContext hook.
import { createContext, useContext, useState, ReactNode } from 'react';
// => Define context value type
interface ThemeContextType {
theme: 'light' | 'dark'; // => Current theme
toggleTheme: () => void; // => Function to toggle theme
}
// => Create context with default value
// => Default value used only if no Provider found
const ThemeContext = createContext<ThemeContextType>({
theme: 'light', // => Default theme
toggleTheme: () => {
console.warn('toggleTheme called outside Provider');
}
});
// => createContext returns Context object
// => Contains Provider and Consumer components
// => Custom hook for consuming context
// => Wraps useContext for type safety and convenience
function useTheme() {
const context = useContext(ThemeContext);
// => useContext reads current context value
// => Returns value from nearest Provider above in tree
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
// => Error if used outside Provider
}
return context;
// => Returns ThemeContextType value
}
// => Provider component props
interface ThemeProviderProps {
children: ReactNode;
}
// => Provider component manages state
// => Wraps children with Context.Provider
function ThemeProvider({ children }: ThemeProviderProps) {
// => State for theme
const [theme, setTheme] = useState<'light' | 'dark'>('light');
// => Toggle function
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
// => Flips between light and dark
};
// => Context value object
const value: ThemeContextType = {
theme,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{/* => Provider component from context */}
{/* => All children can access value via useContext */}
{children}
</ThemeContext.Provider>
);
}
// => Consumer component 1
function Header() {
// => Use custom hook to access context
const { theme, toggleTheme } = useTheme();
// => Destructure context value
return (
<header style={{
backgroundColor: theme === 'light' ? '#f0f0f0' : '#333',
color: theme === 'light' ? '#000' : '#fff',
padding: '16px'
}}>
<h1>Prayer Times Dashboard</h1>
<button onClick={toggleTheme}>
Toggle to {theme === 'light' ? 'Dark' : 'Light'} Mode
{/* => Button text based on current theme */}
</button>
</header>
);
}
// => Consumer component 2
function Content() {
const { theme } = useTheme();
// => Access context without drilling props
return (
<main style={{
backgroundColor: theme === 'light' ? '#fff' : '#222',
color: theme === 'light' ? '#000' : '#fff',
padding: '20px',
minHeight: '400px'
}}>
<h2>Today's Prayers</h2>
<ul>
<li>Fajr: 5:30 AM</li>
<li>Dhuhr: 12:45 PM</li>
<li>Asr: 4:15 PM</li>
</ul>
</main>
);
}
// => Main app component
function App() {
return (
<ThemeProvider>
{/* => Wrap app with Provider */}
{/* => All descendants can access theme context */}
<div>
<Header />
<Content />
</div>
</ThemeProvider>
);
}
export default App;Key Takeaway: Context provides global state without prop drilling. Create context with createContext, provide value with Provider, consume with useContext hook. Custom hook pattern improves type safety.
Expected Output: Dashboard with header and content. “Toggle” button switches between light and dark themes. Both header and content reflect theme change without passing props.
Common Pitfalls: Using context for frequently changing values (performance issues), not providing default value (TypeScript errors), or forgetting Provider wrapper (default value used or error thrown).
Example 12: Context with TypeScript
Advanced TypeScript patterns for type-safe context with proper null handling and optional values.
import { createContext, useContext, useState, ReactNode } from 'react';
// => Type for user data
interface User {
id: string;
name: string;
email: string;
role: 'donor' | 'admin';
}
// => Context value type with optional user
interface AuthContextType {
user: User | null; // => null when not logged in
isAuthenticated: boolean; // => Derived from user
login: (user: User) => void; // => Login function
logout: () => void; // => Logout function
}
// => Create context with undefined initial value
// => Using undefined + null check pattern
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// => undefined means "not provided yet"
// => Forces consumers to handle missing context
// => Custom hook with null check
// => Throws error if used outside Provider
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
// => context is AuthContextType | undefined
if (context === undefined) {
throw new Error('useAuth must be used within AuthProvider');
// => Prevents usage outside Provider
// => Helps catch errors at development time
}
return context;
// => TypeScript knows context is AuthContextType here
}
// => Provider props
interface AuthProviderProps {
children: ReactNode;
}
// => Provider implementation
function AuthProvider({ children }: AuthProviderProps) {
// => State for user (null or User object)
const [user, setUser] = useState<User | null>(null);
// => Derived state: authentication status
const isAuthenticated = user !== null;
// => true if user exists, false otherwise
// => Login function
const login = (userData: User) => {
console.log('User logged in:', userData);
setUser(userData); // => Set user data
// => Triggers re-render for all consumers
};
// => Logout function
const logout = () => {
console.log('User logged out');
setUser(null); // => Clear user data
};
// => Context value
const value: AuthContextType = {
user,
isAuthenticated,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// => Protected component (only for authenticated users)
function DonationDashboard() {
const { user, logout } = useAuth();
// => Type-safe access to context
// => Type guard: user could be null
if (!user) {
return <p>Please log in to access dashboard.</p>;
// => Fallback for unauthenticated state
}
return (
<div style={{ padding: '20px' }}>
<h2>Donation Dashboard</h2>
<p>Welcome, {user.name}!</p>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
{/* => Conditional rendering based on role */}
{user.role === 'admin' && (
<div style={{
backgroundColor: '#029E73',
padding: '12px',
borderRadius: '4px',
color: '#fff',
marginTop: '16px'
}}>
<strong>Admin Panel</strong>
<p>You have access to administrative features.</p>
</div>
)}
<button onClick={logout} style={{ marginTop: '16px', padding: '8px 16px' }}>
Logout
</button>
</div>
);
}
// => Login component
function LoginForm() {
const { login, isAuthenticated } = useAuth();
const handleLogin = (role: 'donor' | 'admin') => {
// => Mock login with different roles
const mockUser: User = {
id: '1',
name: role === 'admin' ? 'Fatima Ahmed' : 'Omar Hassan',
email: role === 'admin' ? 'fatima@example.com' : 'omar@example.com',
role
};
login(mockUser);
// => Calls context login function
};
if (isAuthenticated) {
return null; // => Hide login form when authenticated
}
return (
<div style={{ padding: '20px' }}>
<h2>Login</h2>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={() => handleLogin('donor')} style={{ padding: '8px 16px' }}>
Login as Donor
</button>
<button onClick={() => handleLogin('admin')} style={{ padding: '8px 16px' }}>
Login as Admin
</button>
</div>
</div>
);
}
// => Main app
function App() {
const { isAuthenticated } = useAuth();
return (
<div>
{!isAuthenticated ? <LoginForm /> : <DonationDashboard />}
{/* => Conditional rendering based on auth state */}
</div>
);
}
// => Root component with Provider
function Root() {
return (
<AuthProvider>
{/* => Wrap entire app with AuthProvider */}
<App />
</AuthProvider>
);
}
export default Root;Key Takeaway: Use undefined for context initial value to enforce Provider usage. Custom hooks with null checks provide type-safe access. Context perfect for authentication state shared across app.
Expected Output: Login form with two buttons. Clicking “Login as Donor” shows dashboard with user info. “Login as Admin” additionally shows admin panel. Logout button returns to login form.
Common Pitfalls: Not handling null user state (runtime errors), using non-null assertion (defeats type safety), or forgetting error boundary for context errors.
Example 13: Multiple Contexts Pattern
Compose multiple contexts for separation of concerns. Each context manages one domain.
import { createContext, useContext, useState, ReactNode } from 'react';
// => Context 1: Theme Context
// => Manages light/dark theme across entire app
interface ThemeContextType {
theme: 'light' | 'dark'; // => Current theme value
toggleTheme: () => void; // => Function to switch themes
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// => Create context with undefined default
// => undefined signals context used outside provider
function useTheme() {
// => Custom hook to access theme context
// => Encapsulates useContext + error checking
const context = useContext(ThemeContext);
// => Get context value from nearest ThemeProvider
if (!context) throw new Error('useTheme must be used within ThemeProvider');
// => Runtime error if hook used outside provider
// => Catches developer mistakes early
return context;
// => Return context value (theme + toggleTheme)
}
function ThemeProvider({ children }: { children: ReactNode }) {
// => Provider component manages theme state
// => Wraps app/sections that need theme access
const [theme, setTheme] = useState<'light' | 'dark'>('light');
// => State: current theme
// => Starts with 'light' theme
const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
// => Toggle between light and dark
// => Functional update ensures latest state
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{/* => Provide theme state to children */}
{/* => Any descendant can access via useTheme() */}
{children}
{/* => Render child components */}
</ThemeContext.Provider>
);
}
// => Context 2: Language Context
// => Manages internationalization (i18n) for bilingual app
type Language = 'en' | 'ar'; // => English or Arabic
// => Union type restricts to valid languages
interface LanguageContextType {
language: Language; // => Current active language
setLanguage: (lang: Language) => void; // => Function to change language
t: (key: string) => string; // => Translation function
// => t() looks up translation for key in active language
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
// => Create language context
// => undefined default for provider detection
function useLanguage() {
// => Custom hook for language context access
// => Provides clean API: const { language, setLanguage, t } = useLanguage()
const context = useContext(LanguageContext);
// => Get language context from nearest provider
if (!context) throw new Error('useLanguage must be used within LanguageProvider');
// => Error if used outside provider
// => Prevents runtime bugs from missing context
return context;
}
function LanguageProvider({ children }: { children: ReactNode }) {
// => Provider manages language state and translations
const [language, setLanguage] = useState<Language>('en');
// => Current language state
// => Defaults to English
// => Simple translation map
// => Record<Language, Record<string, string>>
// => Structure: { language: { key: translation } }
const translations: Record<Language, Record<string, string>> = {
en: {
// => English translations
title: 'Donation Form',
amount: 'Amount',
submit: 'Submit Donation',
theme: 'Theme',
language: 'Language'
},
ar: {
// => Arabic translations
// => Maps same keys to Arabic text
title: 'نموذج التبرع', // => 'Donation Form' in Arabic
amount: 'المبلغ', // => 'Amount' in Arabic
submit: 'تقديم التبرع', // => 'Submit Donation' in Arabic
theme: 'السمة', // => 'Theme' in Arabic
language: 'اللغة' // => 'Language' in Arabic
}
};
// => In production: load from JSON files, use i18n library
// => Translation function
// => Looks up key in active language's translations
const t = (key: string): string => {
return translations[language][key] || key;
// => translations[language] gets language object: { title: '...', ... }
// => [key] gets specific translation
// => || key provides fallback: if translation missing, return key
// => Example: t('title') with language='en' → 'Donation Form'
};
return (
<LanguageContext.Provider value={{ language, setLanguage, t }}>
{/* => Provide language state + setter + translation function */}
{/* => Children can access via useLanguage() hook */}
{children}
</LanguageContext.Provider>
);
}
// => Context 3: Donation Context
// => Manages donation data and business logic
interface DonationContextType {
totalDonations: number; // => Sum of all donation amounts
donate: (amount: number) => void; // => Function to add new donation
donations: Array<{ id: number; amount: number; date: string }>;
// => Array of all donations with metadata
}
const DonationContext = createContext<DonationContextType | undefined>(undefined);
// => Create donation context
// => Separates donation logic from theme/language concerns
function useDonations() {
// => Custom hook for donation context access
// => Usage: const { totalDonations, donate, donations } = useDonations()
const context = useContext(DonationContext);
// => Get context from nearest DonationProvider
if (!context) throw new Error('useDonations must be used within DonationProvider');
// => Enforce provider requirement
// => Catches setup errors at runtime
return context;
}
function DonationProvider({ children }: { children: ReactNode }) {
// => Provider manages donation state and operations
const [donations, setDonations] = useState<Array<{ id: number; amount: number; date: string }>>([]);
// => State: array of donation objects
// => Each donation has: id (unique), amount (number), date (string)
// => Starts empty []
// => Derived state: total donations
// => Computed on every render from donations array
const totalDonations = donations.reduce((sum, d) => sum + d.amount, 0);
// => reduce() sums all donation amounts
// => (sum, d) => sum + d.amount accumulates total
// => 0 is initial value
// => Example: [{amount: 10}, {amount: 20}] → 30
// => Donate function
// => Adds new donation to list
const donate = (amount: number) => {
const newDonation = {
id: Date.now(), // => Unique ID from timestamp
// => Simple ID strategy (production: use UUID)
amount, // => Donation amount (shorthand property)
date: new Date().toLocaleString() // => Formatted timestamp
// => Example: '1/29/2026, 4:30:15 PM'
};
setDonations(prev => [...prev, newDonation]);
// => Add new donation to list immutably
// => Spread existing donations, append new one
// => Triggers re-render for components using this context
};
return (
<DonationContext.Provider value={{ totalDonations, donate, donations }}>
{/* => Provide donation state + derived total + donate function */}
{/* => Any descendant can access via useDonations() */}
{children}
</DonationContext.Provider>
);
}
// => Component using multiple contexts
// => Demonstrates composition: combines theme + language + donation
function DonationForm() {
// => Access all three contexts via custom hooks
// => Each hook provides access to one domain
const { theme } = useTheme();
// => Get current theme ('light' or 'dark')
// => Used for styling (background, text color)
const { language, setLanguage, t } = useLanguage();
// => Get language state, setter, and translation function
// => t() translates UI text to current language
const { totalDonations, donate, donations } = useDonations();
// => Get donation total, donate function, and full donation list
// => Separate concern from theme/language
const [amount, setAmount] = useState<number>(0);
// => Local state for donation amount input
// => Not shared via context (component-local)
const handleSubmit = (e: React.FormEvent) => {
// => Handle form submission
e.preventDefault();
// => Prevent page reload
if (amount > 0) {
// => Validate amount is positive
donate(amount);
// => Call donate from DonationContext
// => Adds to global donation list
setAmount(0);
// => Reset input to 0 after successful donation
}
};
return (
<div style={{
backgroundColor: theme === 'light' ? '#fff' : '#222',
// => Light theme: white background
// => Dark theme: dark gray background (#222)
color: theme === 'light' ? '#000' : '#fff',
// => Light theme: black text
// => Dark theme: white text (contrast)
padding: '20px',
minHeight: '100vh',
// => Fill viewport height minimum
direction: language === 'ar' ? 'rtl' : 'ltr'
// => RTL layout for Arabic (right-to-left)
// => LTR layout for English (left-to-right)
// => CSS property for i18n text direction
}}>
<h1>{t('title')}</h1>
{/* => Translate 'title' key to current language */}
{/* => en: 'Donation Form', ar: 'نموذج التبرع' */}
<form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
{/* => Form submission calls handleSubmit */}
<div style={{ marginBottom: '12px' }}>
<label>{t('amount')}: </label>
{/* => Translated label for amount field */}
<input
type="number"
value={amount}
{/* => Controlled input from local state */}
onChange={(e) => setAmount(Number(e.target.value))}
{/* => Convert string value to number */}
{/* => Update local amount state */}
min="0"
{/* => HTML5 validation: no negative amounts */}
style={{ padding: '8px', marginLeft: '8px' }}
/>
</div>
<button type="submit" style={{ padding: '8px 16px' }}>
{t('submit')}
{/* => Translated submit button text */}
</button>
</form>
<div style={{ marginBottom: '20px' }}>
<h3>Total Donations: ${totalDonations}</h3>
{/* => Display total from DonationContext */}
{/* => Derived state (sum of all amounts) */}
<h4>Recent Donations ({donations.length}):</h4>
{/* => Show count of all donations */}
<ul>
{donations.slice(-5).reverse().map(donation => (
// => slice(-5) gets last 5 donations
// => reverse() shows newest first
// => map() renders each as list item
<li key={donation.id}>
{/* => Unique key from donation ID */}
${donation.amount} - {donation.date}
{/* => Display amount and formatted timestamp */}
</li>
))}
</ul>
</div>
<div>
<button onClick={() => setLanguage(language === 'en' ? 'ar' : 'en')} style={{ marginRight: '8px', padding: '8px' }}>
{/* => Toggle language on click */}
{/* => Switches between English and Arabic */}
{t('language')}: {language === 'en' ? 'English' : 'العربية'}
{/* => Display current language name */}
</button>
</div>
</div>
);
}
// => App component using Theme context
// => Demonstrates accessing single context from multiple contexts available
function App() {
const { theme, toggleTheme } = useTheme();
// => Access only theme context
// => Language and Donation contexts available but not used here
return (
<div>
<button
onClick={toggleTheme}
{/* => Click toggles theme light ↔ dark */}
{/* => Calls toggleTheme from ThemeContext */}
style={{
position: 'fixed',
// => Fixed positioning (doesn't scroll with page)
top: '10px',
right: '10px',
// => Top-right corner position
padding: '8px 16px',
zIndex: 1000
// => High z-index ensures button stays on top
}}
>
{theme === 'light' ? '🌙 Dark' : '☀️ Light'}
{/* => Conditional button text based on current theme */}
{/* => Light theme shows "Dark" (toggle TO dark) */}
{/* => Dark theme shows "Light" (toggle TO light) */}
</button>
<DonationForm />
{/* => Main form component */}
{/* => Accesses all three contexts */}
</div>
);
}
// => Root with composed providers
// => Demonstrates provider composition pattern
function Root() {
return (
<ThemeProvider>
{/* => Provider 1: Theme */}
{/* => Outermost provider wraps all other providers */}
{/* => Makes theme available to all descendants */}
<LanguageProvider>
{/* => Provider 2: Language */}
{/* => Nested inside ThemeProvider */}
{/* => Has access to theme context + provides language context */}
<DonationProvider>
{/* => Provider 3: Donations */}
{/* => Innermost provider */}
{/* => Has access to theme + language contexts */}
{/* => Provides donation context */}
<App />
{/* => App and descendants have access to all three contexts */}
{/* => Context access flows from outer to inner */}
</DonationProvider>
</LanguageProvider>
</ThemeProvider>
);
// => Nested providers compose contexts
// => Each provider manages one concern (separation of concerns)
// => Order matters: outer providers rendered first
// => All children have access to ALL contexts
// => Alternative: use composition helper to reduce nesting
}
export default Root;Key Takeaway: Compose multiple contexts for separation of concerns. Each context manages one domain (theme, language, data). Nest providers to combine contexts. Components access only needed contexts.
Expected Output: Donation form with theme toggle, language switcher (English/Arabic with RTL), and donation tracking. Changing theme updates colors. Switching language updates text and layout direction. Donations tracked separately.
Common Pitfalls: Creating too many contexts (over-separation), deeply nesting providers (hard to read - consider composition helper), or sharing unrelated state in one context (violates single responsibility).
Example 14: Context with useReducer
Combine Context with useReducer for complex state management with actions.
import { createContext, useContext, useReducer, ReactNode } from 'react';
// => State type
interface Donation {
id: string;
donorName: string;
amount: number;
category: 'zakat' | 'sadaqah' | 'general';
date: string;
}
interface DonationState {
donations: Donation[];
totalAmount: number;
filter: 'all' | 'zakat' | 'sadaqah' | 'general';
}
// => Action types (discriminated union)
type DonationAction =
| { type: 'ADD_DONATION'; payload: Omit<Donation, 'id' | 'date'> }
| { type: 'REMOVE_DONATION'; payload: { id: string } }
| { type: 'SET_FILTER'; payload: { filter: DonationState['filter'] } }
| { type: 'CLEAR_ALL' };
// => Context type
interface DonationContextType {
state: DonationState;
dispatch: React.Dispatch<DonationAction>;
// => Expose dispatch for direct action dispatching
addDonation: (donation: Omit<Donation, 'id' | 'date'>) => void;
// => Helper function wrapping dispatch
removeDonation: (id: string) => void;
setFilter: (filter: DonationState['filter']) => void;
clearAll: () => void;
}
// => Reducer function
function donationReducer(state: DonationState, action: DonationAction): DonationState {
switch (action.type) {
case 'ADD_DONATION': {
// => Create new donation with ID and date
const newDonation: Donation = {
...action.payload,
id: Date.now().toString(), // => Generate unique ID
date: new Date().toISOString() // => Current timestamp
};
// => Calculate new total
const newTotal = state.totalAmount + action.payload.amount;
return {
...state,
donations: [...state.donations, newDonation],
// => Append new donation
totalAmount: newTotal
};
}
case 'REMOVE_DONATION': {
// => Find donation to remove
const donationToRemove = state.donations.find(d => d.id === action.payload.id);
if (!donationToRemove) return state; // => Not found, return unchanged
// => Calculate new total
const newTotal = state.totalAmount - donationToRemove.amount;
return {
...state,
donations: state.donations.filter(d => d.id !== action.payload.id),
// => Remove matching donation
totalAmount: newTotal
};
}
case 'SET_FILTER':
return {
...state,
filter: action.payload.filter
};
case 'CLEAR_ALL':
// => Reset to initial state
return {
donations: [],
totalAmount: 0,
filter: 'all'
};
default:
// => TypeScript ensures exhaustive checking
return state;
}
}
// => Initial state
const initialState: DonationState = {
donations: [],
totalAmount: 0,
filter: 'all'
};
// => Create context
const DonationContext = createContext<DonationContextType | undefined>(undefined);
// => Custom hook
function useDonationContext() {
const context = useContext(DonationContext);
if (!context) throw new Error('useDonationContext must be used within DonationProvider');
return context;
}
// => Provider component
function DonationProvider({ children }: { children: ReactNode }) {
// => useReducer with reducer function and initial state
const [state, dispatch] = useReducer(donationReducer, initialState);
// => state is DonationState
// => dispatch is function to trigger actions
// => Helper functions wrapping dispatch
const addDonation = (donation: Omit<Donation, 'id' | 'date'>) => {
dispatch({ type: 'ADD_DONATION', payload: donation });
// => Dispatches action with payload
};
const removeDonation = (id: string) => {
dispatch({ type: 'REMOVE_DONATION', payload: { id } });
};
const setFilter = (filter: DonationState['filter']) => {
dispatch({ type: 'SET_FILTER', payload: { filter } });
};
const clearAll = () => {
dispatch({ type: 'CLEAR_ALL' });
};
// => Context value
const value: DonationContextType = {
state,
dispatch,
addDonation,
removeDonation,
setFilter,
clearAll
};
return (
<DonationContext.Provider value={value}>
{children}
</DonationContext.Provider>
);
}
// => Component: Donation form
function DonationForm() {
const { addDonation } = useDonationContext();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const donorName = formData.get('donorName') as string;
const amount = Number(formData.get('amount'));
const category = formData.get('category') as 'zakat' | 'sadaqah' | 'general';
addDonation({ donorName, amount, category });
// => Calls helper function
// => Triggers reducer action
e.currentTarget.reset(); // => Clear form
};
return (
<form onSubmit={handleSubmit} style={{ marginBottom: '20px', padding: '16px', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
<h3>Add Donation</h3>
<div style={{ marginBottom: '12px' }}>
<label>Donor Name: </label>
<input name="donorName" type="text" required style={{ padding: '4px' }} />
</div>
<div style={{ marginBottom: '12px' }}>
<label>Amount ($): </label>
<input name="amount" type="number" min="1" required style={{ padding: '4px' }} />
</div>
<div style={{ marginBottom: '12px' }}>
<label>Category: </label>
<select name="category" required style={{ padding: '4px' }}>
<option value="zakat">Zakat</option>
<option value="sadaqah">Sadaqah</option>
<option value="general">General</option>
</select>
</div>
<button type="submit" style={{ padding: '8px 16px' }}>Add Donation</button>
</form>
);
}
// => Component: Donation list
function DonationList() {
const { state, removeDonation, setFilter, clearAll } = useDonationContext();
// => Filter donations based on current filter
const filteredDonations = state.filter === 'all'
? state.donations
: state.donations.filter(d => d.category === state.filter);
return (
<div style={{ padding: '16px' }}>
<div style={{ marginBottom: '16px' }}>
<h3>Total Donations: ${state.totalAmount.toFixed(2)}</h3>
<div style={{ marginBottom: '12px' }}>
<label>Filter: </label>
<select
value={state.filter}
onChange={(e) => setFilter(e.target.value as DonationState['filter'])}
style={{ padding: '4px', marginLeft: '8px' }}
>
<option value="all">All</option>
<option value="zakat">Zakat</option>
<option value="sadaqah">Sadaqah</option>
<option value="general">General</option>
</select>
</div>
<button onClick={clearAll} style={{ padding: '8px 16px', backgroundColor: '#CC78BC' }}>
Clear All Donations
</button>
</div>
<h3>Donations ({filteredDonations.length})</h3>
{filteredDonations.length === 0 ? (
<p>No donations yet.</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
{filteredDonations.map(donation => (
<li key={donation.id} style={{
padding: '12px',
marginBottom: '8px',
backgroundColor: '#f9f9f9',
borderRadius: '4px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<strong>{donation.donorName}</strong> - ${donation.amount}
<br />
<small>{donation.category} - {new Date(donation.date).toLocaleString()}</small>
</div>
<button
onClick={() => removeDonation(donation.id)}
style={{ padding: '4px 12px', backgroundColor: '#DE8F05' }}
>
Remove
</button>
</li>
))}
</ul>
)}
</div>
);
}
// => Main app
function App() {
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<h1>Donation Management System</h1>
<DonationForm />
<DonationList />
</div>
);
}
// => Root with provider
function Root() {
return (
<DonationProvider>
<App />
</DonationProvider>
);
}
export default Root;Key Takeaway: Combine Context with useReducer for complex state management. Reducer centralizes state logic. Actions provide type-safe state transitions. Context distributes state and dispatch across app.
Expected Output: Donation management system with form and list. Add donations with name, amount, category. Filter by category. Remove individual donations or clear all. Total updates automatically.
Common Pitfalls: Not typing actions properly (lose type safety), exposing only dispatch (consumers must know action structure), or using this pattern for simple state (useState simpler).
Example 15: Authentication Context Example
Complete authentication context with login, logout, and protected routes logic.
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
// => User type
interface User {
id: string;
username: string;
email: string;
role: 'user' | 'admin';
token: string; // => JWT token (mock)
}
// => Auth context type
interface AuthContextType {
user: User | null;
isLoading: boolean; // => Loading state during initialization
isAuthenticated: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
checkPermission: (requiredRole: 'user' | 'admin') => boolean;
}
// => Create context
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// => Custom hook
function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
// => Storage key for token
const TOKEN_KEY = 'auth_token';
// => Provider component
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// => Loading true during initialization
// => Initialize: check for stored token on mount
useEffect(() => {
const initAuth = async () => {
try {
const storedToken = localStorage.getItem(TOKEN_KEY);
// => Try to get token from localStorage
if (storedToken) {
// => Token found, validate and load user
console.log('Token found, loading user...');
// => Simulate API call to validate token
await new Promise(resolve => setTimeout(resolve, 1000));
// => 1 second delay to simulate network
// => Mock: decode token and load user
const mockUser: User = {
id: '1',
username: 'donor_user',
email: 'user@example.com',
role: 'user',
token: storedToken
};
setUser(mockUser);
console.log('User loaded from token');
}
} catch (error) {
console.error('Auth initialization failed:', error);
localStorage.removeItem(TOKEN_KEY); // => Clear invalid token
} finally {
setIsLoading(false); // => Initialization complete
}
};
initAuth();
}, []);
// => Empty dependency: runs once on mount
// => Login function
const login = async (username: string, password: string) => {
try {
setIsLoading(true);
console.log('Logging in...', { username, password });
// => Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// => Mock: check credentials
if (password !== 'password123') {
throw new Error('Invalid credentials');
}
// => Mock: generate token
const mockToken = `token_${Date.now()}`;
// => Mock: create user object
const mockUser: User = {
id: Date.now().toString(),
username,
email: `${username}@example.com`,
role: username === 'admin' ? 'admin' : 'user',
// => Admin role if username is 'admin'
token: mockToken
};
// => Store token
localStorage.setItem(TOKEN_KEY, mockToken);
// => Update state
setUser(mockUser);
console.log('Login successful:', mockUser);
} catch (error) {
console.error('Login failed:', error);
throw error; // => Re-throw for component handling
} finally {
setIsLoading(false);
}
};
// => Logout function
const logout = () => {
console.log('Logging out...');
setUser(null);
localStorage.removeItem(TOKEN_KEY); // => Clear stored token
};
// => Permission check helper
const checkPermission = (requiredRole: 'user' | 'admin'): boolean => {
if (!user) return false; // => Not authenticated
if (requiredRole === 'admin') {
return user.role === 'admin'; // => Admin access only
}
return true; // => User access (both user and admin)
};
const isAuthenticated = user !== null;
const value: AuthContextType = {
user,
isLoading,
isAuthenticated,
login,
logout,
checkPermission
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// => Login component
function LoginPage() {
const { login } = useAuth();
const [error, setError] = useState<string>('');
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setError('');
const formData = new FormData(e.currentTarget);
const username = formData.get('username') as string;
const password = formData.get('password') as string;
try {
await login(username, password);
// => On success, user state updates, UI re-renders
} catch (err) {
setError('Login failed. Use password: password123');
}
};
return (
<div style={{ maxWidth: '400px', margin: '50px auto', padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h2>Login</h2>
{error && (
<div style={{ padding: '12px', backgroundColor: '#ffebee', color: '#c62828', borderRadius: '4px', marginBottom: '16px' }}>
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>Username:</label>
<input name="username" type="text" required style={{ width: '100%', padding: '8px' }} />
<small>Try: "donor" or "admin"</small>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>Password:</label>
<input name="password" type="password" required style={{ width: '100%', padding: '8px' }} />
<small>Use: "password123"</small>
</div>
<button type="submit" style={{ width: '100%', padding: '12px', backgroundColor: '#0173B2', color: '#fff', border: 'none', borderRadius: '4px' }}>
Login
</button>
</form>
</div>
);
}
// => Protected dashboard
function Dashboard() {
const { user, logout, checkPermission } = useAuth();
const isAdmin = checkPermission('admin');
return (
<div style={{ padding: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h1>Dashboard</h1>
<button onClick={logout} style={{ padding: '8px 16px' }}>Logout</button>
</div>
<div style={{ padding: '16px', backgroundColor: '#f5f5f5', borderRadius: '8px', marginBottom: '16px' }}>
<h2>Welcome, {user?.username}!</h2>
<p>Email: {user?.email}</p>
<p>Role: {user?.role}</p>
</div>
<div style={{ padding: '16px', backgroundColor: '#e3f2fd', borderRadius: '8px', marginBottom: '16px' }}>
<h3>User Dashboard</h3>
<p>This content is visible to all authenticated users.</p>
</div>
{isAdmin && (
<div style={{ padding: '16px', backgroundColor: '#fff3e0', borderRadius: '8px' }}>
<h3>Admin Panel</h3>
<p>This content is only visible to administrators.</p>
<p>You have elevated privileges.</p>
</div>
)}
</div>
);
}
// => Main app with conditional rendering
function App() {
const { isAuthenticated, isLoading } = useAuth();
// => Show loading during initialization
if (isLoading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<p>Loading...</p>
</div>
);
}
// => Show login or dashboard based on auth state
return isAuthenticated ? <Dashboard /> : <LoginPage />;
}
// => Root with provider
function Root() {
return (
<AuthProvider>
<App />
</AuthProvider>
);
}
export default Root;Key Takeaway: Authentication context manages user state, login/logout logic, and permission checks. Initialize from localStorage for persistence. Use isLoading for initialization state. Conditional rendering based on authentication.
Expected Output: Login page with username/password fields. After login (password: “password123”), shows dashboard with user info. Admin users see additional admin panel. Logout returns to login page. Token persists across refreshes.
Common Pitfalls: Not handling initialization loading state (flash of wrong content), storing sensitive data in context (use secure storage), or forgetting token expiration logic (add refresh mechanism in production).
Group 4: Data Fetching and API Integration (React Query - Examples 16-20)
Note: These examples require @tanstack/react-query package. Install with:
npm install @tanstack/react-queryExample 16: React Query Basics
React Query simplifies server state management with automatic caching, refetching, and background updates.
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
// => Create QueryClient instance
// => Manages cache and default options
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1, // => Retry failed requests once
staleTime: 60000, // => Data fresh for 60 seconds
cacheTime: 300000, // => Cache for 5 minutes
},
},
});
// => Configure globally for all queries
// => Type for fetched donation data
interface Donation {
id: number;
userId: number;
title: string;
body: string;
}
// => Fetch function
// => Standard async function that returns Promise
async function fetchDonations(): Promise<Donation[]> {
const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
// => Mock API call (returns posts, treating as donations)
if (!response.ok) {
throw new Error('Network response was not ok');
// => Throw error for React Query to catch
}
return response.json();
// => Return parsed JSON
}
// => Component using React Query
function DonationList() {
// => useQuery hook for data fetching
// => First parameter: unique query key (array)
// => Second parameter: fetch function
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['donations'], // => Unique identifier for this query
// => React Query caches by this key
// => Use same key to access cached data elsewhere
queryFn: fetchDonations, // => Function that fetches data
// => Must return Promise
});
// => Returns query state object
// => Loading state
if (isLoading) {
return (
<div style={{ padding: '20px' }}>
<p>Loading donations...</p>
{/* => Shown while fetching initial data */}
</div>
);
}
// => Error state
if (isError) {
return (
<div style={{ padding: '20px' }}>
<p style={{ color: 'red' }}>Error: {error.message}</p>
{/* => Display error message */}
<button onClick={() => refetch()} style={{ padding: '8px 16px', marginTop: '12px' }}>
Retry
</button>
{/* => refetch() triggers manual refetch */}
</div>
);
}
// => Success state
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2>Recent Donations</h2>
<button onClick={() => refetch()} style={{ padding: '8px 16px' }}>
Refresh
</button>
{/* => Manual refetch button */}
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{data?.map(donation => (
<li key={donation.id} style={{
padding: '16px',
marginBottom: '12px',
backgroundColor: '#f5f5f5',
borderRadius: '8px'
}}>
<h3>{donation.title}</h3>
<p>{donation.body}</p>
<small>Donation ID: {donation.id} | User ID: {donation.userId}</small>
</li>
))}
</ul>
<div style={{ marginTop: '20px', fontSize: '0.875rem', color: '#666' }}>
<p><strong>React Query Features:</strong></p>
<ul>
<li>✅ Automatic caching (data cached for 5 minutes)</li>
<li>✅ Background refetching (refetches on window focus)</li>
<li>✅ Automatic retry (retries once on failure)</li>
<li>✅ Stale data (data stays fresh for 60 seconds)</li>
</ul>
</div>
</div>
);
}
// => App component
function App() {
return (
<div>
<h1 style={{ textAlign: 'center' }}>Donation Dashboard</h1>
<DonationList />
</div>
);
}
// => Root component with QueryClientProvider
// => CRITICAL: Wrap app with provider
function Root() {
return (
<QueryClientProvider client={queryClient}>
{/* => Provides React Query functionality to all children */}
{/* => queryClient instance manages cache and config */}
<App />
</QueryClientProvider>
);
}
export default Root;Key Takeaway: React Query manages server state with automatic caching, refetching, and error handling. useQuery hook takes query key and fetch function. Returns loading, error, and data states. QueryClientProvider required at app root.
Expected Output: List of 5 donations from mock API. Loading state shown initially. Refresh button manually refetches. Data cached automatically. Switch to another tab and back to see background refetch.
Common Pitfalls: Forgetting QueryClientProvider (hooks won’t work), not making query key unique (cache collisions), or using wrong key type (must be array).
Continuation marker for next group of examples
Example 17: React Query Mutations
Mutations handle data modifications (POST, PUT, DELETE). Use useMutation for create/update/delete operations.
import { useMutation, useQuery, useQueryClient, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';
const queryClient = new QueryClient();
// => Type for donation
interface Donation {
id: number;
title: string;
body: string;
userId: number;
}
// => Fetch donations
async function fetchDonations(): Promise<Donation[]> {
const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10');
if (!response.ok) throw new Error('Fetch failed');
return response.json();
}
// => Create donation function
async function createDonation(newDonation: Omit<Donation, 'id'>): Promise<Donation> {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newDonation),
});
// => POST request with JSON body
if (!response.ok) throw new Error('Create failed');
return response.json();
// => Returns created donation with ID
}
// => Delete donation function
async function deleteDonation(id: number): Promise<void> {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Delete failed');
}
function DonationManager() {
const queryClientInstance = useQueryClient();
// => Access QueryClient for cache manipulation
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
// => Query for fetching donations
const { data: donations, isLoading } = useQuery({
queryKey: ['donations'],
queryFn: fetchDonations,
});
// => Mutation for creating donation
const createMutation = useMutation({
mutationFn: createDonation,
// => Function that performs mutation
onSuccess: (newDonation) => {
// => Called when mutation succeeds
console.log('Donation created:', newDonation);
// => Invalidate donations query to trigger refetch
queryClientInstance.invalidateQueries({ queryKey: ['donations'] });
// => This refetches donations list
// => Ensures UI shows latest data
// => Clear form
setTitle('');
setBody('');
},
onError: (error) => {
// => Called when mutation fails
console.error('Create failed:', error);
alert('Failed to create donation');
},
});
// => Mutation for deleting donation
const deleteMutation = useMutation({
mutationFn: deleteDonation,
onSuccess: (_, deletedId) => {
// => Second parameter is variables passed to mutation
console.log('Donation deleted:', deletedId);
// => Invalidate to refetch
queryClientInstance.invalidateQueries({ queryKey: ['donations'] });
},
onError: (error) => {
console.error('Delete failed:', error);
alert('Failed to delete donation');
},
});
// => Handle form submission
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title || !body) {
alert('Please fill all fields');
return;
}
// => Trigger mutation
createMutation.mutate({
title,
body,
userId: 1, // => Mock user ID
});
// => mutate() executes mutation function
};
// => Handle delete
const handleDelete = (id: number) => {
if (confirm('Delete this donation?')) {
deleteMutation.mutate(id);
// => Pass ID to mutation function
}
};
if (isLoading) return <div style={{ padding: '20px' }}>Loading...</div>;
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h2>Donation Manager</h2>
{/* => Create form */}
<form onSubmit={handleSubmit} style={{ marginBottom: '20px', padding: '16px', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
<h3>Create New Donation</h3>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>Title:</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
style={{ width: '100%', padding: '8px' }}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>Description:</label>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
rows={4}
style={{ width: '100%', padding: '8px' }}
/>
</div>
<button
type="submit"
disabled={createMutation.isPending}
// => Disable button during mutation
style={{ padding: '8px 16px', backgroundColor: '#0173B2', color: '#fff', border: 'none', borderRadius: '4px' }}
>
{createMutation.isPending ? 'Creating...' : 'Create Donation'}
{/* => Show loading state */}
</button>
</form>
{/* => Donations list */}
<div>
<h3>Donations ({donations?.length})</h3>
<ul style={{ listStyle: 'none', padding: 0 }}>
{donations?.map(donation => (
<li key={donation.id} style={{
padding: '16px',
marginBottom: '12px',
backgroundColor: '#fff',
border: '1px solid #ddd',
borderRadius: '8px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<h4 style={{ margin: '0 0 8px 0' }}>{donation.title}</h4>
<p style={{ margin: 0 }}>{donation.body}</p>
</div>
<button
onClick={() => handleDelete(donation.id)}
disabled={deleteMutation.isPending}
style={{ padding: '8px 12px', backgroundColor: '#DE8F05', color: '#fff', border: 'none', borderRadius: '4px' }}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</button>
</li>
))}
</ul>
</div>
</div>
);
}
function Root() {
return (
<QueryClientProvider client={queryClient}>
<DonationManager />
</QueryClientProvider>
);
}
export default Root;Key Takeaway: useMutation handles data modifications (POST, PUT, DELETE). Provides isPending state, mutate function, and callbacks (onSuccess, onError). Invalidate queries after mutations to refetch updated data.
Expected Output: Donation manager with create form and list. Creating donation adds to list (mock API). Deleting removes from list. Form disables during creation. Buttons show loading states.
Common Pitfalls: Not invalidating queries after mutation (stale data), forgetting error handling (silent failures), or not disabling UI during mutation (duplicate requests).
Example 18: Optimistic Updates with React Query
Optimistic updates immediately update UI before server response, rolling back on error.
import { useMutation, useQuery, useQueryClient, QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
interface Todo {
id: number;
text: string;
completed: boolean;
}
async function fetchTodos(): Promise<Todo[]> {
const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5');
if (!response.ok) throw new Error('Fetch failed');
const data = await response.json();
// => Map API data to our Todo type
return data.map((item: any) => ({
id: item.id,
text: item.title,
completed: item.completed,
}));
}
async function toggleTodo(id: number): Promise<void> {
// => Simulate API call with delay
await new Promise(resolve => setTimeout(resolve, 1000));
// => Simulate 20% failure rate
if (Math.random() < 0.2) {
throw new Error('Network error');
}
// => Mock: API would update todo
console.log('Todo toggled on server:', id);
}
function OptimisticTodoList() {
const queryClientInstance = useQueryClient();
const { data: todos, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
// => Mutation with optimistic update
const toggleMutation = useMutation({
mutationFn: toggleTodo,
// => onMutate runs BEFORE mutation function
// => Perfect for optimistic updates
onMutate: async (todoId) => {
// => Cancel outgoing refetches
// => Prevents race condition with optimistic update
await queryClientInstance.cancelQueries({ queryKey: ['todos'] });
// => Snapshot current state for rollback
const previousTodos = queryClientInstance.getQueryData<Todo[]>(['todos']);
// => Save current todos for rollback on error
// => Optimistically update cache
queryClientInstance.setQueryData<Todo[]>(['todos'], (old) => {
if (!old) return old;
// => Toggle completed status immediately
return old.map(todo =>
todo.id === todoId
? { ...todo, completed: !todo.completed }
// => Flip completed for matching todo
: todo
);
});
// => UI updates immediately
// => User sees instant feedback
// => Return context for rollback
return { previousTodos };
// => Passed to onError for rollback
},
// => onError runs if mutation fails
onError: (err, todoId, context) => {
// => Rollback optimistic update
if (context?.previousTodos) {
queryClientInstance.setQueryData(['todos'], context.previousTodos);
// => Restore previous state
// => UI reverts to correct state
}
console.error('Toggle failed, rolled back:', err);
alert('Failed to update todo. Changes reverted.');
},
// => onSettled runs after success or error
onSettled: () => {
// => Refetch to sync with server
queryClientInstance.invalidateQueries({ queryKey: ['todos'] });
// => Ensures eventual consistency
},
});
if (isLoading) return <div style={{ padding: '20px' }}>Loading...</div>;
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Todo List (Optimistic Updates)</h2>
<div style={{ marginBottom: '16px', padding: '12px', backgroundColor: '#e3f2fd', borderRadius: '4px' }}>
<p style={{ margin: 0 }}><strong>Try it:</strong> Toggle todos to see instant updates. 20% chance of failure with rollback.</p>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos?.map(todo => (
<li key={todo.id} style={{
padding: '12px',
marginBottom: '8px',
backgroundColor: '#f5f5f5',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleMutation.mutate(todo.id)}
// => Triggers optimistic update immediately
disabled={toggleMutation.isPending}
style={{ width: '20px', height: '20px' }}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none',
flex: 1
}}>
{todo.text}
</span>
{toggleMutation.isPending && toggleMutation.variables === todo.id && (
<small style={{ color: '#666' }}>Updating...</small>
)}
</li>
))}
</ul>
<div style={{ marginTop: '20px', fontSize: '0.875rem', color: '#666' }}>
<p><strong>Optimistic Update Flow:</strong></p>
<ol>
<li>User clicks checkbox → UI updates instantly (optimistic)</li>
<li>API request sent in background</li>
<li>On success: Change confirmed, no visible change</li>
<li>On error: UI reverts (rollback), user notified</li>
</ol>
</div>
</div>
);
}
function Root() {
return (
<QueryClientProvider client={queryClient}>
<OptimisticTodoList />
</QueryClientProvider>
);
}
export default Root;Key Takeaway: Optimistic updates improve perceived performance by updating UI immediately. Use onMutate for optimistic update, save previous state for rollback. Use onError to rollback on failure. Use onSettled to refetch for consistency.
Expected Output: Todo list with 5 items. Clicking checkbox toggles immediately (optimistic). 1 second delay simulates API. 20% chance of failure with rollback and alert. UI feels instant despite network delay.
Common Pitfalls: Not cancelling queries (race conditions), forgetting rollback context (can’t revert), or not refetching in onSettled (data divergence).
Example 19: Infinite Scrolling with React Query
useInfiniteQuery handles paginated data with automatic loading of next pages.
import { useInfiniteQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
const queryClient = new QueryClient();
interface Donation {
id: number;
title: string;
body: string;
}
interface PageResponse {
data: Donation[];
nextPage: number | undefined; // => Next page number or undefined if last page
}
// => Fetch function with pagination
async function fetchDonationPage({ pageParam = 1 }): Promise<PageResponse> {
// => pageParam provided by React Query
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${pageParam}&_limit=10`
);
// => Fetch 10 items per page
if (!response.ok) throw new Error('Fetch failed');
const data = await response.json();
// => Return data + next page info
return {
data,
nextPage: data.length === 10 ? pageParam + 1 : undefined,
// => If received 10 items, there's next page
// => Otherwise, we're at last page
};
}
function InfiniteDonationList() {
// => Ref for intersection observer target
const loadMoreRef = useRef<HTMLDivElement>(null);
// => useInfiniteQuery for paginated data
const {
data,
fetchNextPage, // => Function to load next page
hasNextPage, // => Boolean: more pages available?
isFetchingNextPage, // => Boolean: loading next page?
isLoading,
isError,
error,
} = useInfiniteQuery({
queryKey: ['donations', 'infinite'],
queryFn: fetchDonationPage,
// => Fetch function receives pageParam
getNextPageParam: (lastPage) => lastPage.nextPage,
// => Extract next page number from response
// => Return undefined to indicate no more pages
initialPageParam: 1,
// => Start with page 1
});
// => Intersection Observer for infinite scroll
useEffect(() => {
if (!loadMoreRef.current || !hasNextPage) return;
// => Create intersection observer
const observer = new IntersectionObserver(
(entries) => {
// => Callback when target enters viewport
const first = entries[0];
if (first.isIntersecting && hasNextPage && !isFetchingNextPage) {
// => Target visible + more pages + not already loading
console.log('Loading next page...');
fetchNextPage();
// => Trigger next page fetch
}
},
{ threshold: 1.0 } // => Trigger when 100% visible
);
// => Observe target element
observer.observe(loadMoreRef.current);
// => Cleanup
return () => observer.disconnect();
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
// => Re-setup when dependencies change
// => Loading state
if (isLoading) {
return <div style={{ padding: '20px' }}>Loading initial donations...</div>;
}
// => Error state
if (isError) {
return (
<div style={{ padding: '20px' }}>
<p style={{ color: 'red' }}>Error: {error.message}</p>
</div>
);
}
// => Flatten pages into single array
const allDonations = data?.pages.flatMap(page => page.data) ?? [];
// => data.pages is array of PageResponse
// => flatMap extracts all donations into single array
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h2>Infinite Scrolling Donations</h2>
<p>Total loaded: {allDonations.length} donations</p>
<ul style={{ listStyle: 'none', padding: 0 }}>
{allDonations.map((donation, index) => (
<li key={`${donation.id}-${index}`} style={{
padding: '16px',
marginBottom: '12px',
backgroundColor: '#f5f5f5',
borderRadius: '8px'
}}>
<h3>Donation #{donation.id}</h3>
<p><strong>{donation.title}</strong></p>
<p>{donation.body}</p>
</li>
))}
</ul>
{/* => Load more trigger */}
<div ref={loadMoreRef} style={{ padding: '20px', textAlign: 'center' }}>
{isFetchingNextPage ? (
<p>Loading more donations...</p>
) : hasNextPage ? (
<p style={{ color: '#666' }}>Scroll to load more</p>
) : (
<p style={{ color: '#666' }}>No more donations to load</p>
)}
</div>
<div style={{ marginTop: '20px', fontSize: '0.875rem', color: '#666' }}>
<p><strong>Infinite Scrolling:</strong></p>
<ul>
<li>Loads 10 donations per page</li>
<li>Automatically fetches next page when scrolling to bottom</li>
<li>Uses Intersection Observer API</li>
<li>All data cached by React Query</li>
</ul>
</div>
</div>
);
}
function Root() {
return (
<QueryClientProvider client={queryClient}>
<InfiniteDonationList />
</QueryClientProvider>
);
}
export default Root;Key Takeaway: useInfiniteQuery handles paginated data with infinite scrolling. Use getNextPageParam to determine next page. Use Intersection Observer to trigger fetchNextPage when user scrolls to bottom. React Query manages page cache.
Expected Output: Donation list starting with 10 items. Scrolling to bottom automatically loads next page. Shows “Loading more…” during fetch. Continues until all pages loaded. Total count updates as pages load.
Common Pitfalls: Not handling getNextPageParam properly (infinite loop), forgetting cleanup for IntersectionObserver (memory leak), or triggering fetchNextPage when already loading (duplicate requests).
Example 20: Error Handling with React Query
Comprehensive error handling patterns with React Query: retry logic, error boundaries, and fallback UI.
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Component, ReactNode, useState } from 'react';
// => Custom error class for API errors
// => Extends Error to include HTTP status code
class ApiError extends Error {
constructor(public status: number, message: string) {
// => status: HTTP status code (404, 500, etc.)
// => message: Error description
super(message);
// => Call Error constructor with message
// => Sets this.message
this.name = 'ApiError';
// => Set error name for debugging
// => Appears in error logs and stack traces
}
}
const queryClient = new QueryClient({
// => Global query client configuration
// => Sets defaults for all queries in application
defaultOptions: {
queries: {
// => Default options for all useQuery calls
retry: (failureCount, error) => {
// => Custom retry logic function
// => failureCount: number of retries so far (0, 1, 2...)
// => error: the error that occurred
// => Return true to retry, false to stop
console.log(`Retry attempt ${failureCount} for error:`, error);
// => Log retry attempts for debugging
// => Don't retry 404 errors
// => 404 means resource doesn't exist - retrying won't help
if (error instanceof ApiError && error.status === 404) {
return false;
// => Stop retrying, show error to user immediately
}
// => Retry max 3 times for other errors
// => Network errors, 500s, etc. might be transient
return failureCount < 3;
// => failureCount starts at 0: retries at 0, 1, 2 → stops at 3
},
retryDelay: (attemptIndex) => {
// => Delay between retry attempts
// => attemptIndex: 0 (first retry), 1 (second), 2 (third)
// => Returns delay in milliseconds
return Math.min(1000 * 2 ** attemptIndex, 30000);
// => Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s...
// => 2 ** 0 = 1s, 2 ** 1 = 2s, 2 ** 2 = 4s
// => Math.min caps at 30 seconds maximum delay
// => Prevents infinite growth for many retries
},
},
},
});
// => Type for donation
interface Donation {
id: number;
title: string;
amount: number;
}
// => Fetch function with error handling
// => Demonstrates proper HTTP error handling patterns
async function fetchDonation(id: number): Promise<Donation> {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
// => Fetch donation data from API
// => await pauses until response received
// => fetch() doesn't throw on HTTP errors (4xx, 5xx)
// => Handle different HTTP errors
// => Check specific status codes for targeted error handling
if (response.status === 404) {
// => Resource not found error
// => User requested non-existent donation
throw new ApiError(404, 'Donation not found');
// => Throw custom error with status code
// => React Query catches and won't retry (per retry config)
}
if (response.status === 500) {
// => Internal server error
// => Server-side problem, not client's fault
throw new ApiError(500, 'Server error');
// => React Query WILL retry (not 404)
}
if (!response.ok) {
// => Generic HTTP error catch-all
// => response.ok is false for any status >= 400
// => Handles 401, 403, 502, etc.
throw new ApiError(response.status, `HTTP error: ${response.status}`);
// => Include status code in error message for debugging
}
const data = await response.json();
// => Parse JSON response body
// => await pauses until parsing complete
// => Assumes response is valid JSON
// => Transform to our type
// => API response might not match our TypeScript interface exactly
return {
id: data.id, // => Extract ID from API response
title: data.title, // => Extract title from API response
amount: Math.floor(Math.random() * 1000) + 100,
// => Mock amount (API doesn't have this field)
// => Random number between 100-1100
// => Math.floor() removes decimal, Math.random() * 1000 → 0-999
};
}
// => Error Boundary Component
// => Catches JavaScript errors in child component tree
// => Prevents entire app crash, shows fallback UI instead
interface ErrorBoundaryProps {
children: ReactNode; // => Child components to protect
fallback?: (error: Error, reset: () => void) => ReactNode;
// => Optional custom fallback UI renderer
// => Receives error and reset function
}
interface ErrorBoundaryState {
hasError: boolean; // => Whether error occurred
error: Error | null; // => The caught error object
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
// => Must use class component (Error Boundaries not supported in function components yet)
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
// => Initialize with no error state
// => hasError: false = normal operation
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
// => Lifecycle method called when error thrown in child
// => Runs during render phase (before commit)
// => Update state to trigger fallback UI render
return { hasError: true, error };
// => Sets error flag and stores error object
// => Triggers re-render with fallback UI
}
componentDidCatch(error: Error, errorInfo: any) {
// => Lifecycle method for side effects after error caught
// => Runs during commit phase (after render)
// => Perfect for logging errors to external service
console.error('Error Boundary caught:', error, errorInfo);
// => errorInfo contains component stack trace
// => In production: send to Sentry, LogRocket, etc.
}
reset = () => {
// => Reset error boundary to normal state
// => Allows user to retry after error
this.setState({ hasError: false, error: null });
// => Clear error state
// => Triggers re-render, attempts to render children again
};
render() {
if (this.state.hasError && this.state.error) {
// => Error occurred, render fallback UI
// => Render fallback UI
if (this.props.fallback) {
// => Use custom fallback if provided
// => Gives parent control over error UI
return this.props.fallback(this.state.error, this.reset);
// => Pass error and reset function to fallback
}
// => Default fallback UI
return (
<div style={{ padding: '20px', backgroundColor: '#ffebee', borderRadius: '8px' }}>
{/* => Light red background indicates error state */}
<h2>Something went wrong</h2>
<p>{this.state.error.message}</p>
{/* => Display error message to user */}
<button onClick={this.reset} style={{ padding: '8px 16px', marginTop: '12px' }}>
Try Again
</button>
{/* => Reset button clears error, re-renders children */}
</div>
);
}
return this.props.children;
// => No error, render children normally
}
}
// => Component with comprehensive error handling
// => Demonstrates React Query error handling + retry logic + custom error UI
function DonationDetails() {
const [donationId, setDonationId] = useState<number>(1);
// => Local state for which donation to fetch
// => Changing this triggers new query
const { data, isLoading, isError, error, refetch } = useQuery({
// => useQuery returns multiple states and functions
// => data: fetched data (undefined during loading/error)
// => isLoading: true during initial fetch
// => isError: true if fetch failed after retries
// => error: error object if failed
// => refetch: function to manually retry
queryKey: ['donation', donationId],
// => Unique key for this query
// => Changes when donationId changes → new fetch
// => React Query caches by this key
queryFn: () => fetchDonation(donationId),
// => Function that performs the fetch
// => Called automatically when key changes
// => React Query automatically retries with custom logic
// => Retry logic defined in queryClient config above
// => Custom error handling
throwOnError: false,
// => Don't throw errors up to Error Boundary
// => Handle in component instead (better UX)
// => Allows custom error UI per component
});
// => Loading state
// => Show spinner while fetching initial data
if (isLoading) {
return (
<div style={{ padding: '20px' }}>
<div style={{ display: 'inline-block', width: '40px', height: '40px', border: '4px solid #f3f3f3', borderTop: '4px solid #0173B2', borderRadius: '50%', animation: 'spin 1s linear infinite' }}>
{/* => CSS spinner (rotating circle) */}
{/* => Blue top border creates spinning effect */}
</div>
<p>Loading donation {donationId}...</p>
{/* => Show which donation being loaded */}
</div>
);
}
// => Error state with detailed handling
// => Show different UI based on error type
if (isError) {
const is404 = error instanceof ApiError && error.status === 404;
// => Check if error is 404 Not Found
// => Type guard ensures error.status exists
const is500 = error instanceof ApiError && error.status === 500;
// => Check if error is 500 Internal Server Error
// => Different errors need different user messaging
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<div style={{
padding: '20px',
backgroundColor: is404 ? '#fff3e0' : '#ffebee',
// => 404: Orange background (warning, not critical)
// => Other: Red background (error, more serious)
borderRadius: '8px',
border: `2px solid ${is404 ? '#ff9800' : '#f44336'}`,
// => Colored border matches severity
}}>
<h2 style={{ marginTop: 0 }}>
{is404 ? '🔍 Not Found' : is500 ? '⚠️ Server Error' : '❌ Error'}
{/* => Emoji + text based on error type */}
{/* => Clear visual differentiation */}
</h2>
<p><strong>Message:</strong> {error.message}</p>
{/* => Display error message from ApiError */}
{is404 && (
// => Conditional render: 404-specific help text
<p>The donation you're looking for doesn't exist.</p>
// => User-friendly explanation
)}
{is500 && (
// => Conditional render: 500-specific help text
<p>The server encountered an error. This is usually temporary.</p>
// => Reassure user, suggest retry
)}
<div style={{ marginTop: '16px', display: 'flex', gap: '12px' }}>
<button
onClick={() => refetch()}
// => Manual retry using refetch function
// => Useful for transient errors (network, 500)
style={{ padding: '8px 16px', backgroundColor: '#0173B2', color: '#fff', border: 'none', borderRadius: '4px' }}
>
Retry
</button>
<button
onClick={() => setDonationId(prev => prev + 1)}
// => Try next donation ID
// => Useful when current ID doesn't exist (404)
style={{ padding: '8px 16px', backgroundColor: '#666', color: '#fff', border: 'none', borderRadius: '4px' }}
>
Try Next ID
</button>
</div>
</div>
</div>
);
}
// => Success state
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<div style={{ padding: '20px', backgroundColor: '#e8f5e9', borderRadius: '8px', marginBottom: '20px' }}>
<h2 style={{ marginTop: 0 }}>Donation Details</h2>
<p><strong>ID:</strong> {data?.id}</p>
<p><strong>Title:</strong> {data?.title}</p>
<p><strong>Amount:</strong> ${data?.amount}</p>
</div>
<div style={{ display: 'flex', gap: '12px', marginBottom: '20px' }}>
<button
onClick={() => setDonationId(prev => Math.max(1, prev - 1))}
disabled={donationId === 1}
style={{ padding: '8px 16px' }}
>
Previous
</button>
<button
onClick={() => setDonationId(prev => prev + 1)}
style={{ padding: '8px 16px' }}
>
Next
</button>
<input
type="number"
value={donationId}
onChange={(e) => setDonationId(Number(e.target.value))}
min="1"
style={{ padding: '8px', width: '100px' }}
/>
</div>
<div style={{ fontSize: '0.875rem', color: '#666' }}>
<p><strong>Error Handling Features:</strong></p>
<ul>
<li>✅ Custom retry logic (max 3 retries with exponential backoff)</li>
<li>✅ Don't retry 404 errors (not found)</li>
<li>✅ Specific error messages for different error types</li>
<li>✅ Manual retry and alternative actions</li>
</ul>
<p>Try ID 999999 to see 404 error (no retry).</p>
</div>
</div>
);
}
function App() {
return (
<ErrorBoundary
fallback={(error, reset) => (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>🚨 Application Error</h1>
<p>{error.message}</p>
<button onClick={reset} style={{ padding: '12px 24px', marginTop: '16px' }}>
Reload Application
</button>
</div>
)}
>
<h1 style={{ textAlign: 'center' }}>Donation Viewer</h1>
<DonationDetails />
</ErrorBoundary>
);
}
function Root() {
return (
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
}
export default Root;Key Takeaway: React Query provides robust error handling with custom retry logic, error types, and manual retry. Use Error Boundaries for catastrophic errors. Provide specific error messages and recovery actions based on error types.
Expected Output: Donation viewer starting with ID 1. Navigate between donations. Try ID 999999 to see 404 error with no retry. Retry button refetches. Error messages vary by error type. Error boundary catches critical errors.
Common Pitfalls: Not differentiating error types (all errors treated same), retrying unrecoverable errors (wastes resources), or not providing recovery actions (poor UX).
Group 5: Routing and Error Handling (5 examples)
Note: These examples require react-router-dom package. Install with:
npm install react-router-domExample 21: React Router Setup
React Router provides client-side routing for single-page applications. Basic setup with BrowserRouter, Routes, and Route components.
import { BrowserRouter, Routes, Route, Link, useNavigate } from 'react-router-dom';
// => Page components
function HomePage() {
const navigate = useNavigate();
// => useNavigate hook for programmatic navigation
return (
<div style={{ padding: '20px' }}>
<h2>Home Page</h2>
<p>Welcome to the Donation Platform</p>
<div style={{ marginTop: '20px', display: 'flex', gap: '12px' }}>
<button onClick={() => navigate('/donations')} style={{ padding: '8px 16px' }}>
View Donations
</button>
{/* => navigate() changes route programmatically */}
<button onClick={() => navigate('/about')} style={{ padding: '8px 16px' }}>
About Us
</button>
</div>
</div>
);
}
function DonationsPage() {
return (
<div style={{ padding: '20px' }}>
<h2>Donations</h2>
<ul>
<li>Zakat Collection: \$5,000</li>
<li>Sadaqah Fund: \$3,200</li>
<li>General Donations: \$1,800</li>
</ul>
</div>
);
}
function AboutPage() {
return (
<div style={{ padding: '20px' }}>
<h2>About Us</h2>
<p>We facilitate Islamic charitable giving through transparent and Shariah-compliant processes.</p>
</div>
);
}
function ContactPage() {
return (
<div style={{ padding: '20px' }}>
<h2>Contact</h2>
<p>Email: contact@donations.example</p>
<p>Phone: +1 (555) 123-4567</p>
</div>
);
}
// => 404 Not Found page
function NotFoundPage() {
const navigate = useNavigate();
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<button onClick={() => navigate('/')} style={{ padding: '8px 16px', marginTop: '12px' }}>
Go Home
</button>
</div>
);
}
// => Navigation component
function Navigation() {
return (
<nav style={{
padding: '16px',
backgroundColor: '#0173B2',
color: '#fff',
display: 'flex',
gap: '20px'
}}>
{/* => Link component for navigation */}
{/* => Renders as <a> tag but prevents full page reload */}
<Link to="/" style={{ color: '#fff', textDecoration: 'none' }}>
Home
</Link>
{/* => to prop specifies destination route */}
<Link to="/donations" style={{ color: '#fff', textDecoration: 'none' }}>
Donations
</Link>
<Link to="/about" style={{ color: '#fff', textDecoration: 'none' }}>
About
</Link>
<Link to="/contact" style={{ color: '#fff', textDecoration: 'none' }}>
Contact
</Link>
</nav>
);
}
// => Main app component
function App() {
return (
<div>
<Navigation />
{/* => Routes container */}
{/* => Only one route renders at a time */}
<Routes>
{/* => Route component defines path and element */}
<Route path="/" element={<HomePage />} />
{/* => path: URL path to match */}
{/* => element: Component to render */}
<Route path="/donations" element={<DonationsPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
{/* => Catch-all route for 404 */}
<Route path="*" element={<NotFoundPage />} />
{/* => path="*" matches any unmatched route */}
</Routes>
</div>
);
}
// => Root component with BrowserRouter
function Root() {
return (
<BrowserRouter>
{/* => BrowserRouter enables routing */}
{/* => Uses HTML5 History API */}
{/* => Clean URLs without hash (#) */}
<App />
</BrowserRouter>
);
}
export default Root;Key Takeaway: React Router enables client-side routing. BrowserRouter provides routing context. Routes contains Route components. Link for declarative navigation, useNavigate for programmatic. Catch-all route (*) for 404 pages.
Expected Output: Navigation bar with 4 links. Clicking links changes page without full reload. Buttons use programmatic navigation. URL updates in browser. Invalid URLs show 404 page.
Common Pitfalls: Forgetting BrowserRouter wrapper (routes won’t work), using instead of Link (full page reload), or not providing catch-all route (blank page for invalid URLs).
Example 22: Dynamic Routes with Params
Dynamic routes accept URL parameters for flexible navigation. Use useParams hook to access route parameters.
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from 'react-router-dom';
import { useState } from 'react';
// => Type for donation
interface Donation {
id: string;
title: string;
amount: number;
category: 'zakat' | 'sadaqah' | 'general';
donor: string;
}
// => Mock donation database
const donations: Donation[] = [
{ id: '1', title: 'Zakat Payment', amount: 500, category: 'zakat', donor: 'Aisha Ahmed' },
{ id: '2', title: 'Sadaqah Jariyah', amount: 1000, category: 'sadaqah', donor: 'Omar Hassan' },
{ id: '3', title: 'General Donation', amount: 250, category: 'general', donor: 'Fatima Ali' },
{ id: '4', title: 'Ramadan Zakat', amount: 750, category: 'zakat', donor: 'Yusuf Ibrahim' },
];
// => List page
function DonationListPage() {
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h2>All Donations</h2>
<ul style={{ listStyle: 'none', padding: 0 }}>
{donations.map(donation => (
<li key={donation.id} style={{
padding: '16px',
marginBottom: '12px',
backgroundColor: '#f5f5f5',
borderRadius: '8px'
}}>
{/* => Link to dynamic route with ID parameter */}
<Link
to={`/donations/${donation.id}`}
// => Constructs URL with donation ID
// => Matches route pattern /donations/:id
style={{ textDecoration: 'none', color: '#0173B2', fontSize: '1.1rem', fontWeight: 'bold' }}
>
{donation.title}
</Link>
<p style={{ margin: '8px 0 0 0' }}>
Amount: ${donation.amount} | Category: {donation.category}
</p>
</li>
))}
</ul>
</div>
);
}
// => Detail page with dynamic route parameter
function DonationDetailPage() {
// => useParams extracts route parameters
const { id } = useParams<{ id: string }>();
// => id comes from URL path /donations/:id
// => TypeScript generic ensures type safety
const navigate = useNavigate();
// => Find donation by ID
const donation = donations.find(d => d.id === id);
// => Returns donation or undefined
// => Handle not found
if (!donation) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Donation Not Found</h2>
<p>No donation found with ID: {id}</p>
<button onClick={() => navigate('/donations')} style={{ padding: '8px 16px', marginTop: '12px' }}>
Back to List
</button>
</div>
);
}
// => Render donation details
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<button onClick={() => navigate('/donations')} style={{ padding: '8px 16px', marginBottom: '20px' }}>
← Back to List
</button>
<div style={{ padding: '20px', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
<h2>{donation.title}</h2>
<div style={{ marginTop: '16px' }}>
<p><strong>Donation ID:</strong> {donation.id}</p>
<p><strong>Amount:</strong> ${donation.amount}</p>
<p><strong>Category:</strong> {donation.category}</p>
<p><strong>Donor:</strong> {donation.donor}</p>
</div>
<div style={{ marginTop: '20px', padding: '12px', backgroundColor: '#e3f2fd', borderRadius: '4px' }}>
<p style={{ margin: 0 }}>
<strong>Receipt Number:</strong> {`RCP-${donation.category.toUpperCase()}-${donation.id.padStart(5, '0')}`}
</p>
</div>
</div>
{/* => Navigate to other donations */}
<div style={{ marginTop: '20px' }}>
<h3>Other Donations</h3>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
{donations
.filter(d => d.id !== id)
.map(d => (
<Link
key={d.id}
to={`/donations/${d.id}`}
// => Client-side navigation to different donation
style={{
padding: '8px 16px',
backgroundColor: '#0173B2',
color: '#fff',
textDecoration: 'none',
borderRadius: '4px'
}}
>
{d.title}
</Link>
))}
</div>
</div>
</div>
);
}
// => Category filter page (multiple params)
function CategoryPage() {
// => Extract category parameter
const { category } = useParams<{ category: string }>();
// => Filter donations by category
const filteredDonations = donations.filter(
d => d.category === category
);
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h2>Donations - {category}</h2>
<p>Showing {filteredDonations.length} donations in "{category}" category</p>
{filteredDonations.length === 0 ? (
<p>No donations found in this category.</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
{filteredDonations.map(donation => (
<li key={donation.id} style={{
padding: '16px',
marginBottom: '12px',
backgroundColor: '#f5f5f5',
borderRadius: '8px'
}}>
<Link to={`/donations/${donation.id}`} style={{ textDecoration: 'none', color: '#0173B2', fontWeight: 'bold' }}>
{donation.title}
</Link>
<p style={{ margin: '8px 0 0 0' }}>
${donation.amount} by {donation.donor}
</p>
</li>
))}
</ul>
)}
<div style={{ marginTop: '20px' }}>
<h3>Browse by Category</h3>
<div style={{ display: 'flex', gap: '12px' }}>
<Link to="/category/zakat" style={{ padding: '8px 16px', backgroundColor: '#029E73', color: '#fff', textDecoration: 'none', borderRadius: '4px' }}>
Zakat
</Link>
<Link to="/category/sadaqah" style={{ padding: '8px 16px', backgroundColor: '#DE8F05', color: '#000', textDecoration: 'none', borderRadius: '4px' }}>
Sadaqah
</Link>
<Link to="/category/general" style={{ padding: '8px 16px', backgroundColor: '#CC78BC', color: '#000', textDecoration: 'none', borderRadius: '4px' }}>
General
</Link>
</div>
</div>
</div>
);
}
function App() {
return (
<div>
<nav style={{ padding: '16px', backgroundColor: '#0173B2', color: '#fff' }}>
<Link to="/" style={{ color: '#fff', marginRight: '20px', textDecoration: 'none' }}>
Home
</Link>
<Link to="/donations" style={{ color: '#fff', marginRight: '20px', textDecoration: 'none' }}>
All Donations
</Link>
</nav>
<Routes>
<Route path="/" element={<div style={{ padding: '20px' }}><h1>Donation Platform</h1></div>} />
{/* => Static route */}
<Route path="/donations" element={<DonationListPage />} />
{/* => Dynamic route with :id parameter */}
<Route path="/donations/:id" element={<DonationDetailPage />} />
{/* => :id is route parameter */}
{/* => Accessible via useParams */}
{/* => Dynamic route with :category parameter */}
<Route path="/category/:category" element={<CategoryPage />} />
<Route path="*" element={<div style={{ padding: '20px' }}>404 - Not Found</div>} />
</Routes>
</div>
);
}
function Root() {
return (
<BrowserRouter>
<App />
</BrowserRouter>
);
}
export default Root;Key Takeaway: Dynamic routes use colon syntax (:id, :category) for URL parameters. useParams hook extracts parameter values. Links construct dynamic URLs. Enables detail pages, filtering, and flexible navigation patterns.
Expected Output: Donation list with clickable titles. Clicking opens detail page with URL like /donations/1. Back button returns to list. Category links filter donations. URL reflects current donation/category.
Common Pitfalls: Forgetting type parameter in useParams (loses type safety), not handling missing/invalid IDs (crashes on 404), or using string concatenation instead of template literals for URLs.
Example 23: Protected Routes Pattern
Implement authentication-based route protection. Redirect unauthenticated users to login page.
import { BrowserRouter, Routes, Route, Link, Navigate, useLocation } from 'react-router-dom';
import { createContext, useContext, useState, ReactNode } from 'react';
// => Auth context (from Example 15, simplified)
interface AuthContextType {
user: { name: string; role: 'user' | 'admin' } | null;
login: (username: string, role: 'user' | 'admin') => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<{ name: string; role: 'user' | 'admin' } | null>(null);
const login = (username: string, role: 'user' | 'admin') => {
setUser({ name: username, role });
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// => Protected Route wrapper component
interface ProtectedRouteProps {
children: ReactNode;
requiredRole?: 'user' | 'admin'; // => Optional role requirement
}
function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
const { user } = useAuth();
const location = useLocation();
// => useLocation gives current location info
// => Check authentication
if (!user) {
// => Not logged in: redirect to login
return <Navigate to="/login" state={{ from: location }} replace />;
// => Navigate component performs redirect
// => state: pass current location to return after login
// => replace: replace history entry (back button won't go to protected route)
}
// => Check role authorization
if (requiredRole && user.role !== requiredRole) {
// => User doesn't have required role
return <Navigate to="/unauthorized" replace />;
// => Redirect to unauthorized page
}
// => Authorized: render children
return <>{children}</>;
// => Renders protected component
}
// => Public pages
function LoginPage() {
const { login } = useAuth();
const location = useLocation();
const navigate = useNavigate();
// => Extract redirect location from state
const from = (location.state as any)?.from?.pathname || '/';
// => Default to home if no redirect location
const handleLogin = (role: 'user' | 'admin') => {
login(role === 'admin' ? 'Admin User' : 'Regular User', role);
// => Perform login
navigate(from, { replace: true });
// => Redirect to original destination or home
};
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: '50px auto', textAlign: 'center' }}>
<h2>Login Required</h2>
<p>Please log in to access the application</p>
<div style={{ marginTop: '20px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
<button
onClick={() => handleLogin('user')}
style={{ padding: '12px', backgroundColor: '#0173B2', color: '#fff', border: 'none', borderRadius: '4px' }}
>
Login as User
</button>
<button
onClick={() => handleLogin('admin')}
style={{ padding: '12px', backgroundColor: '#029E73', color: '#fff', border: 'none', borderRadius: '4px' }}
>
Login as Admin
</button>
</div>
</div>
);
}
function UnauthorizedPage() {
const navigate = useNavigate();
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>⛔ Unauthorized</h1>
<p>You don't have permission to access this page.</p>
<button onClick={() => navigate('/')} style={{ padding: '8px 16px', marginTop: '12px' }}>
Go Home
</button>
</div>
);
}
// => Protected pages
function DashboardPage() {
return (
<div style={{ padding: '20px' }}>
<h2>Dashboard</h2>
<p>This page is accessible to all authenticated users.</p>
<div style={{ padding: '16px', backgroundColor: '#e3f2fd', borderRadius: '8px', marginTop: '16px' }}>
<h3>Recent Donations</h3>
<ul>
<li>Zakat: \$500 - Aisha Ahmed</li>
<li>Sadaqah: \$250 - Omar Hassan</li>
<li>General: \$100 - Fatima Ali</li>
</ul>
</div>
</div>
);
}
function AdminPage() {
return (
<div style={{ padding: '20px' }}>
<h2>Admin Panel</h2>
<p>This page is only accessible to administrators.</p>
<div style={{ padding: '16px', backgroundColor: '#fff3e0', borderRadius: '8px', marginTop: '16px' }}>
<h3>Admin Functions</h3>
<ul>
<li>Manage Users</li>
<li>View Reports</li>
<li>Configure Settings</li>
<li>Approve Donations</li>
</ul>
</div>
</div>
);
}
// => Navigation
function Navigation() {
const { user, logout } = useAuth();
return (
<nav style={{
padding: '16px',
backgroundColor: '#0173B2',
color: '#fff',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div style={{ display: 'flex', gap: '20px' }}>
<Link to="/" style={{ color: '#fff', textDecoration: 'none' }}>
Home
</Link>
{user && (
<>
<Link to="/dashboard" style={{ color: '#fff', textDecoration: 'none' }}>
Dashboard
</Link>
{user.role === 'admin' && (
<Link to="/admin" style={{ color: '#fff', textDecoration: 'none' }}>
Admin
</Link>
)}
</>
)}
</div>
<div>
{user ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span>{user.name} ({user.role})</span>
<button onClick={logout} style={{ padding: '4px 12px', backgroundColor: '#fff', color: '#0173B2', border: 'none', borderRadius: '4px' }}>
Logout
</button>
</div>
) : (
<Link to="/login" style={{ color: '#fff', textDecoration: 'none' }}>
Login
</Link>
)}
</div>
</nav>
);
}
function App() {
return (
<div>
<Navigation />
<Routes>
{/* => Public routes */}
<Route path="/" element={<div style={{ padding: '20px' }}><h1>Welcome to Donation Platform</h1></div>} />
<Route path="/login" element={<LoginPage />} />
<Route path="/unauthorized" element={<UnauthorizedPage />} />
{/* => Protected route: requires authentication */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
{/* => Wraps component in ProtectedRoute */}
{/* => Redirects to /login if not authenticated */}
<DashboardPage />
</ProtectedRoute>
}
/>
{/* => Protected route: requires admin role */}
<Route
path="/admin"
element={
<ProtectedRoute requiredRole="admin">
{/* => requiredRole prop checks user role */}
{/* => Redirects to /unauthorized if not admin */}
<AdminPage />
</ProtectedRoute>
}
/>
<Route path="*" element={<div style={{ padding: '20px' }}>404 - Not Found</div>} />
</Routes>
</div>
);
}
function Root() {
return (
<BrowserRouter>
<AuthProvider>
{/* => AuthProvider wraps app for auth context */}
<App />
</AuthProvider>
</BrowserRouter>
);
}
export default Root;Key Takeaway: Protected routes check authentication before rendering. Use wrapper component with Navigate for redirects. Pass location state to return after login. Check user roles for authorization. Redirect unauthorized users to specific pages.
Expected Output: Public home page accessible to all. Dashboard and Admin links show when logged in. Accessing protected routes while logged out redirects to login. Admin page only accessible with admin role. After login, redirects to originally requested page.
Common Pitfalls: Not using replace flag (back button broken), forgetting location state (can’t return after login), or hardcoding redirects (inflexible pattern).
Example 24: Error Boundaries
Error Boundaries catch JavaScript errors in component tree and display fallback UI.
import { Component, ReactNode } from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
// => Error Boundary class component
// => Must be class component (not hooks-based yet)
interface ErrorBoundaryProps {
children: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
errorInfo: any;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
// => Static method called when error caught
// => Returns new state
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
// => Update state to trigger fallback UI
console.log('getDerivedStateFromError called');
return {
hasError: true,
error
};
}
// => Lifecycle method called after error caught
// => Used for logging errors
componentDidCatch(error: Error, errorInfo: any) {
// => Log error to error reporting service
console.error('Error caught by boundary:', error);
console.error('Component stack:', errorInfo.componentStack);
// => componentStack shows where error occurred
this.setState({ errorInfo });
// => In production, send to error tracking service
// => Example: Sentry.captureException(error, { extra: errorInfo });
}
// => Reset error state
resetError = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
if (this.state.hasError && this.state.error) {
// => Render fallback UI when error occurred
return (
<div style={{
padding: '20px',
margin: '20px',
backgroundColor: '#ffebee',
border: '2px solid #f44336',
borderRadius: '8px'
}}>
<h1 style={{ color: '#c62828' }}>🚨 Something went wrong</h1>
<div style={{ marginTop: '16px' }}>
<h3>Error Message:</h3>
<pre style={{
padding: '12px',
backgroundColor: '#fff',
borderRadius: '4px',
overflow: 'auto'
}}>
{this.state.error.toString()}
</pre>
</div>
{this.state.errorInfo && (
<details style={{ marginTop: '16px' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>
Component Stack (Click to expand)
</summary>
<pre style={{
padding: '12px',
backgroundColor: '#fff',
borderRadius: '4px',
overflow: 'auto',
fontSize: '0.875rem'
}}>
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
<div style={{ marginTop: '20px', display: 'flex', gap: '12px' }}>
<button
onClick={this.resetError}
style={{
padding: '8px 16px',
backgroundColor: '#0173B2',
color: '#fff',
border: 'none',
borderRadius: '4px'
}}
>
Try Again
</button>
<button
onClick={() => window.location.href = '/'}
style={{
padding: '8px 16px',
backgroundColor: '#666',
color: '#fff',
border: 'none',
borderRadius: '4px'
}}
>
Go Home
</button>
</div>
</div>
);
}
// => No error: render children normally
return this.props.children;
}
}
// => Component that might throw error
// => Used to demonstrate Error Boundary catching
function BuggyComponent({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) {
// => Intentionally throw error for demonstration
// => Error thrown during render phase
// => Caught by nearest ErrorBoundary
throw new Error('Intentional error for testing Error Boundary');
// => This prevents component from rendering
// => Error propagates up component tree
}
return (
<div style={{ padding: '16px', backgroundColor: '#e8f5e9', borderRadius: '8px' }}>
{/* => Green background indicates success */}
<h3>✅ Component Working</h3>
<p>This component is rendering successfully.</p>
{/* => Only renders when shouldThrow is false */}
</div>
);
}
// => Component with potential runtime error
// => Demonstrates error handling in event handlers
function DonationCalculator() {
const calculateZakat = (amount: string) => {
// => Function that might throw error
// => Called from event handler (button click)
// => Convert to number
const numAmount = parseFloat(amount);
// => parseFloat() converts string to number
// => Returns NaN if string invalid
// => Example: parseFloat('1000') → 1000, parseFloat('abc') → NaN
if (isNaN(numAmount)) {
// => Check if conversion failed
// => isNaN() returns true for NaN
// => Throw error for invalid input
throw new Error(`Invalid amount: "${amount}" is not a number`);
// => Error includes actual invalid input for debugging
// => Template literal shows what user entered
}
if (numAmount < 0) {
// => Business logic validation
// => Negative amounts don't make sense for Zakat
throw new Error('Amount cannot be negative');
// => Clear error message for user
}
return numAmount * 0.025; // => 2.5% Zakat rate
// => Calculate Zakat (Islamic wealth tax)
// => 2.5% is standard Zakat rate
// => Example: \$1000 → \$25 Zakat
};
return (
<div style={{ padding: '20px' }}>
<h3>Zakat Calculator</h3>
<div style={{ marginTop: '16px' }}>
<button
onClick={() => calculateZakat('1000')}
{/* => Click calls calculateZakat with valid input */}
{/* => Should succeed: \$1000 * 0.025 = \$25 */}
style={{ padding: '8px 16px', marginRight: '8px' }}
>
Calculate (Valid: \$1000)
</button>
<button
onClick={() => calculateZakat('invalid')}
{/* => Click calls calculateZakat with invalid input */}
{/* => Triggers error: 'invalid' is not a number */}
{/* => Error caught by nearest ErrorBoundary */}
style={{ padding: '8px 16px', marginRight: '8px', backgroundColor: '#DE8F05' }}
>
Calculate (Invalid Input)
{/* => Orange button warns this will error */}
</button>
<button
onClick={() => calculateZakat('-500')}
{/* => Click calls calculateZakat with negative input */}
{/* => Triggers error: 'Amount cannot be negative' */}
{/* => Demonstrates business logic validation */}
style={{ padding: '8px 16px', backgroundColor: '#CC78BC' }}
>
Calculate (Negative)
{/* => Purple button indicates this scenario */}
</button>
</div>
</div>
);
}
// => Page components
// => Each demonstrates different Error Boundary scenario
function SafePage() {
return (
<div style={{ padding: '20px' }}>
<h2>Safe Page</h2>
<ErrorBoundary>
{/* => Local Error Boundary for this section */}
{/* => Catches errors only from children */}
{/* => Doesn't affect rest of app if error occurs */}
<BuggyComponent shouldThrow={false} />
{/* => shouldThrow=false → component renders normally */}
{/* => No error thrown, boundary inactive */}
</ErrorBoundary>
</div>
);
}
function ErrorPage() {
return (
<div style={{ padding: '20px' }}>
<h2>Error Demo Page</h2>
<p>Click the button below to trigger an error:</p>
<ErrorBoundary>
{/* => Boundary catches error from BuggyComponent */}
{/* => Shows fallback UI when error thrown */}
<BuggyComponent shouldThrow={true} />
{/* => shouldThrow=true → throws error during render */}
{/* => Error caught by ErrorBoundary */}
{/* => Fallback UI displayed instead of component */}
</ErrorBoundary>
<div style={{ marginTop: '20px', padding: '16px', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
<p>This section is outside the error boundary and continues to work.</p>
{/* => Demonstrates error isolation */}
{/* => Error in boundary doesn't affect siblings */}
{/* => Rest of page still functional */}
</div>
</div>
);
}
function CalculatorPage() {
return (
<div style={{ padding: '20px' }}>
<h2>Calculator with Error Handling</h2>
<ErrorBoundary>
{/* => Boundary catches calculator errors */}
{/* => Errors from button clicks caught here */}
{/* => User can retry with "Try Again" button */}
<DonationCalculator />
</ErrorBoundary>
</div>
);
}
function App() {
return (
<div>
<nav style={{ padding: '16px', backgroundColor: '#0173B2', color: '#fff', display: 'flex', gap: '20px' }}>
{/* => Navigation bar with links to demo pages */}
{/* => Blue background from accessible color palette */}
<Link to="/" style={{ color: '#fff', textDecoration: 'none' }}>Home</Link>
{/* => Home page explains demo */}
<Link to="/safe" style={{ color: '#fff', textDecoration: 'none' }}>Safe Page</Link>
{/* => Page with no errors */}
<Link to="/error" style={{ color: '#fff', textDecoration: 'none' }}>Error Demo</Link>
{/* => Page that intentionally throws error */}
<Link to="/calculator" style={{ color: '#fff', textDecoration: 'none' }}>Calculator</Link>
{/* => Page with validation errors */}
</nav>
{/* => Global Error Boundary wraps entire app */}
{/* => Catches errors from all routes */}
{/* => Last line of defense before app crashes */}
<ErrorBoundary>
<Routes>
{/* => Route configuration */}
{/* => Each route renders different page component */}
<Route path="/" element={<div style={{ padding: '20px' }}><h1>Error Boundary Demo</h1><p>Navigate to different pages to see error handling.</p></div>} />
{/* => Home route with inline content */}
<Route path="/safe" element={<SafePage />} />
{/* => Safe page route (no errors) */}
<Route path="/error" element={<ErrorPage />} />
{/* => Error demo route (throws error) */}
<Route path="/calculator" element={<CalculatorPage />} />
{/* => Calculator route (validation errors) */}
<Route path="*" element={<div style={{ padding: '20px' }}>404 - Not Found</div>} />
{/* => Catch-all route for non-existent paths */}
{/* => Asterisk (*) matches any unmatched route */}
</Routes>
</ErrorBoundary>
</div>
);
}
function Root() {
return (
<BrowserRouter>
{/* => BrowserRouter provides routing context */}
{/* => Uses HTML5 history API for clean URLs */}
{/* => Wraps entire app to enable routing */}
<App />
{/* => Render main App component */}
{/* => App contains Routes and navigation */}
</BrowserRouter>
);
}
export default Root;Key Takeaway: Error Boundaries catch JavaScript errors in component tree and prevent entire app crash. Use class components with getDerivedStateFromError and componentDidCatch. Wrap sections with boundaries for isolation. Provide reset and recovery options in fallback UI.
Expected Output: Navigation with 4 pages. Safe page works normally. Error page triggers error, showing fallback UI with error message and component stack. Calculator page catches calculation errors. “Try Again” button resets error state. Rest of app continues working.
Common Pitfalls: Using functional components for boundaries (not supported yet), catching all errors in single boundary (can’t isolate), or not providing recovery mechanism (users stuck).
Example 25: Murabaha Contract Dashboard (Complete Financial Application)
Comprehensive example combining all intermediate concepts: Context, React Query, routing, error handling for Islamic finance application.
import { BrowserRouter, Routes, Route, Link, useParams, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { createContext, useContext, useState, ReactNode } from 'react';
// => Query Client configuration
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60000, // => 60 seconds
retry: 1,
},
},
});
// => Types
interface MurabahaContract {
id: string;
clientName: string;
principalAmount: number; // => Original financed amount
profitRate: number; // => Profit rate percentage
termMonths: number; // => Contract term in months
status: 'active' | 'completed' | 'pending';
createdAt: string;
}
// => Auth Context
interface User {
name: string;
role: 'client' | 'admin';
}
interface AuthContextType {
user: User | null;
login: (user: User) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
return (
<AuthContext.Provider value={{ user, login: setUser, logout: () => setUser(null) }}>
{children}
</AuthContext.Provider>
);
}
// => Mock API functions
const mockContracts: MurabahaContract[] = [
{
id: '1',
clientName: 'Aisha Ahmed',
principalAmount: 100000,
profitRate: 5,
termMonths: 12,
status: 'active',
createdAt: '2025-01-15T10:00:00Z'
},
{
id: '2',
clientName: 'Omar Hassan',
principalAmount: 50000,
profitRate: 4.5,
termMonths: 24,
status: 'active',
createdAt: '2025-01-20T14:30:00Z'
},
{
id: '3',
clientName: 'Fatima Ali',
principalAmount: 200000,
profitRate: 5.5,
termMonths: 36,
status: 'pending',
createdAt: '2025-01-25T09:15:00Z'
},
];
async function fetchContracts(): Promise<MurabahaContract[]> {
// => Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
return mockContracts;
}
async function fetchContractById(id: string): Promise<MurabahaContract> {
await new Promise(resolve => setTimeout(resolve, 800));
const contract = mockContracts.find(c => c.id === id);
if (!contract) throw new Error('Contract not found');
return contract;
}
async function createContract(data: Omit<MurabahaContract, 'id' | 'createdAt'>): Promise<MurabahaContract> {
await new Promise(resolve => setTimeout(resolve, 1000));
const newContract: MurabahaContract = {
...data,
id: (mockContracts.length + 1).toString(),
createdAt: new Date().toISOString(),
};
mockContracts.push(newContract);
return newContract;
}
// => Protected Route wrapper
function ProtectedRoute({ children }: { children: ReactNode }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}
// => Login Page
function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const handleLogin = (role: 'client' | 'admin') => {
login({
name: role === 'admin' ? 'Admin User' : 'Client User',
role
});
navigate('/');
};
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: '50px auto', textAlign: 'center' }}>
<h2>Murabaha Contract Management</h2>
<p>Login to continue</p>
<div style={{ marginTop: '20px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
<button
onClick={() => handleLogin('client')}
style={{ padding: '12px', backgroundColor: '#0173B2', color: '#fff', border: 'none', borderRadius: '4px' }}
>
Login as Client
</button>
<button
onClick={() => handleLogin('admin')}
style={{ padding: '12px', backgroundColor: '#029E73', color: '#fff', border: 'none', borderRadius: '4px' }}
>
Login as Admin
</button>
</div>
</div>
);
}
// => Contract List Page
function ContractListPage() {
const { data: contracts, isLoading, isError, error } = useQuery({
queryKey: ['contracts'],
queryFn: fetchContracts,
});
if (isLoading) {
return <div style={{ padding: '20px' }}>Loading contracts...</div>;
}
if (isError) {
return <div style={{ padding: '20px', color: 'red' }}>Error: {error.message}</div>;
}
// => Calculate totals
const totalPrincipal = contracts?.reduce((sum, c) => sum + c.principalAmount, 0) || 0;
const totalProfit = contracts?.reduce((sum, c) => sum + (c.principalAmount * (c.profitRate / 100)), 0) || 0;
return (
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<h2>Murabaha Contracts</h2>
<Link to="/contracts/new" style={{ padding: '8px 16px', backgroundColor: '#0173B2', color: '#fff', textDecoration: 'none', borderRadius: '4px' }}>
+ New Contract
</Link>
</div>
{/* => Summary cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px', marginBottom: '20px' }}>
<div style={{ padding: '16px', backgroundColor: '#e3f2fd', borderRadius: '8px' }}>
<h4 style={{ margin: '0 0 8px 0' }}>Total Contracts</h4>
<p style={{ margin: 0, fontSize: '2rem', fontWeight: 'bold' }}>{contracts?.length}</p>
</div>
<div style={{ padding: '16px', backgroundColor: '#e8f5e9', borderRadius: '8px' }}>
<h4 style={{ margin: '0 0 8px 0' }}>Total Principal</h4>
<p style={{ margin: 0, fontSize: '2rem', fontWeight: 'bold' }}>${totalPrincipal.toLocaleString()}</p>
</div>
<div style={{ padding: '16px', backgroundColor: '#fff3e0', borderRadius: '8px' }}>
<h4 style={{ margin: '0 0 8px 0' }}>Total Profit</h4>
<p style={{ margin: 0, fontSize: '2rem', fontWeight: 'bold' }}>${totalProfit.toLocaleString()}</p>
</div>
</div>
{/* => Contracts table */}
<div style={{ backgroundColor: '#fff', border: '1px solid #ddd', borderRadius: '8px', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead style={{ backgroundColor: '#f5f5f5' }}>
<tr>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '1px solid #ddd' }}>Client</th>
<th style={{ padding: '12px', textAlign: 'right', borderBottom: '1px solid #ddd' }}>Principal</th>
<th style={{ padding: '12px', textAlign: 'right', borderBottom: '1px solid #ddd' }}>Profit Rate</th>
<th style={{ padding: '12px', textAlign: 'right', borderBottom: '1px solid #ddd' }}>Term</th>
<th style={{ padding: '12px', textAlign: 'center', borderBottom: '1px solid #ddd' }}>Status</th>
<th style={{ padding: '12px', textAlign: 'center', borderBottom: '1px solid #ddd' }}>Actions</th>
</tr>
</thead>
<tbody>
{contracts?.map(contract => (
<tr key={contract.id} style={{ borderBottom: '1px solid #f0f0f0' }}>
<td style={{ padding: '12px' }}>{contract.clientName}</td>
<td style={{ padding: '12px', textAlign: 'right' }}>${contract.principalAmount.toLocaleString()}</td>
<td style={{ padding: '12px', textAlign: 'right' }}>{contract.profitRate}%</td>
<td style={{ padding: '12px', textAlign: 'right' }}>{contract.termMonths} months</td>
<td style={{ padding: '12px', textAlign: 'center' }}>
<span style={{
padding: '4px 12px',
borderRadius: '4px',
backgroundColor: contract.status === 'active' ? '#e8f5e9' : contract.status === 'completed' ? '#e3f2fd' : '#fff3e0',
color: '#000'
}}>
{contract.status}
</span>
</td>
<td style={{ padding: '12px', textAlign: 'center' }}>
<Link to={`/contracts/${contract.id}`} style={{ padding: '4px 12px', backgroundColor: '#0173B2', color: '#fff', textDecoration: 'none', borderRadius: '4px' }}>
View
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
// => Contract Detail Page
function ContractDetailPage() {
const { id } = useParams<{ id: string }>();
const { data: contract, isLoading, isError } = useQuery({
queryKey: ['contract', id],
queryFn: () => fetchContractById(id!),
enabled: !!id, // => Only fetch if id exists
});
if (isLoading) return <div style={{ padding: '20px' }}>Loading contract...</div>;
if (isError || !contract) return <div style={{ padding: '20px' }}>Contract not found</div>;
// => Calculate payment details
const totalProfit = contract.principalAmount * (contract.profitRate / 100);
const totalPayment = contract.principalAmount + totalProfit;
const monthlyPayment = totalPayment / contract.termMonths;
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<Link to="/" style={{ marginBottom: '20px', display: 'inline-block' }}>← Back to List</Link>
<h2>Contract Details</h2>
<div style={{ padding: '20px', backgroundColor: '#f5f5f5', borderRadius: '8px', marginBottom: '20px' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<div>
<p><strong>Contract ID:</strong> {contract.id}</p>
<p><strong>Client:</strong> {contract.clientName}</p>
<p><strong>Status:</strong> {contract.status}</p>
<p><strong>Created:</strong> {new Date(contract.createdAt).toLocaleDateString()}</p>
</div>
<div>
<p><strong>Principal Amount:</strong> ${contract.principalAmount.toLocaleString()}</p>
<p><strong>Profit Rate:</strong> {contract.profitRate}%</p>
<p><strong>Term:</strong> {contract.termMonths} months</p>
</div>
</div>
</div>
<div style={{ padding: '20px', backgroundColor: '#e8f5e9', borderRadius: '8px' }}>
<h3 style={{ marginTop: 0 }}>Payment Breakdown</h3>
<p><strong>Total Profit:</strong> ${totalProfit.toLocaleString()}</p>
<p><strong>Total Payment:</strong> ${totalPayment.toLocaleString()}</p>
<p><strong>Monthly Payment:</strong> ${monthlyPayment.toLocaleString()}</p>
</div>
</div>
);
}
// => Create Contract Page
function CreateContractPage() {
const queryClientInstance = useQueryClient();
const navigate = useNavigate();
const createMutation = useMutation({
mutationFn: createContract,
onSuccess: () => {
queryClientInstance.invalidateQueries({ queryKey: ['contracts'] });
navigate('/');
},
});
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createMutation.mutate({
clientName: formData.get('clientName') as string,
principalAmount: Number(formData.get('principalAmount')),
profitRate: Number(formData.get('profitRate')),
termMonths: Number(formData.get('termMonths')),
status: 'pending',
});
};
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Create New Murabaha Contract</h2>
<form onSubmit={handleSubmit} style={{ marginTop: '20px' }}>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>Client Name:</label>
<input name="clientName" type="text" required style={{ width: '100%', padding: '8px' }} />
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>Principal Amount ($):</label>
<input name="principalAmount" type="number" min="1000" required style={{ width: '100%', padding: '8px' }} />
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>Profit Rate (%):</label>
<input name="profitRate" type="number" min="0" max="20" step="0.1" required style={{ width: '100%', padding: '8px' }} />
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '4px' }}>Term (months):</label>
<input name="termMonths" type="number" min="6" max="60" required style={{ width: '100%', padding: '8px' }} />
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
type="submit"
disabled={createMutation.isPending}
style={{ padding: '12px 24px', backgroundColor: '#0173B2', color: '#fff', border: 'none', borderRadius: '4px', flex: 1 }}
>
{createMutation.isPending ? 'Creating...' : 'Create Contract'}
</button>
<button
type="button"
onClick={() => navigate('/')}
style={{ padding: '12px 24px', backgroundColor: '#666', color: '#fff', border: 'none', borderRadius: '4px' }}
>
Cancel
</button>
</div>
</form>
</div>
);
}
// => Navigation
function Navigation() {
const { user, logout } = useAuth();
if (!user) return null;
return (
<nav style={{ padding: '16px', backgroundColor: '#0173B2', color: '#fff', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '20px' }}>
<Link to="/" style={{ color: '#fff', textDecoration: 'none', fontWeight: 'bold' }}>
Murabaha Dashboard
</Link>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<span>{user.name} ({user.role})</span>
<button onClick={logout} style={{ padding: '4px 12px', backgroundColor: '#fff', color: '#0173B2', border: 'none', borderRadius: '4px' }}>
Logout
</button>
</div>
</nav>
);
}
function App() {
return (
<div>
<Navigation />
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<ProtectedRoute><ContractListPage /></ProtectedRoute>} />
<Route path="/contracts/new" element={<ProtectedRoute><CreateContractPage /></ProtectedRoute>} />
<Route path="/contracts/:id" element={<ProtectedRoute><ContractDetailPage /></ProtectedRoute>} />
<Route path="*" element={<div style={{ padding: '20px' }}>404 - Not Found</div>} />
</Routes>
</div>
);
}
function Root() {
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<App />
</AuthProvider>
</QueryClientProvider>
</BrowserRouter>
);
}
export default Root;Key Takeaway: Complete production-ready application combining Context (auth), React Query (data fetching/mutations), React Router (navigation), protected routes, and TypeScript. Demonstrates real-world Islamic finance use case (Murabaha contracts) with proper state management and error handling.
Expected Output: Login page with client/admin roles. Dashboard showing contract list with summary cards (count, principal, profit). Create new contracts with form. View contract details with payment breakdown. Protected routes redirect to login when not authenticated.
Common Pitfalls: Not composing providers in correct order (AuthProvider before usage), missing error boundaries (app crashes), or not invalidating queries after mutations (stale data).
Next Steps
You’ve completed the intermediate section covering 25 production-ready React + TypeScript patterns. These examples provide:
- Advanced Hooks: useReducer, useCallback, useMemo, useRef, useImperativeHandle
- Custom Hooks: Reusable logic extraction with useLocalStorage, useDebounce, useFetch, useForm
- Context API: Global state management with multiple contexts and authentication
- React Query: Server state management with queries, mutations, optimistic updates, infinite scrolling
- React Router: Client-side routing with dynamic routes, protected routes, and navigation
- Error Handling: Error Boundaries for robust error recovery
Continue Learning:
- Advanced Topics: Performance optimization, testing strategies, accessibility patterns
- Production Deployment: Build optimization, environment configuration, monitoring
- Real Projects: Apply these patterns in actual applications
Practice building complete features combining multiple patterns. Master these intermediate concepts before advancing to optimization and architecture patterns.