Part 1: Custom Hooks
Reusable stateful logic extracted into hooks
1. useFocus Hook
Track focus state of an element programmatically instead of using CSS :focus-within.
The Goal
function App() {
const [ref, isFocused] = useFocus()
return (
<div>
<input ref={ref}/>
{isFocused && <p>focused</p>}
</div>
)
}
Implementation
import { useState, useRef, useCallback } from 'react';
function useFocus<T extends HTMLElement = HTMLElement>(): [
(node: T | null) => void,
boolean
] {
const [isFocused, setIsFocused] = useState(false);
const nodeRef = useRef<T | null>(null);
// Store handlers in a ref so they're stable across renders
const handlersRef = useRef({
focus: () => setIsFocused(true),
blur: () => setIsFocused(false),
});
const ref = useCallback((node: T | null) => {
const { focus, blur } = handlersRef.current;
// Cleanup previous node
if (nodeRef.current) {
nodeRef.current.removeEventListener('focus', focus);
nodeRef.current.removeEventListener('blur', blur);
}
nodeRef.current = node;
// Attach to new node
if (node) {
node.addEventListener('focus', focus);
node.addEventListener('blur', blur);
// Sync initial state
setIsFocused(document.activeElement === node);
} else {
setIsFocused(false);
}
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
const { focus, blur } = handlersRef.current;
if (nodeRef.current) {
nodeRef.current.removeEventListener('focus', focus);
nodeRef.current.removeEventListener('blur', blur);
}
};
}, []);
return [ref, isFocused];
}
Why These Choices?
| Choice | Reason |
|---|---|
| Callback ref instead of useRef | A regular useRef doesn't notify us when content changes. Callback ref is called by React when ref is attached/detached. |
| Handlers stored in ref | We need the exact same function references to remove event listeners. If we created handlers inline, removeEventListener wouldn't find them. |
| useCallback with [] deps | Keeps ref function stable. Without it, React would call ref(null) then ref(element) on every render. |
| Sync initial focus state | If element has autoFocus, it's already focused when ref attaches. We check document.activeElement. |
2. useIsMounted Hook
Check if component is still mounted (useful for async operations to avoid "setState on unmounted component" warnings).
Implementation
import { useRef, useCallback, useEffect } from 'react';
function useIsMounted(): () => boolean {
const isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return useCallback(() => isMountedRef.current, []);
}
// Usage
function App() {
const isMounted = useIsMounted();
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then((result) => {
if (isMounted()) { // Check before setting state
setData(result);
}
});
}, []);
return <div>{data}</div>;
}
Why These Choices?
| Choice | Reason |
|---|---|
| useRef not useState | We don't need re-renders. Just need to track a value silently. |
| Return a function, not the value | If we returned the value directly, it would be captured in closures at render time (always true). The function reads the ref at call time, getting the fresh value. |
| useCallback wrapper | Ensures stable reference so it won't cause issues in dependency arrays. |
| Start with false | Component isn't fully mounted until effects run. |
3. useClickOutside Hook
Detect clicks outside an element (for dropdowns, modals, etc.).
Implementation
import { useRef, useEffect, useCallback } from 'react';
function useClickOutside<T extends HTMLElement = HTMLElement>(
callback: () => void
): (node: T | null) => void {
const callbackRef = useRef(callback);
const nodeRef = useRef<T | null>(null);
// Always keep the latest callback
callbackRef.current = callback;
useEffect(() => {
const handleClick = (event: MouseEvent) => {
if (nodeRef.current && !nodeRef.current.contains(event.target as Node)) {
callbackRef.current();
}
};
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, []);
return useCallback((node: T | null) => {
nodeRef.current = node;
}, []);
}
Why These Choices?
| Choice | Reason |
|---|---|
| mousedown instead of click | Fires before click, prevents race conditions where dropdown opens and immediately closes. |
| Callback stored in ref | Avoids stale closures. Callback might change between renders. |
| node.contains(target) | Returns true if target is the element or any descendant. |
4. useDebounce Hook
Debounce a value to limit updates (useful for search inputs).
Implementation
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timeout);
}, [value, delay]); // Include value in deps!
return debouncedValue;
}
// Usage
function SearchBox() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery); // Only called 500ms after user stops typing
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
How It Works
User types: "r" โ "re" โ "rea" โ "reac" โ "react"
โ โ โ โ โ
start clear clear clear start
timer +new +new +new timer
โ
500ms passes
โ
setDebouncedValue("react")
โ
API called once!
Part 2: React Rendering & Re-rendering Quiz
25 questions to test your understanding
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<Child />
</div>
);
}
function Child() {
console.log('Child rendered');
return <div>I am a child</div>;
}
Yes, "Child rendered" will be logged.
When Parent re-renders (due to count state change), React re-renders all children by default โ even if Child receives no props. React doesn't automatically check if a child "needs" to re-render; it just re-renders the entire subtree.
This is why React.memo, useMemo, and useCallback exist โ to opt into skipping unnecessary re-renders.
const Child = React.memo(function Child() {
console.log('Child rendered');
return <div>I am a child</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<Child />
</div>
);
}
No, "Child rendered" will NOT be logged.
React.memo does a shallow comparison of props. When there are no props:
- Previous props:
{} - Next props:
{} - Shallow comparison: equal โ
Since "nothing changed" (empty object equals empty object), React skips re-rendering Child.
const Child = React.memo(function Child({ style }) {
console.log('Child rendered');
return <div style={style}>I am a child</div>;
});
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<Child style={{ color: 'red' }} />
</div>
);
}
Yes, "Child rendered" will be logged.
Every render creates a new object { color: 'red' }. Even though the content is identical, React.memo does a shallow comparison which checks reference equality:
{ color: 'red' } === { color: 'red' } // false (different objects in memory)
Fix: Use useMemo to keep the same reference:
const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />
const Child = React.memo(function Child({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<p>Count: {count}</p>
<Child onClick={handleClick} />
</div>
);
}
No, "Child rendered" will NOT be logged.
useCallback with [] dependencies returns the same function reference across all renders.
namestate changes โ Parent re-rendershandleClickis the same reference (thanks to useCallback)React.memocompares: onClick unchanged โ skip re-render โ
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+3</button>
</div>
);
}
count will be 1 (not 3!)
All three setCount calls capture the same count value (0) from the closure:
setCount(0 + 1); // โ 1
setCount(0 + 1); // โ 1
setCount(0 + 1); // โ 1
React batches these updates, and the final value is 1.
Fix: Use the functional updater:
setCount(c => c + 1); // 0 โ 1
setCount(c => c + 1); // 1 โ 2
setCount(c => c + 1); // 2 โ 3
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect ran');
});
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
4 times
When useEffect has no dependency array, it runs after every render:
- Initial mount โ 1 log
- 3 clicks โ 3 more logs
- Total: 4
| Dependency Array | When Effect Runs |
|---|---|
| None | Every render |
[] | Only on mount |
[a, b] | Mount + when a or b changes |
The full quiz continues with 19 more questions covering: children prop with React.memo, lazy initialization, bailout behavior, key prop gotchas, context re-renders, refs timing, stale closures, useLayoutEffect, automatic batching, and more.
Part 3: React Design Patterns
17 essential patterns for scalable applications
Pattern 1: Compound Components
Components that work together, sharing implicit state via Context. Like native <select> and <option>.
<Select
options={[
{ value: 'apple', label: 'Apple', icon: <AppleIcon /> },
{ type: 'divider' },
{ type: 'group', label: 'Tropical', children: [...] },
]}
/>
<Select>
<Select.Option value="apple">
<AppleIcon /> Apple
</Select.Option>
<Select.Divider />
<Select.Group label="Tropical">
<Select.Option value="banana">Banana</Select.Option>
</Select.Group>
</Select>
Implementation (Tabs Example)
const TabsContext = createContext(null);
function Tabs({ defaultValue, children, onChange }) {
const [selected, setSelected] = useState(defaultValue);
const handleSelect = (value) => {
setSelected(value);
onChange?.(value);
};
return (
<TabsContext.Provider value={{ selected, onSelect: handleSelect }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Tab({ value, children }) {
const { selected, onSelect } = useContext(TabsContext);
const isActive = selected === value;
return (
<button
className={isActive ? 'tab active' : 'tab'}
onClick={() => onSelect(value)}
>
{children}
</button>
);
}
function Panel({ value, children }) {
const { selected } = useContext(TabsContext);
if (selected !== value) return null;
return <div className="tab-panel">{children}</div>;
}
// Attach sub-components
Tabs.Tab = Tab;
Tabs.Panel = Panel;
Radix UI, Headless UI, Chakra UI, Reach UI, MUI
Pattern 2: Render Props
Pass a function as children to let consumers decide what to render with the data.
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
// Call the function with data
return children(position);
}
// Different UIs, same logic:
<MouseTracker>
{({ x, y }) => <p>Position: {x}, {y}</p>}
</MouseTracker>
<MouseTracker>
{({ x, y }) => (
<div style={{ position: 'fixed', left: x + 10, top: y + 10 }}>
Tooltip follows mouse!
</div>
)}
</MouseTracker>
Custom hooks are preferred today. Render props still useful for libraries like Formik.
Pattern 3: Custom Hooks
Extract and reuse stateful logic. Must start with use.
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Composing hooks
function useTheme() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const isDark = theme === 'dark';
const toggle = useCallback(() => {
setTheme(t => t === 'light' ? 'dark' : 'light');
}, []);
return { theme, isDark, toggle };
}
Pattern 4: Provider Pattern
Encapsulate state + logic + actions in a provider with custom hook.
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const login = async (email, password) => {
const user = await loginAPI(email, password);
setUser(user);
};
const logout = async () => {
await logoutAPI();
setUser(null);
};
const value = {
user,
loading,
isAuthenticated: !!user,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Usage
<AuthProvider><App /></AuthProvider>
function Navbar() {
const { user, logout } = useAuth();
return <button onClick={logout}>Logout {user.name}</button>;
}
Pattern 5: Error Boundary
Catch JavaScript errors in child components (must be class component).
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Caught:', error);
// Send to Sentry, etc.
}
render() {
if (this.state.hasError) {
return this.props.fallback || <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
<ErrorBoundary fallback={<p>Failed to load.</p>}>
<MyComponent />
</ErrorBoundary>
| โ Caught | โ Not Caught |
|---|---|
| Render errors | Event handlers |
| Lifecycle methods | Async code (setTimeout, fetch) |
Pattern 6: Polymorphic Components
Change which element is rendered using an as prop.
function Button({ as: Component = 'button', children, ...props }) {
return <Component {...props}>{children}</Component>;
}
<Button onClick={handleClick}>Click me</Button>
// Renders: <button>Click me</button>
<Button as="a" href="/about">About</Button>
// Renders: <a href="/about">About</a>
<Button as={Link} to="/about">About</Button>
// Renders: <Link to="/about">About</Link>
Pattern 7: Higher-Order Components (HOC)
A function that takes a component and returns a new enhanced component.
// HOC that adds loading state
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div className="spinner">Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// Usage
const UserListWithLoading = withLoading(UserList);
<UserListWithLoading
isLoading={loading}
users={users}
/>
Custom hooks are generally preferred today. HOCs still useful with Redux connect().
Pattern 8: Controlled vs Uncontrolled Components
Controlled: React state is the source of truth. Uncontrolled: DOM holds the state.
function ControlledInput() {
const [value, setValue] = useState('');
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
function UncontrolledInput() {
const inputRef = useRef(null);
const handleSubmit = () => {
console.log(inputRef.current.value);
};
return <input ref={inputRef} defaultValue="" />;
}
| Use Controlled When | Use Uncontrolled When |
|---|---|
| Need real-time validation | Simple forms, file inputs |
| Conditional disabling | Integration with non-React code |
| Format input on change | Performance-critical forms |
Pattern 9: Container/Presentational
Separate data/logic (Container) from UI rendering (Presentational).
// Presentational: Only UI, receives props
function UserCard({ name, avatar, onFollow }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<button onClick={onFollow}>Follow</button>
</div>
);
}
// Container: Handles data & logic
function UserCardContainer({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
const handleFollow = () => followUser(userId);
if (!user) return <Loading />;
return (
<UserCard
name={user.name}
avatar={user.avatar}
onFollow={handleFollow}
/>
);
}
Pattern 10: State Reducer Pattern
Let consumers customize state updates by passing their own reducer.
function useToggle({ reducer = (state, action) => action.changes } = {}) {
const [on, setOn] = useState(false);
function dispatch(action) {
const changes = { on: !on };
const newState = reducer({ on }, { ...action, changes });
setOn(newState.on);
}
return { on, toggle: () => dispatch({ type: 'toggle' }) };
}
// Consumer can customize behavior
function App() {
const { on, toggle } = useToggle({
reducer: (state, action) => {
// Prevent turning off after 3 toggles
if (toggleCount >= 3 && action.changes.on === false) {
return state; // Don't change
}
return action.changes;
}
});
}
Downshift library uses this pattern extensively for customizable autocomplete.
Pattern 11: Props Getter Pattern
Return functions that bundle complex props together for ease of use.
function useDropdown() {
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
// Bundle all trigger props together
const getTriggerProps = (props = {}) => ({
'aria-expanded': isOpen,
'aria-haspopup': 'listbox',
onClick: () => setIsOpen(!isOpen),
...props, // Allow overrides
});
// Bundle all menu props together
const getMenuProps = (props = {}) => ({
role: 'listbox',
'aria-activedescendant': selectedIndex,
hidden: !isOpen,
...props,
});
return { isOpen, getTriggerProps, getMenuProps };
}
// Usage - clean and accessible
function Dropdown() {
const { getTriggerProps, getMenuProps } = useDropdown();
return (
<>
<button {...getTriggerProps()}>Menu</button>
<ul {...getMenuProps()}>...</ul>
</>
);
}
Pattern 12: Slot Pattern
Named content areas that parent can fill (like Vue slots).
function Card({ header, children, footer }) {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Usage
<Card
header={<h2>Title</h2>}
footer={<button>Save</button>}
>
<p>Card content goes here</p>
</Card>
Next.js App Router uses slots for parallel routes and layouts.
Pattern 13: Forwarding Refs
Pass refs through wrapper components to the underlying DOM element.
// Without forwardRef - ref would attach to FancyButton, not button
const FancyButton = forwardRef(function FancyButton(props, ref) {
return (
<button ref={ref} className="fancy-button">
{props.children}
</button>
);
});
// Usage - ref now points to the actual button element
function Form() {
const buttonRef = useRef(null);
useEffect(() => {
buttonRef.current.focus(); // Works!
}, []);
return <FancyButton ref={buttonRef}>Click me</FancyButton>;
}
Pattern 14: Portal Pattern
Render children into a DOM node outside the parent hierarchy (modals, tooltips).
function Modal({ isOpen, onClose, children }) {
if (!isOpen) return null;
// Render into document.body instead of parent
return createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>ร</button>
{children}
</div>
</div>,
document.body
);
}
// Usage - renders at body level, not inside Button
function App() {
const [showModal, setShowModal] = useState(false);
return (
<div className="app">
<button onClick={() => setShowModal(true)}>Open</button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)}>
<h2>Modal Content</h2>
</Modal>
</div>
);
}
Pattern 15: Optimistic Updates
Update UI immediately, then sync with server. Rollback on failure.
function TodoList() {
const [todos, setTodos] = useState([]);
const toggleTodo = async (id) => {
// 1. Save previous state for rollback
const previousTodos = todos;
// 2. Optimistically update UI immediately
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
try {
// 3. Sync with server
await api.toggleTodo(id);
} catch (error) {
// 4. Rollback on failure
setTodos(previousTodos);
toast.error('Failed to update todo');
}
};
}
Provides built-in onMutate, onError, onSettled for optimistic updates.
Pattern 16: Suspense for Data Fetching
Declarative loading states with Suspense boundaries.
// Wrapper that throws promise while loading
function fetchData(url) {
let status = 'pending';
let result;
const promise = fetch(url)
.then(r => r.json())
.then(data => { status = 'success'; result = data; })
.catch(err => { status = 'error'; result = err; });
return {
read() {
if (status === 'pending') throw promise;
if (status === 'error') throw result;
return result;
}
};
}
const userResource = fetchData('/api/user');
function UserProfile() {
const user = userResource.read(); // Suspends if pending
return <h1>{user.name}</h1>;
}
// Usage with Suspense boundary
<Suspense fallback={<Skeleton />}>
<UserProfile />
</Suspense>
Pattern 17: Lazy Loading / Code Splitting
Load components on demand to reduce initial bundle size.
import { lazy, Suspense } from 'react';
// Lazy load heavy components
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
const Analytics = lazy(() =>
import('./Analytics').then(module => ({ default: module.Analytics }))
);
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
// Preload on hover for better UX
<Link
to="/dashboard"
onMouseEnter={() => import('./Dashboard')}
>
Dashboard
</Link>
๐ All 17 Patterns Summary
| # | Pattern | Use Case | Libraries |
|---|---|---|---|
| 1 | Compound Components | Flexible, related UI | Radix, Headless UI |
| 2 | Render Props | Share logic with custom rendering | Formik, Downshift |
| 3 | HOC | Enhance components | Redux (connect) |
| 4 | Controlled/Uncontrolled | Form handling | React Hook Form |
| 5 | Provider | Global state | Redux, React Query |
| 6 | Custom Hooks | Reusable logic | SWR, React Query |
| 7 | Container/Presentational | Separate logic from UI | Storybook libs |
| 8 | State Reducer | Customizable behavior | Downshift |
| 9 | Props Getter | Bundle complex props | React Table |
| 10 | Slot | Content injection | Next.js layouts |
| 11 | Error Boundary | Graceful errors | react-error-boundary |
| 12 | Polymorphic | Flexible element type | Chakra UI |
| 13 | Forwarding Refs | Pass refs through | All UI libs |
| 14 | Portal | Render outside parent | Radix, Headless UI |
| 15 | Optimistic Updates | Instant feedback | React Query |
| 16 | Suspense | Declarative loading | React Query |
| 17 | Lazy Loading | Code splitting | React.lazy |