20 minlesson

Advanced Patterns Deep Dive

Advanced Patterns Deep Dive

Let's explore more patterns and techniques for building flexible, maintainable React components.

Composition Over Inheritance

React favors composition over inheritance:

jsx
1// Instead of inheritance:
2class SpecialButton extends Button { ... }
3
4// Use composition:
5function SpecialButton(props) {
6 return (
7 <Button
8 {...props}
9 className={`special ${props.className || ''}`}
10 icon={<StarIcon />}
11 />
12 );
13}
14
15// Or use slots/children:
16function Button({ leftIcon, rightIcon, children, ...props }) {
17 return (
18 <button {...props}>
19 {leftIcon && <span className="left-icon">{leftIcon}</span>}
20 {children}
21 {rightIcon && <span className="right-icon">{rightIcon}</span>}
22 </button>
23 );
24}
25
26<Button leftIcon={<StarIcon />}>Special Button</Button>

Inversion of Control

Give control to users:

jsx
1// Before: Hardcoded behavior
2function List({ items }) {
3 return (
4 <ul>
5 {items.map((item) => (
6 <li key={item.id}>{item.name}</li>
7 ))}
8 </ul>
9 );
10}
11
12// After: User controls rendering
13function List({ items, renderItem }) {
14 return (
15 <ul>
16 {items.map((item, index) => (
17 <li key={item.id}>{renderItem(item, index)}</li>
18 ))}
19 </ul>
20 );
21}
22
23// Usage - Full control over item rendering
24<List
25 items={users}
26 renderItem={(user) => <UserCard user={user} showAvatar />}
27/>;

Hook Composition

Build complex hooks from simple ones:

jsx
1// Base hooks
2function useBoolean(initial = false) {
3 const [value, setValue] = useState(initial);
4
5 const setTrue = useCallback(() => setValue(true), []);
6 const setFalse = useCallback(() => setValue(false), []);
7 const toggle = useCallback(() => setValue((v) => !v), []);
8
9 return { value, setValue, setTrue, setFalse, toggle };
10}
11
12function useDisclosure(initial = false) {
13 const {
14 value: isOpen,
15 setTrue: open,
16 setFalse: close,
17 toggle,
18 } = useBoolean(initial);
19
20 return { isOpen, open, close, toggle };
21}
22
23// Composed hook
24function useModal(initial = false) {
25 const disclosure = useDisclosure(initial);
26
27 // Add modal-specific behavior
28 useEffect(() => {
29 if (disclosure.isOpen) {
30 document.body.style.overflow = "hidden";
31 } else {
32 document.body.style.overflow = "";
33 }
34 return () => {
35 document.body.style.overflow = "";
36 };
37 }, [disclosure.isOpen]);
38
39 // Close on escape
40 useEffect(() => {
41 const handleEscape = (e) => {
42 if (e.key === "Escape") disclosure.close();
43 };
44
45 if (disclosure.isOpen) {
46 document.addEventListener("keydown", handleEscape);
47 }
48
49 return () => document.removeEventListener("keydown", handleEscape);
50 }, [disclosure.isOpen, disclosure.close]);
51
52 return disclosure;
53}

Provider Pattern

Combine multiple contexts with a single provider:

jsx
1// Individual providers become unwieldy
2function App() {
3 return (
4 <ThemeProvider>
5 <AuthProvider>
6 <CartProvider>
7 <NotificationProvider>
8 <RouterProvider>
9 <Content />
10 </RouterProvider>
11 </NotificationProvider>
12 </CartProvider>
13 </AuthProvider>
14 </ThemeProvider>
15 );
16}
17
18// Approach: Compose providers
19function composeProviders(...providers) {
20 return ({ children }) =>
21 providers.reduceRight(
22 (acc, Provider) => <Provider>{acc}</Provider>,
23 children
24 );
25}
26
27const AppProviders = composeProviders(
28 ThemeProvider,
29 AuthProvider,
30 CartProvider,
31 NotificationProvider,
32 RouterProvider
33);
34
35function App() {
36 return (
37 <AppProviders>
38 <Content />
39 </AppProviders>
40 );
41}

Container/Presenter Pattern

Separate data logic from presentation:

jsx
1// Container: Handles data
2function UserListContainer() {
3 const {
4 data: users,
5 isLoading,
6 error,
7 } = useQuery({
8 queryKey: ["users"],
9 queryFn: fetchUsers,
10 });
11
12 if (isLoading) return <UserListSkeleton />;
13 if (error) return <ErrorMessage error={error} />;
14
15 return <UserList users={users} />;
16}
17
18// Presenter: Pure presentation
19function UserList({ users }) {
20 return (
21 <ul className="user-list">
22 {users.map((user) => (
23 <UserListItem key={user.id} user={user} />
24 ))}
25 </ul>
26 );
27}
28
29// Benefits:
30// - UserList is easy to test (just pass props)
31// - UserList works in Storybook without mocking
32// - Clear separation of concerns

Headless Components

Provide behavior without UI:

jsx
1function useSelect({ items, onChange }) {
2 const [isOpen, setIsOpen] = useState(false);
3 const [selectedIndex, setSelectedIndex] = useState(-1);
4 const [highlightedIndex, setHighlightedIndex] = useState(0);
5
6 const select = (index) => {
7 setSelectedIndex(index);
8 onChange?.(items[index]);
9 setIsOpen(false);
10 };
11
12 const handleKeyDown = (e) => {
13 switch (e.key) {
14 case "ArrowDown":
15 e.preventDefault();
16 setHighlightedIndex((i) => Math.min(i + 1, items.length - 1));
17 break;
18 case "ArrowUp":
19 e.preventDefault();
20 setHighlightedIndex((i) => Math.max(i - 1, 0));
21 break;
22 case "Enter":
23 e.preventDefault();
24 select(highlightedIndex);
25 break;
26 case "Escape":
27 setIsOpen(false);
28 break;
29 }
30 };
31
32 return {
33 isOpen,
34 selectedItem: items[selectedIndex],
35 highlightedIndex,
36 getToggleProps: () => ({
37 onClick: () => setIsOpen(!isOpen),
38 onKeyDown: handleKeyDown,
39 "aria-expanded": isOpen,
40 "aria-haspopup": "listbox",
41 }),
42 getMenuProps: () => ({
43 role: "listbox",
44 "aria-activedescendant": `option-${highlightedIndex}`,
45 }),
46 getItemProps: (index) => ({
47 id: `option-${index}`,
48 role: "option",
49 "aria-selected": index === selectedIndex,
50 onClick: () => select(index),
51 onMouseEnter: () => setHighlightedIndex(index),
52 }),
53 };
54}
55
56// User provides their own UI
57function CustomSelect({ items, onChange }) {
58 const {
59 isOpen,
60 selectedItem,
61 highlightedIndex,
62 getToggleProps,
63 getMenuProps,
64 getItemProps,
65 } = useSelect({ items, onChange });
66
67 return (
68 <div className="my-custom-select">
69 <button {...getToggleProps()} className="my-trigger">
70 {selectedItem || "Select..."}
71 </button>
72
73 {isOpen && (
74 <ul {...getMenuProps()} className="my-menu">
75 {items.map((item, index) => (
76 <li
77 key={item}
78 {...getItemProps(index)}
79 className={index === highlightedIndex ? "highlighted" : ""}
80 >
81 {item}
82 </li>
83 ))}
84 </ul>
85 )}
86 </div>
87 );
88}

Render Delegation

Let parent control child rendering:

jsx
1function DataTable({ data, columns, renderRow, renderCell, renderEmpty }) {
2 if (data.length === 0) {
3 return renderEmpty?.() || <p>No data</p>;
4 }
5
6 return (
7 <table>
8 <thead>
9 <tr>
10 {columns.map((col) => (
11 <th key={col.key}>{col.header}</th>
12 ))}
13 </tr>
14 </thead>
15 <tbody>
16 {data.map((row, rowIndex) =>
17 renderRow ? (
18 renderRow(row, rowIndex)
19 ) : (
20 <tr key={rowIndex}>
21 {columns.map((col) => (
22 <td key={col.key}>
23 {renderCell
24 ? renderCell(row[col.key], col, row)
25 : row[col.key]}
26 </td>
27 ))}
28 </tr>
29 )
30 )}
31 </tbody>
32 </table>
33 );
34}
35
36// Usage
37<DataTable
38 data={users}
39 columns={[
40 { key: "name", header: "Name" },
41 { key: "email", header: "Email" },
42 { key: "role", header: "Role" },
43 ]}
44 renderCell={(value, column, row) =>
45 column.key === "role" ? <RoleBadge role={value} /> : value
46 }
47 renderEmpty={() => <EmptyState message="No users found" />}
48/>;

Error Handling Patterns

Graceful error handling:

jsx
1// Error Boundary with recovery
2class ErrorBoundary extends Component {
3 state = { hasError: false, error: null };
4
5 static getDerivedStateFromError(error) {
6 return { hasError: true, error };
7 }
8
9 componentDidCatch(error, info) {
10 logErrorToService(error, info);
11 }
12
13 retry = () => {
14 this.setState({ hasError: false, error: null });
15 };
16
17 render() {
18 if (this.state.hasError) {
19 return this.props.fallback({
20 error: this.state.error,
21 retry: this.retry,
22 });
23 }
24 return this.props.children;
25 }
26}
27
28// Usage
29<ErrorBoundary
30 fallback={({ error, retry }) => (
31 <div>
32 <p>Error: {error.message}</p>
33 <button onClick={retry}>Try Again</button>
34 </div>
35 )}
36>
37 <RiskyComponent />
38</ErrorBoundary>;

Optimistic UI Pattern

Show success before server confirms:

jsx
1function useMutation({ mutationFn, onSuccess, onError, onSettled }) {
2 const [state, setState] = useState({
3 status: "idle",
4 data: null,
5 error: null,
6 });
7
8 const mutate = async (variables, options = {}) => {
9 setState({ status: "pending", data: null, error: null });
10
11 try {
12 const data = await mutationFn(variables);
13 setState({ status: "success", data, error: null });
14 onSuccess?.(data, variables);
15 options.onSuccess?.(data, variables);
16 return data;
17 } catch (error) {
18 setState({ status: "error", data: null, error });
19 onError?.(error, variables);
20 options.onError?.(error, variables);
21 throw error;
22 } finally {
23 onSettled?.();
24 options.onSettled?.();
25 }
26 };
27
28 return { mutate, ...state };
29}
30
31// With optimistic update
32function TodoItem({ todo, onToggle }) {
33 const [optimisticDone, setOptimisticDone] = useState(todo.done);
34
35 const handleToggle = async () => {
36 const newDone = !optimisticDone;
37 setOptimisticDone(newDone); // Optimistic update
38
39 try {
40 await onToggle(todo.id, newDone);
41 } catch (error) {
42 setOptimisticDone(!newDone); // Revert on error
43 }
44 };
45
46 return (
47 <li>
48 <input type="checkbox" checked={optimisticDone} onChange={handleToggle} />
49 {todo.title}
50 </li>
51 );
52}

Module Pattern for Exports

Organize related components:

jsx
1// components/Menu/index.js
2export { Menu as Root } from "./Menu";
3export { MenuItem as Item } from "./MenuItem";
4export { MenuDivider as Divider } from "./MenuDivider";
5export { MenuGroup as Group } from "./MenuGroup";
6
7// Usage
8import * as Menu from "./components/Menu";
9
10<Menu.Root>
11 <Menu.Group label="Actions">
12 <Menu.Item>Edit</Menu.Item>
13 <Menu.Item>Duplicate</Menu.Item>
14 </Menu.Group>
15 <Menu.Divider />
16 <Menu.Item variant="danger">Delete</Menu.Item>
17</Menu.Root>;

Summary

Key advanced patterns:

  1. Composition over inheritance - Build with small, composable pieces
  2. Inversion of control - Let users customize behavior
  3. Hook composition - Build complex hooks from simple ones
  4. Provider composition - Combine multiple providers cleanly
  5. Container/Presenter - Separate data from UI
  6. Headless components - Behavior without styling
  7. Render delegation - Let parents control rendering
  8. Optimistic UI - Show success before confirmation
  9. Module exports - Organize related components

Next, let's build a component library that uses these patterns!