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:
jsx1// Instead of inheritance:2class SpecialButton extends Button { ... }34// Use composition:5function SpecialButton(props) {6 return (7 <Button8 {...props}9 className={`special ${props.className || ''}`}10 icon={<StarIcon />}11 />12 );13}1415// 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}2526<Button leftIcon={<StarIcon />}>Special Button</Button>
Inversion of Control
Give control to users:
jsx1// Before: Hardcoded behavior2function List({ items }) {3 return (4 <ul>5 {items.map((item) => (6 <li key={item.id}>{item.name}</li>7 ))}8 </ul>9 );10}1112// After: User controls rendering13function 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}2223// Usage - Full control over item rendering24<List25 items={users}26 renderItem={(user) => <UserCard user={user} showAvatar />}27/>;
Hook Composition
Build complex hooks from simple ones:
jsx1// Base hooks2function useBoolean(initial = false) {3 const [value, setValue] = useState(initial);45 const setTrue = useCallback(() => setValue(true), []);6 const setFalse = useCallback(() => setValue(false), []);7 const toggle = useCallback(() => setValue((v) => !v), []);89 return { value, setValue, setTrue, setFalse, toggle };10}1112function useDisclosure(initial = false) {13 const {14 value: isOpen,15 setTrue: open,16 setFalse: close,17 toggle,18 } = useBoolean(initial);1920 return { isOpen, open, close, toggle };21}2223// Composed hook24function useModal(initial = false) {25 const disclosure = useDisclosure(initial);2627 // Add modal-specific behavior28 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]);3839 // Close on escape40 useEffect(() => {41 const handleEscape = (e) => {42 if (e.key === "Escape") disclosure.close();43 };4445 if (disclosure.isOpen) {46 document.addEventListener("keydown", handleEscape);47 }4849 return () => document.removeEventListener("keydown", handleEscape);50 }, [disclosure.isOpen, disclosure.close]);5152 return disclosure;53}
Provider Pattern
Combine multiple contexts with a single provider:
jsx1// Individual providers become unwieldy2function 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}1718// Approach: Compose providers19function composeProviders(...providers) {20 return ({ children }) =>21 providers.reduceRight(22 (acc, Provider) => <Provider>{acc}</Provider>,23 children24 );25}2627const AppProviders = composeProviders(28 ThemeProvider,29 AuthProvider,30 CartProvider,31 NotificationProvider,32 RouterProvider33);3435function App() {36 return (37 <AppProviders>38 <Content />39 </AppProviders>40 );41}
Container/Presenter Pattern
Separate data logic from presentation:
jsx1// Container: Handles data2function UserListContainer() {3 const {4 data: users,5 isLoading,6 error,7 } = useQuery({8 queryKey: ["users"],9 queryFn: fetchUsers,10 });1112 if (isLoading) return <UserListSkeleton />;13 if (error) return <ErrorMessage error={error} />;1415 return <UserList users={users} />;16}1718// Presenter: Pure presentation19function 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}2829// Benefits:30// - UserList is easy to test (just pass props)31// - UserList works in Storybook without mocking32// - Clear separation of concerns
Headless Components
Provide behavior without UI:
jsx1function useSelect({ items, onChange }) {2 const [isOpen, setIsOpen] = useState(false);3 const [selectedIndex, setSelectedIndex] = useState(-1);4 const [highlightedIndex, setHighlightedIndex] = useState(0);56 const select = (index) => {7 setSelectedIndex(index);8 onChange?.(items[index]);9 setIsOpen(false);10 };1112 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 };3132 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}5556// User provides their own UI57function CustomSelect({ items, onChange }) {58 const {59 isOpen,60 selectedItem,61 highlightedIndex,62 getToggleProps,63 getMenuProps,64 getItemProps,65 } = useSelect({ items, onChange });6667 return (68 <div className="my-custom-select">69 <button {...getToggleProps()} className="my-trigger">70 {selectedItem || "Select..."}71 </button>7273 {isOpen && (74 <ul {...getMenuProps()} className="my-menu">75 {items.map((item, index) => (76 <li77 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:
jsx1function DataTable({ data, columns, renderRow, renderCell, renderEmpty }) {2 if (data.length === 0) {3 return renderEmpty?.() || <p>No data</p>;4 }56 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 {renderCell24 ? renderCell(row[col.key], col, row)25 : row[col.key]}26 </td>27 ))}28 </tr>29 )30 )}31 </tbody>32 </table>33 );34}3536// Usage37<DataTable38 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} /> : value46 }47 renderEmpty={() => <EmptyState message="No users found" />}48/>;
Error Handling Patterns
Graceful error handling:
jsx1// Error Boundary with recovery2class ErrorBoundary extends Component {3 state = { hasError: false, error: null };45 static getDerivedStateFromError(error) {6 return { hasError: true, error };7 }89 componentDidCatch(error, info) {10 logErrorToService(error, info);11 }1213 retry = () => {14 this.setState({ hasError: false, error: null });15 };1617 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}2728// Usage29<ErrorBoundary30 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:
jsx1function useMutation({ mutationFn, onSuccess, onError, onSettled }) {2 const [state, setState] = useState({3 status: "idle",4 data: null,5 error: null,6 });78 const mutate = async (variables, options = {}) => {9 setState({ status: "pending", data: null, error: null });1011 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 };2728 return { mutate, ...state };29}3031// With optimistic update32function TodoItem({ todo, onToggle }) {33 const [optimisticDone, setOptimisticDone] = useState(todo.done);3435 const handleToggle = async () => {36 const newDone = !optimisticDone;37 setOptimisticDone(newDone); // Optimistic update3839 try {40 await onToggle(todo.id, newDone);41 } catch (error) {42 setOptimisticDone(!newDone); // Revert on error43 }44 };4546 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:
jsx1// components/Menu/index.js2export { Menu as Root } from "./Menu";3export { MenuItem as Item } from "./MenuItem";4export { MenuDivider as Divider } from "./MenuDivider";5export { MenuGroup as Group } from "./MenuGroup";67// Usage8import * as Menu from "./components/Menu";910<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:
- Composition over inheritance - Build with small, composable pieces
- Inversion of control - Let users customize behavior
- Hook composition - Build complex hooks from simple ones
- Provider composition - Combine multiple providers cleanly
- Container/Presenter - Separate data from UI
- Headless components - Behavior without styling
- Render delegation - Let parents control rendering
- Optimistic UI - Show success before confirmation
- Module exports - Organize related components
Next, let's build a component library that uses these patterns!