Advanced React Patterns
Key Concepts
- Compound Components
- Higher-Order Components (HOC)
- Render Props
- Context API
- Custom Hooks
- Controlled Components
- Uncontrolled Components
- Provider Pattern
- State Reducer Pattern
- State Initializer Pattern
- State Normalization
- State Persistence
Compound Components
Compound Components are a pattern where multiple components work together to form a single, cohesive unit. This pattern allows for more flexible and reusable components.
Example:
function Accordion({ children }) { return <div>{children}</div>; } function AccordionItem({ title, children }) { return ( <div> <h3>{title}</h3> <div>{children}</div> </div> ); } function App() { return ( <Accordion> <AccordionItem title="Item 1">Content 1</AccordionItem> <AccordionItem title="Item 2">Content 2</AccordionItem> </Accordion> ); }
Higher-Order Components (HOC)
Higher-Order Components are functions that take a component and return a new component with additional props or behavior. This pattern is useful for sharing logic across multiple components.
Example:
function withLoading(Component) { return function WithLoadingComponent({ isLoading, ...props }) { if (!isLoading) return <Component {...props} />; return <p>Loading...</p>; } } const EnhancedComponent = withLoading(MyComponent);
Render Props
Render Props is a pattern where a component's prop is a function that returns a React element. This allows for dynamic rendering and sharing of logic between components.
Example:
function MouseTracker({ render }) { const [position, setPosition] = useState({ x: 0, y: 0 }); const handleMouseMove = (event) => { setPosition({ x: event.clientX, y: event.clientY }); }; return ( <div style={{ height: '100vh' }} onMouseMove={handleMouseMove}> {render(position)} </div> ); } function App() { return ( <MouseTracker render={(position) => ( <h1>The mouse position is ({position.x}, {position.y})</h1> )}></MouseTracker> ); }
Context API
The Context API allows you to pass data through the component tree without having to pass props down manually at every level. This is useful for global state management.
Example:
const ThemeContext = React.createContext('light'); function App() { return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar() { return ( <div> <ThemedButton /> </div> ); } function ThemedButton() { const theme = useContext(ThemeContext); return <Button theme={theme} />; }
Custom Hooks
Custom Hooks allow you to extract component logic into reusable functions. This pattern promotes code reuse and separation of concerns.
Example:
function useWindowSize() { const [size, setSize] = useState([0, 0]); useEffect(() => { const handleResize = () => { setSize([window.innerWidth, window.innerHeight]); }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, []); return size; } function App() { const [width, height] = useWindowSize(); return ( <div> <p>Width: {width}</p> <p>Height: {height}</p> </div> ); }
Controlled Components
Controlled Components are components where form data is handled by the React component's state. This pattern ensures that the component has full control over the form elements.
Example:
function ControlledInput() { const [value, setValue] = useState(''); const handleChange = (event) => { setValue(event.target.value); }; return ( <input type="text" value={value} onChange={handleChange} /> ); }
Uncontrolled Components
Uncontrolled Components are components where form data is handled by the DOM itself. This pattern is useful for simpler forms where you don't need to control the input values.
Example:
function UncontrolledInput() { const inputRef = useRef(null); const handleSubmit = (event) => { alert('A name was submitted: ' + inputRef.current.value); event.preventDefault(); }; return ( <form onSubmit={handleSubmit}> <label> Name: <input type="text" ref={inputRef} /> </label> <input type="submit" value="Submit" /> </form> ); }
Provider Pattern
The Provider Pattern involves wrapping a component tree with a Provider component that supplies context values to its descendants. This pattern is useful for managing global state.
Example:
const ThemeContext = React.createContext('light'); function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } function App() { return ( <ThemeProvider> <Toolbar /> </ThemeProvider> ); }
State Reducer Pattern
The State Reducer Pattern allows you to externalize the state management logic by passing a reducer function to a component. This pattern is useful for customizing state updates.
Example:
function Counter({ initialCount = 0, reducer = (state) => state }) { const [state, dispatch] = useReducer(reducer, { count: initialCount }); return ( <div> Count: {state.count} <button onClick={() => dispatch({ type: 'increment' })}>+</button> <button onClick={() => dispatch({ type: 'decrement' })}>-</button> </div> ); } function customReducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; } } function App() { return <Counter reducer={customReducer} />; }
State Initializer Pattern
The State Initializer Pattern allows you to pass an initial state to a component, which can be useful for resetting the state to its initial value.
Example:
function Counter({ initialCount = 0 }) { const [count, setCount] = useState(initialCount); return ( <div> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </div> ); }
State Normalization
State Normalization involves organizing state in a way that avoids duplication and makes it easier to update. This pattern is useful for managing complex state structures.
Example:
const initialState = { byId: { 1: { id: 1, name: 'Item 1' }, 2: { id: 2, name: 'Item 2' }, }, allIds: [1, 2], }; function reducer(state, action) { switch (action.type) { case 'ADD_ITEM': return { ...state, byId: { ...state.byId, [action.payload.id]: action.payload, }, allIds: [...state.allIds, action.payload.id], }; default: return state; } }
State Persistence
State Persistence involves saving the state to a persistent storage like localStorage or sessionStorage. This pattern is useful for maintaining state across page reloads.
Example:
function usePersistentState(key, initialState) { const [state, setState] = useState(() => { const storedState = localStorage.getItem(key); return storedState ? JSON.parse(storedState) : initialState; }); useEffect(() => { localStorage.setItem(key, JSON.stringify(state)); }, [key, state]); return [state, setState]; } function App() { const [count, setCount] = usePersistentState('count', 0); return ( <div> Count: {count} <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> </div> ); }
Analogies
Think of Compound Components as a family of Lego blocks that fit together to build a complex structure. Higher-Order Components are like adding accessories to a toy, enhancing its functionality. Render Props are like a recipe where you can substitute ingredients to get different results. Context API is like a central air conditioning system that cools the entire house. Custom Hooks are like reusable tools in a toolbox. Controlled Components are like a remote-controlled toy, fully managed by the controller. Uncontrolled Components are like a toy that operates on its own. The Provider Pattern is like a power outlet that supplies electricity to all connected devices. The State Reducer Pattern is like a programmable thermostat that adjusts the temperature based on your preferences. The State Initializer Pattern is like a reset button on a game console. State Normalization is like organizing your closet by categories. State Persistence is like saving your game progress so you can resume later.