This is a collection of the most important design patterns to use in React. Made by Cosden Solutions.
1. Single Responsibility Principle
Your components should have only one responsibility. They should only do "one thing" and delegate everything else to other components. Here's an example of a component that has too many responsibilities:
1// ❌ Too many responsibilities! 2function BigComponent() { 3 // Responsible for multiple unrelated states 4 const [data, setData] = useState(); 5 const [isModalOpen, setIsModalOpen] = useState(false); 6 7 // Responsible for fetching data 8 useEffect(() => { 9 fetch('/api/data') 10 .then(response => response.json()) 11 .then(data => setData(data)); 12 }, []); 13 14 // Responsible for implementing sending analytics events 15 useEffect(() => { 16 sendAnalyticsEvent('page_view', { page: 'big_component' }); 17 }, []); 18 19 // Responsible for toggling modal 20 function toggleModal() { 21 setIsModalOpen(prev => !prev); 22 } 23 24 // ... other code 25} 26
Instead, create multiple components/hooks each with a single responsibility.
First, create useFetchData.ts. This hook will hold the data state and manage fetching and updating it.
1// ✅ Single responsibility: managing data 2export function useFetchData() { 3 const [data, setData] = useState(); 4 5 useEffect(() => { 6 fetch('/api/data') 7 .then(response => response.json()) 8 .then(data => setData(data)); 9 }, []); 10 11 return data; 12} 13
Or even better, use react-query.
1// ✅ Single responsibility: managing data through react query 2export function useFetchData() { 3 return useQuery({ 4 queryKey: ['data'], 5 queryFn: () => fetch('/api/data'), 6 }); 7} 8
Then create usePageAnalytics.ts. This hook will receive an event through props and send it.
1type Event = { 2 page: string; 3}; 4 5// ✅ Single responsibility: managing analytics 6export function usePageAnalytics(event: Event) { 7 useEffect(() => { 8 sendAnalyticsEvent('page_view', event); 9 }, []); 10} 11
Finally create Modal.tsx. This component will receive children as props, and manage its own isModalOpen state.
1type ModalProps = { 2 children: React.ReactNode; 3}; 4 5// ✅ Single responsibility: managing modals 6export function Modal({ children }: ModalProps) { 7 const [isModalOpen, setIsModalOpen] = useState(false); 8 9 function toggleModal() { 10 setIsModalOpen(prev => !prev); 11 } 12 13 return ( 14 <> 15 <button onPress={toggleModal}>Open</button> 16 {isModalOpen && children} 17 </> 18 ); 19} 20
With this, BigComponent just needs to import and put everything together. It is now small, easy to manage, and highly scalable.
1import { useFetchData } from './useFetchData'; 2import { useAnalytics } from './useAnalytics'; 3import { Modal } from './Modal'; 4 5// ✅ Single responsibility: put everything together 6function BigComponent() { 7 const data = useFetchData(); 8 9 useAnalytics(); 10 11 return <Modal>{/* ... other code */}</Modal>; 12} 13
2. Container and Presentation Components
To keep code organized, you can split your components into a container and a presentation component. The container component holds all the logic, and the presentation component renders the UI.
1// Container component responsible for logic 2function ContainerComponent() { 3 const [items, setItems] = useState([]); 4 const [filters, setFilters] = useState({}); 5 6 useEffect(() => { 7 const filteredItems = filterItems(items, filters); 8 }, [filters]); 9 10 function handleFilters(newFilters) { 11 setFilters(newFilters); 12 } 13 14 // ... other business logic code 15 16 return <PresentationComponent items={items} />; 17} 18 19// Presentation component responsible for UI 20function PresentationComponent({ items }) { 21 return ( 22 <> 23 {/* ... other UI code */} 24 {items.map(item => ( 25 <ItemCard key={item.id} item={item} /> 26 ))} 27 {/* ... other UI code */} 28 </> 29 ); 30} 31
3. Compound Components Pattern
Group components meant to be used together into a compound component with the React Context API.
1import { createContext, useState } from 'react'; 2 3const ToggleContext = createContext(); 4 5// Main component exported for use in project 6export default function Toggle({ children }) { 7 const [on, setOn] = useState(false); 8 9 function toggle() { 10 setOn(!on); 11 } 12 13 return ( 14 <ToggleContext.Provider value={{ on, toggle }}> 15 {children} 16 </ToggleContext.Provider> 17 ); 18} 19 20// Compound component attached to main component 21Toggle.On = function ToggleOn({ children }) { 22 const { on } = useContext(ToggleContext); 23 return on ? children : null; 24}; 25 26// Compound component attached to main component 27Toggle.Off = function ToggleOff({ children }) { 28 const { on } = useContext(ToggleContext); 29 return on ? null : children; 30}; 31 32// Compound component attached to main component 33Toggle.Button = function ToggleButton(props) { 34 const { on, toggle } = useContext(ToggleContext); 35 return <button onClick={toggle} {...props} />; 36}; 37
This component can now be used anywhere with great flexibility. Place sub-components in any order, or only use a subset of them:
1import Toggle from '@/components/Toggle'; 2 3// Example use case with all components 4function App() { 5 return ( 6 <Toggle> 7 <Toggle.On>The button is on</Toggle.On> 8 <Toggle.Off>The button is off</Toggle.Off> 9 <Toggle.Button>Toggle</Toggle.Button> 10 </Toggle> 11 ); 12} 13 14// Example use case with different order 15function App() { 16 return ( 17 <Toggle> 18 <Toggle.Button>Toggle</Toggle.Button> 19 <Toggle.Off>The button is off</Toggle.Off> 20 <Toggle.On>The button is on</Toggle.On> 21 </Toggle> 22 ); 23} 24 25// Example use case with partial components 26function App() { 27 return ( 28 <Toggle> 29 <Toggle.Button>Toggle</Toggle.Button> 30 </Toggle> 31 ); 32} 33
4. Nested Prop Forwarding
When a flexible component uses another, allow props to be forwarded to the nested component.
1// Receives props as `...rest` 2function Text({ children, ...rest }) { 3 return ( 4 <span className="text-primary" {...rest}> 5 {children} 6 </span> 7 ); 8} 9 10// Button component uses `Text` component for its text 11function Button({ children, textProps, ...rest }) { 12 return ( 13 <button {...rest}> 14 {/* ✅ `textProps` are forwarded */} 15 <Text {...textProps}>{children}</Text> 16 </button> 17 ); 18} 19
Example usage:
1function App() { 2 return ( 3 <Button textProps={{ className: 'text-red-500' }}> 4 Button with red text 5 </Button> 6 ); 7} 8
5. Children Components Pattern
To improve performance and prevent unnecessary re-renders, lift up components and pass them as children instead.
1function Component() { 2 const [count, setCount] = useState(0); 3 4 return ( 5 <div> 6 {count} 7 {/* ❌ Expensive component will re-render unnecessarily everytime count changes */} 8 <ExpensiveComponent /> 9 </div> 10 ); 11} 12
Moving ExpensiveComponent up and passing it as children will prevent it re-rendering
1// Component 2function Component({ children }) { 3 const [count, setCount] = useState(0); 4 5 // ✅ Children don't re-render when state changes 6 return <Component>{children}</Component>; 7} 8 9// App 10function App() { 11 return ( 12 <Component> 13 {/* ✅ Expensive component will not re-render when Component does */} 14 <ExpensiveComponent /> 15 </Component> 16 ); 17} 18
6. Custom Hooks
To keep code clean and re-usable, extract related functionality into a custom hook that can be shared.
1// ❌ All code related to `items` is directly in component. 2function Component() { 3 const [items, setItems] = useState([]); 4 const [filters, setFilters] = useState({}); 5 6 useEffect(() => { 7 const filteredItems = filterItems(items, filters); 8 }, [filters]); 9 10 function handleFilters(newFilters) { 11 setFilters(newFilters); 12 } 13 14 // ... other code 15} 16
You can create useFilteredItems.ts and put all of the functionality there.
1// ✅ All code related to `items` is in custom re-usable hook 2export function useFilteredItems() { 3 const [items, setItems] = useState([]); 4 const [filters, setFilters] = useState({}); 5 6 useEffect(() => { 7 const filteredItems = filterItems(items, filters); 8 }, [filters]); 9 10 function handleFilters(newFilters) { 11 setFilters(newFilters); 12 } 13 14 return { 15 items, 16 filters, 17 handleFilters, 18 }; 19} 20
Then in Component you can use the hook instead.
1// ✅ Component is cleaner, and can share functionality of filtered items 2function Component() { 3 const { items, filters, handleFilters } = useFilteredItems(); 4 5 // ... other code 6} 7
7. Higher Order Components (HOC)
Sometimes, it's better to create a higher order component (HOC) to share re-usable functionality.
1function Button(props) { 2 // ❌ Styles object is duplicated 3 const style = { padding: 8, margin: 12 }; 4 return <button style={style} {...props} />; 5} 6 7function TextInput(props) { 8 // ❌ Styles object is duplicated 9 const style = { padding: 8, margin: 12 }; 10 return <input type="text" style={style} {...props} />; 11} 12
With HOCs, you can create a wrapper component that takes a component with its props, and enhances it.
1// ✅ Higher order component to implement styles 2function withStyles(Component) { 3 return props => { 4 const style = { padding: 8, margin: 12 }; 5 6 // Merges component props with custom style object 7 return <Component style={style} {...props} />; 8 }; 9} 10 11// Inner components receive style through props 12function Button({ style, ...props }) { 13 return <button style={style} {...props} />; 14} 15function TextInput({ style, ...props }) { 16 return <input type="text" style={style} {...props} />; 17} 18 19// ✅ Wrap exports with HOC 20export default withStyles(Button); 21export default withStyles(Text); 22
8. Variant Props
If you have components that are shared across the app, create variant props to easily customize them using preset values.
1type ButtonProps = ComponentProps<'button'> & { 2 variant?: 'primary' | 'secondary'; 3 size?: 'sm' | 'md' | 'lg'; 4}; 5 6function Button({ variant = 'primary', size = 'md', ...rest }: ButtonProps) { 7 // ✅ Styles derived based on variant and size 8 const style = { 9 ...styles.variant[variant], 10 ...styles.size[size], 11 }; 12 13 return <button style={style} {...rest} />; 14} 15 16// ✅ Custom object with clearly defined styles for every variant/size 17const styles = { 18 variant: { 19 primary: { 20 backgroundColor: 'blue', 21 }, 22 secondary: { 23 backgroundColor: 'gray', 24 }, 25 }, 26 size: { 27 sm: { 28 minHeight: 10, 29 }, 30 md: { 31 minHeight: 12, 32 }, 33 lg: { 34 minHeight: 16, 35 }, 36 }, 37}; 38
Example usage:
1function App() { 2 return ( 3 <div> 4 <Button>Primary Button</Button> 5 <Button variant="secondary" size="sm"> 6 Secondary Button 7 </Button> 8 </div> 9 ); 10} 11
9. Expose functionality through ref
Sometimes it can be useful to export functionality from one child component to a parent through a ref. This can be done using the useImperativeHandle hook.
1type Props = { 2 componentRef: React.RefObject<{ reset: () => void }>; 3}; 4 5function Component({ componentRef }: Props) { 6 const [count, setCount] = useState(0); 7 8 // ✅ Exposes custom reset function to parent through ref to change state 9 useImperativeHandle(componentRef, () => ({ 10 reset: () => { 11 setCount(0); 12 }, 13 })); 14 15 return ( 16 <div> 17 {count} 18 <button onClick={() => setCount(count + 1)}>Increment</button> 19 </div> 20 ); 21} 22
And to use it, simply create a ref in the same component where it is rendered.
1function App() { 2 const componentRef = useRef(null); 3 4 return ( 5 <> 6 <Component componentRef={componentRef} /> 7 8 {/* ✅ Using the ref we can reset the inner state of Component */} 9 <button onClick={() => componentRef.current?.reset()}>Reset</button> 10 </> 11 ); 12} 13
10. Use providers for frequently used data
If you have data that is shared across multiple components, consider putting it in a provider using the Context API.
1function Component1() { 2 // ❌ User is fetched in multiple components 3 const { data: user } = useFetchUser(); 4 5 // ❌ Unnecessary duplicate check for undefined user 6 if (!user) { 7 return <div>Loading...</div>; 8 } 9 10 // ... return JSX 11} 12 13function Component2() { 14 // ❌ User is fetched in multiple components 15 const { data: user } = useFetchUser(); 16 17 // ❌ Unnecessary duplicate check for undefined user 18 if (!user) { 19 return <div>Loading...</div>; 20 } 21 22 // ... return JSX 23} 24
With a Provider, we can have all of that functionality inside a single component.
1const UserContext = createContext(undefined); 2 3function UserProvider({ children }) { 4 // ✅ User fetch is done in provider 5 const { data: user } = useFetchUser(); 6 7 // ✅ User check is done in provider 8 if (!user) { 9 return <div>Loading...</div>; 10 } 11 12 return ( 13 {/* ✅ User is always going to be available from here on */} 14 <UserContext.Provider value={{ user }}>{children}</UserContext.Provider> 15 ); 16} 17 18// Custom hook to easily access context 19export function useUser() { 20 const context = useContext(UserContext); 21 22 if (!context) { 23 throw new Error('useUser must be used within a UserProvider.'); 24 } 25 26 return context; 27} 28
After wrapping the entire app with it, you can use the shared functionality everywhere.
1function App() { 2 return ( 3 {/* ✅ Wrap every component with the provider */} 4 <UserProvider> 5 <Component1 /> 6 <Component2 /> 7 </UserProvider> 8 ); 9} 10function Component1() { 11 // ✅ User is accessed from provider 12 const { user } = useUser(); 13 14 // ✅ Can directly use user without checking if it is there 15} 16 17function Component2() { 18 // ✅ User is accessed from provider 19 const { user } = useUser(); 20 21 // ✅ Can directly use user without checking if it is there 22} 23
