If you're a React developer, you probably know how exciting and fun it is to build user interfaces. But as projects grow bigger, things can get messy and hard to maintain. That's where React Design Patterns come in to save the day! In this article, we're going to cover 11 important design patterns that can make your React code: cleaner more efficient easier to understand Mastering design patterns is the step towards becoming a senior web developer But before we dive into the list, let's break down what design patterns actually are and why you should care about them. What is a Design Pattern in Coding? A design pattern is a tried-and-tested solution to a common coding problem. A design pattern is a tried-and-tested solution to a common coding problem. Instead of reinventing the wheel every time you write code, you can use a design pattern to solve the issue in a reliable way. Think of it like a blueprint for your code. These patterns are not code that you copy and paste, but ideas and structures you can use to improve your work. They help developers organize their projects better and avoid common pitfalls. Think of it like a blueprint for your code. Why Use Design Patterns in React? Using design patterns is essential because they: Make Your Code Easy to Read: Clear patterns mean other developers (or future you) can understand your code faster. Reduce Bugs: Structured code leads to fewer mistakes. Boost Efficiency: You don't have to solve the same problems over and over. Improve Collaboration: Teams can work more effectively with shared patterns. Scale Better: When your app gets bigger, design patterns keep things from getting chaotic. You can use design patterns as a benchmark for code quality standards Now that you know why they matter, let’s get into the 12 React design patterns you should know! 11 React Design Patterns Design Pattern #1: Container and Presentational Components This pattern helps you separate the logic of your app (containers) from the display (presentational components). It keeps your code organized and makes each part easier to manage. What Are Container and Presentational Components? Container Components handle the logic and data fetching. They do not concern themselves with how things look. Presentational Components focus on the UI. They receive data from props and render it. Purpose The purpose of this pattern is to separate concerns. Containers handle logic, while presentational components handle UI. This makes your code easier to understand, test, and maintain. Tips Keep Presentational Components Dumb: They should only care about displaying data, not where it comes from. Reusable UI: Because presentational components are decoupled from logic, you can reuse them in different parts of your app. Pros and Cons Pros ✅ Cons ❌ Clear separation of logic and UI Can lead to more files and components Easier to test (containers and UI separately) Might feel like overkill for simple apps Promotes reusable UI components Best For Medium to large applications Projects with complex data-fetching logic Code example Presentational component It displays data - that's it. // UserList.jsx const UserList = ({ users }) => ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); export default UserList; Container component It performs a logic - in this case fetching data. // UserListContainer.jsx import { useEffect, useState } from 'react'; import UserList from './UserList'; const UserListContainer = () => { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(setUsers); }, []); return <UserList users={users} />; }; export default UserListContainer; Design Pattern #2: Custom hooks Custom hooks allow you to extract and reuse stateful logic in your React components. They help you avoid repeating the same logic across multiple components by packaging that logic into a reusable function. Why Use Custom Hooks When components share the same logic (e.g., fetching data, handling form inputs), custom hooks allow you to abstract this logic and reuse it. Naming Convention Custom hooks should always begin with use, which follows React's built-in hooks convention (like useState, useEffect). Example: useDataFetch() Purpose The goal of custom hooks is to make your code DRY (Don't Repeat Yourself) by reusing stateful logic. This keeps your components clean, focused, and easier to understand. Tips Keep It Focused: Custom hooks should solve a specific problem (e.g., data fetching, form handling). Return What You Need: Return only the data and functions your component needs. Use Other Hooks Inside: Custom hooks can call other React hooks like useState, useEffect, or even other custom hooks. Pros and Cons Pros ✅ Cons ❌ Reduces code duplication Can make the code harder to follow if overused Keeps components clean and focused Easy to test and reuse Best For Reusable logic that involves state or effects Fetching data, authentication and form handling Code Example // useFetch.js import { useState, useEffect } from 'react'; const useFetch = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch(url) .then((res) => res.json()) .then((data) => { setData(data); setLoading(false); }) .catch((error) => { setError(error); setLoading(false); }); }, [url]); return { data, loading, error }; }; export default useFetch; // Component using the custom hook import useFetch from './useFetch'; const UserList = () => { const { data: users, loading, error } = useFetch('https://api.example.com/users'); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; export default UserList; Have you noticed that in this code example we also used Design Pattern #1: Container and Presentational Components 😊 When NOT to Use Custom Hooks If the logic is very specific to one component and unlikely to be reused. If it introduces unnecessary abstraction, making the code harder to understand. To reuse JSX markup, create a component. To reuse logic without React hooks, create a utility function To reuse logic with React hooks, create a custom hook Design Pattern #3: Compound Components A compound component in React is a design pattern where a component is composed of several smaller components that work together. The idea is to create a flexible and reusable component system where each subcomponent has its own specific responsibility, but they work together to form a cohesive whole. It’s like building a set of Lego pieces that are designed to fit together. Real life example A good example is the <BlogCard> component. Its typical children include a title, description, image, and a “Read More” button. Since the blog consists of multiple pages, you might want to display <BlogCard> differently depending on the context. For instance, you might exclude the image on a search results page or display the image above the title on another page. One way to achieve this is by using props and conditional rendering. However, if there are many variations, your code can quickly become clumsy. This is where Compound Components come in handy. 😊 Example Use Cases Tabs Dropdown Menus Accordions Blog & Product Card Purpose The purpose of the Compound Component pattern is to give users flexibility in composing UI elements while maintaining a shared state and behavior. Tips Keep State in the Parent: The parent component should manage the shared state. Use Context for Deep Nesting: If you have many nested components, React Context can simplify passing state. Pros and Cons Pros ✅ Cons ❌ Provides flexibility to compose components Can be complex for beginners Keeps related components encapsulated Harder to understand if components are deeply nested Best For UI patterns like tabs, accordions, dropdowns, and cards Components that need shared state between parts Code Example // ProductCard.jsx export default function ProductCard({ children }) { return ( <> <div className='product-card'>{children}</div>; </> ); } ProductCard.Title = ({ title }) => { return <h2 className='product-title'>{title}</h2>; }; ProductCard.Image = ({ imageSrc }) => { return <img className='product-image' src={imageSrc} alt='Product' />; }; ProductCard.Price = ({ price }) => { return <p className='product-price'>${price}</p>; }; ProductCard.Title.displayName = 'ProductCard.Title'; ProductCard.Image.displayName = 'ProductCard.Image'; ProductCard.Price.displayName = 'ProductCard.Price'; // App.jsx import ProductCard from './components/ProductCard'; export default function App() { return ( <> <ProductCard> <ProductCard.Image imageSrc='https://via.placeholder.com/150' /> <ProductCard.Title title='Product Title' /> <ProductCard.Price price='9.99' /> </ProductCard> </> ); } You can layout inner components in any order 🙂 Design Pattern #4: Prop Combination The Prop Combination pattern allows you to modify the behavior or appearance of a component by passing different combinations of props. Instead of creating multiple versions of a component, you control variations through the props. This pattern helps you achieve flexibility and customization without cluttering your codebase with many similar components. Common Use Cases Buttons with different styles (e.g., primary, secondary, disabled) Cards with optional elements like images, icons, or titles Default Values: You can set default values for props to avoid unexpected behavior when no props are provided. Purpose The purpose of this pattern is to provide a simple way to create variations of a component without duplicating code. This keeps your components clean and easy to maintain. Tips Combine Boolean Props: For simple variations, use boolean props (e.g., isPrimary, isDisabled). Avoid Too Many Props: If a component requires too many props to control behavior, consider breaking it into smaller components. Use Design Pattern #3: Compound Components 🙂 Use Default Props: Set default values to handle missing props gracefully. Pros and Cons Pros ✅ Cons ❌ Reduces the need for multiple similar components Can lead to "prop explosion" if overused Easy to customize behavior and appearance Complex combinations may become hard to understand Keeps code DRY (Don't Repeat Yourself) Best For Buttons, cards, alerts, and similar components Components with multiple configurable states Code Example Let's say you're building a Button component that can vary in style, size, and whether it's disabled: // Button.jsx const Button = ({ type = 'primary', size = 'medium', disabled = false, children, onClick }) => { let className = `btn ${type} ${size}`; if (disabled) className += ' disabled'; return ( <button className={className} onClick={onClick} disabled={disabled}> {children} </button> ); }; // App.jsx import Button from './components/Button'; const App = () => ( <div> <Button type="primary" size="large" onClick={() => alert('Primary Button')}> Primary Button </Button> <Button type="secondary" size="small" disabled> Disabled Secondary Button </Button> <Button type="danger" size="medium"> Danger Button </Button> </div> ); Design Pattern #5: Controlled components Controlled inputs are form elements whose values are controlled by React state. In this pattern, the form input's value is always in sync with the component's state, making React the single source of truth for the input data. This pattern is often used for input fields, text areas, checkboxes, and select elements. The value of the input element is bound to a piece of React state. When the state changes, the input reflects that change. Controlled vs. Uncontrolled Components: Controlled Components have their value controlled by React state. Uncontrolled Components rely on the DOM to manage their state (e.g., using ref to access values). Purpose The purpose of using controlled components is to have full control over form inputs, making the component behavior predictable and consistent. This is especially useful when you need to validate inputs, apply formatting, or submit data dynamically. Tips Use onChange Events: Always update state through the onChange event handler to keep the input value in sync with the state. Initialize State: Set an initial state value to avoid undefined inputs. Form Validation: Use controlled components to apply real-time validation or formatting. Pros and Cons Pros ✅ Cons ❌ Easy to validate and manipulate inputs Can require more boilerplate code Makes form elements predictable and easier to debug May lead to performance issues with very large forms Full control over user input Best For Forms with Validation: When you need real-time validation or feedback on input fields. Dynamic Forms: When form inputs depend on dynamic data or logic. Complex User Inputs: When inputs require transformations, like formatting phone numbers or emails. Code Example import { useState } from 'react'; function MyForm() { const [name, setName] = useState(''); const handleChange = (e) => { setName(e.target.value); }; return ( <form> <input type="text" value={name} onChange={handleChange} /> <p>Your name is: {name}</p> </form> ); } Design Pattern #6: Error boundaries Error Boundaries are React components that catch JavaScript errors in their child component tree during rendering, lifecycle methods, and event handlers. Instead of crashing the entire application, Error Boundaries display a fallback UI to handle errors gracefully. This pattern is crucial for making React applications more robust and user-friendly. Purpose The purpose of Error Boundaries is to prevent an entire application from crashing when a component encounters an error. Instead, they show a user-friendly fallback UI, allowing the rest of the application to remain functional. Tips Wrap Critical Components: Use Error Boundaries around components that are likely to fail (e.g., third-party integrations). Logging Errors: Log errors into services like Sentry or LogRocket for debugging. Fallback UI: Design a clear fallback UI to inform users that something went wrong. Pros and Cons Pros ✅ Cons ❌ Prevents the entire app from crashing Cannot catch errors in event handlers or asynchronous code Provides a fallback UI for a better user experience Helps catch and log errors in production Best For Large Applications: Where errors in one component shouldn't crash the entire app. Third-Party Integrations: When embedding third-party widgets that may fail unpredictably. Complex UI Components: For components with dynamic content or heavy rendering logic. Code Example React has a built in way to use Error Boundary design pattern. But it’s a bit outdated (still uses a class component). A better recommendation would to use a dedicated npm library: react-error-boundary import { ErrorBoundary } from "react-error-boundary"; <ErrorBoundary fallback={<div>Something went wrong</div>}> <App /> </ErrorBoundary> Design Pattern #7: Lazy Loading (Code Splitting) Lazy Loading is a technique where components or parts of your app are loaded only when they are needed. Instead of loading everything at once when the app starts, lazy loading helps split the code into smaller chunks and load them on demand. This improves performance by reducing the initial load time of your application. How Does It Work in React? React supports lazy loading through the React.lazy() function and Suspense component. React.lazy(): This function lets you dynamically import a component. Suspense: Wraps around a lazily loaded component to show a fallback (like a loading spinner) while waiting for the component to load. Purpose The purpose of lazy loading is to optimize the application's performance by reducing the initial bundle size. This leads to faster load times, especially for large applications where not all components are needed immediately. Tips Split Routes: Use lazy loading for routes to load only the components necessary for each page. Error Boundaries: Combine lazy loading with Error Boundaries to handle failures in loading components. Design Pattern #6: Error boundaries 🙂 Pros and Cons Pros ✅ Cons ❌ Reduces initial load time Adds slight delays when loading components Improves performance for large apps Requires handling of loading states and errors Loads code on demand, saving bandwidth Complexity increases with too many chunks Best For Large Applications: Apps with many components or pages. Single Page Applications (SPA): Where different views are rendered dynamically. Non-Critical Components: Components that aren't needed during the initial render, such as modals or heavy widgets. Code Example // Profile.jsx const Profile = () => { return <h2>This is the Profile component!</h2>; }; export default Profile; // App.jsx import { Suspense, lazy } from 'react'; // Lazy load the Profile component const Profile = lazy(() => import('./Profile')); function App() { return ( <div> <h1>Welcome to My App</h1> {/* Suspense provides a fallback UI while the lazy component is loading */} <Suspense fallback={<div>Loading...</div>}> <Profile /> </Suspense> </div> ); } export default App; Design Pattern #8: Higher-Order Component (HOC) A higher-order component takes in a component as an argument and returns a supercharged component injected with additional data or functionality. HOCs are often used for logic reuse, such as authentication checks, fetching data, or adding styling. Signature of an HOC const EnhancedComponent = withSomething(WrappedComponent); WrappedComponent: The original component that is being enhanced. EnhancedComponent: The new component returned by the HOC. Naming ConventionHOCs are often named with thewith prefix, such as withAuth, withLogging, or withLoading. Tips Pure Functions: Keep your HOCs pure — they should not modify the original WrappedComponent. Props Forwarding: Always pass down the props to the WrappedComponent to ensure it receives everything it needs. Pros and Cons Pros ✅ Cons ❌ Promotes code reuse Can lead to "wrapper hell" (too many nested HOCs) Keeps components clean and focused on their main task Harder to debug due to multiple layers of abstraction Best For Cross-Cutting Concerns: Adding shared logic like authentication, logging, or theming. Reusable Enhancements: When multiple components need the same behavior. Complex Applications: Apps where common logic needs to be abstracted away for readability. Higher-Order Component (HOC) is an advanced React pattern Code Example Here’s an example of a Higher-Order Component that adds a loading state to a component: // HOC - withLoading.js // it returns a functional component const withLoading = (WrappedComponent) => { return ({ isLoading, ...props }) => { if (isLoading) { return <div>Loading...</div>; } return <WrappedComponent {...props} />; }; }; export default withLoading; // DataComponent.js const DataComponent = ({ data }) => { return <div>Data: {data}</div>; }; export default DataComponent; // App.js import { useState, useEffect } from 'react'; import withLoading from './withLoading'; import DataComponent from './DataComponent'; // supercharching with HOC const DataComponentWithLoading = withLoading(DataComponent); const App = () => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { setTimeout(() => { setData('Here is the data!'); setLoading(false); }, 2000); }, []); return ( <div> <h1>My App</h1> <DataComponentWithLoading isLoading={loading} data={data} /> </div> ); }; export default App; Design Pattern #9: State Management with Reducers When the app’s state is more complex instead of using useState to manage your application's state, you can use reducers. Reducers allow you to handle state transitions in a more predictable and organized way. A reducer is simply a function that takes the current state and an action, then returns the new state. Basics Reducer Function: A pure function that takes state and action as arguments and returns a new state. const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } }; Action: An object that describes what kind of state update should happen. Actions usually have a type field and may include additional data (payload). Dispatch: A function used to send actions to the reducer, triggering a state update. Purpose This pattern is useful when the state logic becomes too complex for useState. It centralizes state updates, making your code easier to manage, debug, and scale. Tips Keep Reducers Pure: Ensure your reducer function has no side effects (no API calls or asynchronous code). Use Constants for Action Types: To avoid typos, define action types as constants. Pros and Cons Pros ✅ Cons ❌ Simplifies complex state logic Adds boilerplate code (actions, dispatch, etc.) Centralizes state updates for easier debugging Can be overkill for simple state management Makes state transitions predictable Requires learning curve for beginners Best For Complex State Logic: When state transitions depend on multiple conditions. Medium to Large Applications: Apps with multiple components sharing state. Code Example Here’s an example of state management with useReducer in a counter app: import { useReducer } from 'react'; // Step 1: Define the reducer function const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; case 'RESET': return { count: 0 }; default: return state; } }; // Step 2: Define the initial state const initialState = { count: 0 }; // Step 3: Create the component const Counter = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <h1>Count: {state.count}</h1> <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button> <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button> <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button> </div> ); }; export default Counter; In modern React development, Redux is the library that uses reducers for state management. Design Pattern #10: Data management with Providers (Context API) The provider pattern is very useful for data management as it utilizes the context API to pass data through the application's component tree. This pattern is an effective solution to prop drilling, which has been a common concern in react development. Context API is the solution to prop drilling Providers allow you to manage global state in a React application, making it accessible to any component that needs it. This pattern helps avoid prop drilling (passing props through many layers) by offering a way to "provide" data to a component tree. Basics Context: A React feature that allows you to create and share state globally. Provider: A component that supplies data to any component in its child tree. Consumer: A component that uses the data provided by the Provider. useContext Hook: A way to access context values without needing a Consumer. Purpose The purpose of this pattern is to simplify data sharing between deeply nested components by creating a global state accessible via a Provider. It helps keep code clean, readable, and free of unnecessary prop passing. Tips Use Context Wisely: Context is best for global state like themes, authentication, or user settings. Combine with Reducers: For complex state logic, combine Context with useReducer for more control. Split Contexts: Instead of one giant context, use multiple smaller contexts for different types of data. Pros and Cons Pros ✅ Cons ❌ Reduces prop drilling Not ideal for frequently changing data (can cause unnecessary re-renders) Centralizes data for easier access Performance issues if context value changes often Simple to set up for small to medium-sized apps Best For Global State: Sharing themes, authentication status, language settings, etc. Avoiding Prop Drilling: When data needs to be passed through multiple component layers. Medium Complexity Apps: Apps that need simple global state management. Code Example Here’s an example of data management with a ThemeProvider: // ThemeContext.jsx import { createContext, useState } from 'react'; export const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); }; // ThemeToggleButton.jsx import { useContext } from 'react'; import { ThemeContext } from './ThemeContext'; const ThemeToggleButton = () => { const { theme, toggleTheme } = useContext(ThemeContext); return ( <button onClick={toggleTheme}> Switch to {theme === 'light' ? 'dark' : 'light'} mode </button> ); }; export default ThemeToggleButton; // App.js import { ThemeProvider } from './ThemeContext'; import ThemeToggleButton from './ThemeToggleButton'; const App = () => { return ( <ThemeProvider> <div> <h1>Welcome to the App</h1> <ThemeToggleButton /> </div> </ThemeProvider> ); }; export default App; In React 19, you can render <Context> as a provider instead of <Context.Provider> const ThemeContext = createContext(''); function App({children}) { return ( <ThemeContext value="dark"> {children} </ThemeContext> ); } Design Pattern #11: Portals Portals allow you to render children into a different part of the DOM tree that exists outside the parent component's hierarchy. This is useful for rendering elements like modals, tooltips, or overlays that need to be displayed outside the normal DOM flow of the component. Even though the DOM parent changes, the React component structure stays the same. Purpose The purpose of this pattern is to provide a way to render components outside the parent component hierarchy, making it easy to manage certain UI elements that need to break out of the flow, without disrupting the structure of the main React tree. Tips Modals and Overlays: Portals are perfect for rendering modals, tooltips, and other UI elements that need to appear on top of other content. Avoid Overuse: While useful, portals should be used only when necessary, as they can complicate the component hierarchy and event propagation. Pros and Cons Pros ✅ Cons ❌ Keeps the component tree clean and avoids layout issues Can complicate event propagation (e.g., click events may not bubble) Best For Overlays: Modals, tooltips, or any UI element that needs to appear on top of other content. Breaking DOM Flow: When elements need to break out of the standard component hierarchy (e.g., notifications). Code Example // Modal.jsx import { useEffect } from 'react'; import ReactDOM from 'react-dom'; const Modal = ({ isOpen, closeModal, children }) => { // Prevent body scrolling when modal is open useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden'; } return () => { document.body.style.overflow = 'unset'; }; }, [isOpen]); if (!isOpen) return null; return ReactDOM.createPortal( <> {/* Overlay */} <div style={overlayStyles} onClick={closeModal} /> {/* Modal */} <div style={modalStyles}> {children} <button onClick={closeModal}>Close</button> </div> </>, document.getElementById('modal-root') ); }; const overlayStyles = { ... }; const modalStyles = { ... }; export default Modal; // App.js import { useState } from 'react'; import Modal from './Modal'; const App = () => { const [isModalOpen, setIsModalOpen] = useState(false); return ( <div> <h1>React Portals Example</h1> <button onClick={() => setIsModalOpen(true)}>Open Modal</button> <Modal isOpen={isModalOpen} closeModal={() => setIsModalOpen(false)}> <h2>Modal Content</h2> <p>This is the modal content</p> </Modal> </div> ); }; export default App; // index.html <body> <div id="root"></div> <div id="modal-root"></div> </body> What’s in the end? Learning and mastering design patterns is a crucial step toward becoming a senior web developer. 🆙 These patterns are not just theoretical; they address real-world challenges like state management, performance optimization, and UI component architecture. By adopting them in your everyday work, you'll be equipped to solve a variety of development challenges and create applications that are both performant and easy to maintain. Liked the article? 😊 You can learn more at my personal Javascript blog ➡️ https://jssecrets.com/. Or you can see my projects and read case studies at my personal website ➡️ https://ilyasseisov.com/. Happy coding! 😊 If you're a React developer, you probably know how exciting and fun it is to build user interfaces. But as projects grow bigger, things can get messy and hard to maintain. That's where React Design Patterns come in to save the day! Design Patterns In this article, we're going to cover 11 important design patterns that can make your React code: cleaner more efficient easier to understand cleaner more efficient easier to understand Mastering design patterns is the step towards becoming a senior web developer Mastering design patterns is the step towards becoming a senior web developer But before we dive into the list, let's break down what design patterns actually are and why you should care about them. What is a Design Pattern in Coding? A design pattern is a tried-and-tested solution to a common coding problem. A design pattern is a tried-and-tested solution to a common coding problem. design pattern A design pattern is a tried-and-tested solution to a common coding problem. Instead of reinventing the wheel every time you write code, you can use a design pattern to solve the issue in a reliable way. Think of it like a blueprint for your code. design pattern These patterns are not code that you copy and paste , but ideas and structures you can use to improve your work . They help developers organize their projects better and avoid common pitfalls. not code that you copy and paste ideas and structures you can use to improve your work Think of it like a blueprint for your code. Think of it like a blueprint for your code. Why Use Design Patterns in React? Using design patterns is essential because they: Make Your Code Easy to Read: Clear patterns mean other developers (or future you) can understand your code faster. Reduce Bugs: Structured code leads to fewer mistakes. Boost Efficiency: You don't have to solve the same problems over and over. Improve Collaboration: Teams can work more effectively with shared patterns. Scale Better: When your app gets bigger, design patterns keep things from getting chaotic. Make Your Code Easy to Read: Clear patterns mean other developers (or future you) can understand your code faster. Make Your Code Easy to Read: Reduce Bugs: Structured code leads to fewer mistakes. Reduce Bugs: Boost Efficiency: You don't have to solve the same problems over and over. Boost Efficiency: Improve Collaboration: Teams can work more effectively with shared patterns. Improve Collaboration: Scale Better: When your app gets bigger, design patterns keep things from getting chaotic. Scale Better: You can use design patterns as a benchmark for code quality standards You can use design patterns as a benchmark for code quality standards Now that you know why they matter, let’s get into the 12 React design patterns you should know! 11 React Design Patterns Design Pattern #1: Container and Presentational Components This pattern helps you separate the logic of your app (containers) from the display (presentational components). It keeps your code organized and makes each part easier to manage. What Are Container and Presentational Components? What Are Container and Presentational Components? Container Components handle the logic and data fetching. They do not concern themselves with how things look. Presentational Components focus on the UI. They receive data from props and render it. Container Components handle the logic and data fetching . They do not concern themselves with how things look. Container Components logic data fetching Presentational Components focus on the UI . They receive data from props and render it. Presentational Components UI Purpose Purpose The purpose of this pattern is to separate concerns . separate concerns Containers handle logic , while presentational components handle UI . Containers handle logic presentational components handle UI This makes your code easier to understand, test, and maintain. Tips Tips Keep Presentational Components Dumb: They should only care about displaying data, not where it comes from. Reusable UI: Because presentational components are decoupled from logic, you can reuse them in different parts of your app. Keep Presentational Components Dumb: They should only care about displaying data, not where it comes from. Keep Presentational Components Dumb: Reusable UI: Because presentational components are decoupled from logic, you can reuse them in different parts of your app. Reusable UI: Pros and Cons Pros and Cons Pros ✅ Cons ❌ Clear separation of logic and UI Can lead to more files and components Easier to test (containers and UI separately) Might feel like overkill for simple apps Promotes reusable UI components Pros ✅ Cons ❌ Clear separation of logic and UI Can lead to more files and components Easier to test (containers and UI separately) Might feel like overkill for simple apps Promotes reusable UI components Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Clear separation of logic and UI Can lead to more files and components Clear separation of logic and UI Clear separation of logic and UI Can lead to more files and components Can lead to more files and components Easier to test (containers and UI separately) Might feel like overkill for simple apps Easier to test (containers and UI separately) Easier to test (containers and UI separately) Might feel like overkill for simple apps Might feel like overkill for simple apps Promotes reusable UI components Promotes reusable UI components Promotes reusable UI components Best For Best For Medium to large applications Projects with complex data-fetching logic Medium to large applications Projects with complex data-fetching logic Code example Code example Presentational component Presentational component It displays data - that's it. // UserList.jsx const UserList = ({ users }) => ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); export default UserList; // UserList.jsx const UserList = ({ users }) => ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); export default UserList; Container component Container component It performs a logic - in this case fetching data. // UserListContainer.jsx import { useEffect, useState } from 'react'; import UserList from './UserList'; const UserListContainer = () => { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(setUsers); }, []); return <UserList users={users} />; }; export default UserListContainer; // UserListContainer.jsx import { useEffect, useState } from 'react'; import UserList from './UserList'; const UserListContainer = () => { const [users, setUsers] = useState([]); useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(setUsers); }, []); return <UserList users={users} />; }; export default UserListContainer; Design Pattern #2: Custom hooks Custom hooks allow you to extract and reuse stateful logic in your React components . They help you avoid repeating the same logic across multiple components by packaging that logic into a reusable function. reuse stateful logic in your React components Why Use Custom Hooks Why Use Custom Hooks When components share the same logic (e.g., fetching data, handling form inputs), custom hooks allow you to abstract this logic and reuse it. Naming Convention Naming Convention Custom hooks should always begin with use , which follows React's built-in hooks convention (like useState , useEffect ). use useState useEffect Example: useDataFetch() useDataFetch() Purpose The goal of custom hooks is to make your code DRY (Don't Repeat Yourself) by reusing stateful logic. This keeps your components clean, focused, and easier to understand. DRY (Don't Repeat Yourself) Tips Keep It Focused: Custom hooks should solve a specific problem (e.g., data fetching, form handling). Return What You Need: Return only the data and functions your component needs. Use Other Hooks Inside: Custom hooks can call other React hooks like useState, useEffect, or even other custom hooks. Keep It Focused: Custom hooks should solve a specific problem (e.g., data fetching, form handling). Keep It Focused: Return What You Need: Return only the data and functions your component needs. Return What You Need: Use Other Hooks Inside: Custom hooks can call other React hooks like useState , useEffect , or even other custom hooks. Use Other Hooks Inside: useState useEffect Pros and Cons Pros ✅ Cons ❌ Reduces code duplication Can make the code harder to follow if overused Keeps components clean and focused Easy to test and reuse Pros ✅ Cons ❌ Reduces code duplication Can make the code harder to follow if overused Keeps components clean and focused Easy to test and reuse Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Reduces code duplication Can make the code harder to follow if overused Reduces code duplication Reduces code duplication Can make the code harder to follow if overused Can make the code harder to follow if overused Keeps components clean and focused Keeps components clean and focused Keeps components clean and focused Easy to test and reuse Easy to test and reuse Easy to test and reuse Best For Reusable logic that involves state or effects Fetching data, authentication and form handling Reusable logic that involves state or effects Fetching data, authentication and form handling Code Example // useFetch.js import { useState, useEffect } from 'react'; const useFetch = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch(url) .then((res) => res.json()) .then((data) => { setData(data); setLoading(false); }) .catch((error) => { setError(error); setLoading(false); }); }, [url]); return { data, loading, error }; }; export default useFetch; // useFetch.js import { useState, useEffect } from 'react'; const useFetch = (url) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch(url) .then((res) => res.json()) .then((data) => { setData(data); setLoading(false); }) .catch((error) => { setError(error); setLoading(false); }); }, [url]); return { data, loading, error }; }; export default useFetch; // Component using the custom hook import useFetch from './useFetch'; const UserList = () => { const { data: users, loading, error } = useFetch('https://api.example.com/users'); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; export default UserList; // Component using the custom hook import useFetch from './useFetch'; const UserList = () => { const { data: users, loading, error } = useFetch('https://api.example.com/users'); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; export default UserList; Have you noticed that in this code example we also used Design Pattern #1: Container and Presentational Components 😊 Design Pattern #1: Container and Presentational Components 😊 When NOT to Use Custom Hooks If the logic is very specific to one component and unlikely to be reused. If it introduces unnecessary abstraction, making the code harder to understand. When NOT to Use Custom Hooks When NOT to Use Custom Hooks If the logic is very specific to one component and unlikely to be reused. If it introduces unnecessary abstraction, making the code harder to understand. If the logic is very specific to one component and unlikely to be reused. If it introduces unnecessary abstraction, making the code harder to understand. To reuse JSX markup, create a component. To reuse logic without React hooks, create a utility function To reuse logic with React hooks, create a custom hook To reuse JSX markup , create a component. JSX markup To reuse logic without React hooks , create a utility function logic without React hooks To reuse logic with React hooks , create a custom hook logic with React hooks Design Pattern #3: Compound Components A compound component in React is a design pattern where a component is composed of several smaller components that work together. The idea is to create a flexible and reusable component system where each subcomponent has its own specific responsibility, but they work together to form a cohesive whole. component is composed of several smaller components It’s like building a set of Lego pieces that are designed to fit together. Lego pieces Real life example A good example is the <BlogCard> component. Its typical children include a title, description, image, and a “Read More” button. Since the blog consists of multiple pages, you might want to display <BlogCard> differently depending on the context. <BlogCard> <BlogCard> For instance, you might exclude the image on a search results page or display the image above the title on another page. One way to achieve this is by using props and conditional rendering. However, if there are many variations, your code can quickly become clumsy. This is where Compound Components come in handy. 😊 Compound Components Example Use Cases Tabs Dropdown Menus Accordions Blog & Product Card Tabs Dropdown Menus Accordions Blog & Product Card Purpose The purpose of the Compound Component pattern is to give users flexibility in composing UI elements while maintaining a shared state and behavior. Tips Keep State in the Parent: The parent component should manage the shared state. Use Context for Deep Nesting: If you have many nested components, React Context can simplify passing state. Keep State in the Parent: The parent component should manage the shared state. Keep State in the Parent: Use Context for Deep Nesting: If you have many nested components, React Context can simplify passing state. Use Context for Deep Nesting: Pros and Cons Pros ✅ Cons ❌ Provides flexibility to compose components Can be complex for beginners Keeps related components encapsulated Harder to understand if components are deeply nested Pros ✅ Cons ❌ Provides flexibility to compose components Can be complex for beginners Keeps related components encapsulated Harder to understand if components are deeply nested Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Provides flexibility to compose components Can be complex for beginners Provides flexibility to compose components Provides flexibility to compose components Can be complex for beginners Can be complex for beginners Keeps related components encapsulated Harder to understand if components are deeply nested Keeps related components encapsulated Keeps related components encapsulated Harder to understand if components are deeply nested Harder to understand if components are deeply nested Best For UI patterns like tabs, accordions, dropdowns, and cards Components that need shared state between parts UI patterns like tabs, accordions, dropdowns, and cards Components that need shared state between parts Code Example // ProductCard.jsx export default function ProductCard({ children }) { return ( <> <div className='product-card'>{children}</div>; </> ); } ProductCard.Title = ({ title }) => { return <h2 className='product-title'>{title}</h2>; }; ProductCard.Image = ({ imageSrc }) => { return <img className='product-image' src={imageSrc} alt='Product' />; }; ProductCard.Price = ({ price }) => { return <p className='product-price'>${price}</p>; }; ProductCard.Title.displayName = 'ProductCard.Title'; ProductCard.Image.displayName = 'ProductCard.Image'; ProductCard.Price.displayName = 'ProductCard.Price'; // ProductCard.jsx export default function ProductCard({ children }) { return ( <> <div className='product-card'>{children}</div>; </> ); } ProductCard.Title = ({ title }) => { return <h2 className='product-title'>{title}</h2>; }; ProductCard.Image = ({ imageSrc }) => { return <img className='product-image' src={imageSrc} alt='Product' />; }; ProductCard.Price = ({ price }) => { return <p className='product-price'>${price}</p>; }; ProductCard.Title.displayName = 'ProductCard.Title'; ProductCard.Image.displayName = 'ProductCard.Image'; ProductCard.Price.displayName = 'ProductCard.Price'; // App.jsx import ProductCard from './components/ProductCard'; export default function App() { return ( <> <ProductCard> <ProductCard.Image imageSrc='https://via.placeholder.com/150' /> <ProductCard.Title title='Product Title' /> <ProductCard.Price price='9.99' /> </ProductCard> </> ); } // App.jsx import ProductCard from './components/ProductCard'; export default function App() { return ( <> <ProductCard> <ProductCard.Image imageSrc='https://via.placeholder.com/150' /> <ProductCard.Title title='Product Title' /> <ProductCard.Price price='9.99' /> </ProductCard> </> ); } You can layout inner components in any order 🙂 Design Pattern #4: Prop Combination The Prop Combination pattern allows you to modify the behavior or appearance of a component by passing different combinations of props. Instead of creating multiple versions of a component, you control variations through the props. Prop Combination This pattern helps you achieve flexibility and customization without cluttering your codebase with many similar components. This pattern helps you achieve flexibility and customization without cluttering your codebase with many similar components. without cluttering your codebase with many similar components. Common Use Cases Buttons with different styles (e.g., primary, secondary, disabled) Cards with optional elements like images, icons, or titles Buttons with different styles (e.g., primary, secondary, disabled) Cards with optional elements like images, icons, or titles Default Values: You can set default values for props to avoid unexpected behavior when no props are provided. Default Values: You can set default values for props to avoid unexpected behavior when no props are provided. Purpose The purpose of this pattern is to provide a simple way to create variations of a component without duplicating code. This keeps your components clean and easy to maintain. Tips Combine Boolean Props: For simple variations, use boolean props (e.g., isPrimary, isDisabled). Avoid Too Many Props: If a component requires too many props to control behavior, consider breaking it into smaller components. Use Design Pattern #3: Compound Components 🙂 Use Default Props: Set default values to handle missing props gracefully. Combine Boolean Props: For simple variations, use boolean props (e.g., isPrimary , isDisabled ). Combine Boolean Props: isPrimary isDisabled Avoid Too Many Props: If a component requires too many props to control behavior, consider breaking it into smaller components. Use Design Pattern #3: Compound Components 🙂 Avoid Too Many Props: Design Pattern #3: Compound Components Use Default Props: Set default values to handle missing props gracefully. Use Default Props: Pros and Cons Pros ✅ Cons ❌ Reduces the need for multiple similar components Can lead to "prop explosion" if overused Easy to customize behavior and appearance Complex combinations may become hard to understand Keeps code DRY (Don't Repeat Yourself) Pros ✅ Cons ❌ Reduces the need for multiple similar components Can lead to "prop explosion" if overused Easy to customize behavior and appearance Complex combinations may become hard to understand Keeps code DRY (Don't Repeat Yourself) Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Reduces the need for multiple similar components Can lead to "prop explosion" if overused Reduces the need for multiple similar components Reduces the need for multiple similar components Can lead to "prop explosion" if overused Can lead to "prop explosion" if overused Easy to customize behavior and appearance Complex combinations may become hard to understand Easy to customize behavior and appearance Easy to customize behavior and appearance Complex combinations may become hard to understand Complex combinations may become hard to understand Keeps code DRY (Don't Repeat Yourself) Keeps code DRY (Don't Repeat Yourself) Keeps code DRY (Don't Repeat Yourself) Best For Buttons, cards, alerts, and similar components Components with multiple configurable states Buttons, cards, alerts, and similar components Components with multiple configurable states Code Example Let's say you're building a Button component that can vary in style, size, and whether it's disabled: Button component // Button.jsx const Button = ({ type = 'primary', size = 'medium', disabled = false, children, onClick }) => { let className = `btn ${type} ${size}`; if (disabled) className += ' disabled'; return ( <button className={className} onClick={onClick} disabled={disabled}> {children} </button> ); }; // Button.jsx const Button = ({ type = 'primary', size = 'medium', disabled = false, children, onClick }) => { let className = `btn ${type} ${size}`; if (disabled) className += ' disabled'; return ( <button className={className} onClick={onClick} disabled={disabled}> {children} </button> ); }; // App.jsx import Button from './components/Button'; const App = () => ( <div> <Button type="primary" size="large" onClick={() => alert('Primary Button')}> Primary Button </Button> <Button type="secondary" size="small" disabled> Disabled Secondary Button </Button> <Button type="danger" size="medium"> Danger Button </Button> </div> ); // App.jsx import Button from './components/Button'; const App = () => ( <div> <Button type="primary" size="large" onClick={() => alert('Primary Button')}> Primary Button </Button> <Button type="secondary" size="small" disabled> Disabled Secondary Button </Button> <Button type="danger" size="medium"> Danger Button </Button> </div> ); Design Pattern #5: Controlled components Controlled inputs are form elements whose values are controlled by React state. In this pattern, the form input's value is always in sync with the component's state, making React the single source of truth for the input data. This pattern is often used for input fields, text areas, checkboxes, and select elements. This pattern is often used for input fields, text areas, checkboxes, and select elements. input fields, text areas, checkboxes, and select elements. The value of the input element is bound to a piece of React state. When the state changes, the input reflects that change. value Controlled vs. Uncontrolled Components: Controlled Components have their value controlled by React state. Uncontrolled Components rely on the DOM to manage their state (e.g., using ref to access values). Controlled vs. Uncontrolled Components: Controlled vs. Uncontrolled Components: Controlled Components have their value controlled by React state. Uncontrolled Components rely on the DOM to manage their state (e.g., using ref to access values). Controlled Components have their value controlled by React state. Controlled Components Uncontrolled Components rely on the DOM to manage their state (e.g., using ref to access values). Uncontrolled Components ref Purpose The purpose of using controlled components is to have full control over form inputs, making the component behavior predictable and consistent. This is especially useful when you need to validate inputs, apply formatting, or submit data dynamically. component behavior predictable and consistent. Tips Use onChange Events: Always update state through the onChange event handler to keep the input value in sync with the state. Initialize State: Set an initial state value to avoid undefined inputs. Form Validation: Use controlled components to apply real-time validation or formatting. Use onChange Events: Always update state through the onChange event handler to keep the input value in sync with the state. Use onChange onChange Initialize State: Set an initial state value to avoid undefined inputs. Initialize State: undefined Form Validation: Use controlled components to apply real-time validation or formatting. Form Validation: Pros and Cons Pros ✅ Cons ❌ Easy to validate and manipulate inputs Can require more boilerplate code Makes form elements predictable and easier to debug May lead to performance issues with very large forms Full control over user input Pros ✅ Cons ❌ Easy to validate and manipulate inputs Can require more boilerplate code Makes form elements predictable and easier to debug May lead to performance issues with very large forms Full control over user input Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Easy to validate and manipulate inputs Can require more boilerplate code Easy to validate and manipulate inputs Easy to validate and manipulate inputs Can require more boilerplate code Can require more boilerplate code Makes form elements predictable and easier to debug May lead to performance issues with very large forms Makes form elements predictable and easier to debug Makes form elements predictable and easier to debug May lead to performance issues with very large forms May lead to performance issues with very large forms Full control over user input Full control over user input Full control over user input Best For Forms with Validation: When you need real-time validation or feedback on input fields. Dynamic Forms: When form inputs depend on dynamic data or logic. Complex User Inputs: When inputs require transformations, like formatting phone numbers or emails. Forms with Validation: When you need real-time validation or feedback on input fields. Forms with Validation: Dynamic Forms: When form inputs depend on dynamic data or logic. Dynamic Forms: Complex User Inputs: When inputs require transformations, like formatting phone numbers or emails. Complex User Inputs: Code Example import { useState } from 'react'; function MyForm() { const [name, setName] = useState(''); const handleChange = (e) => { setName(e.target.value); }; return ( <form> <input type="text" value={name} onChange={handleChange} /> <p>Your name is: {name}</p> </form> ); } import { useState } from 'react'; function MyForm() { const [name, setName] = useState(''); const handleChange = (e) => { setName(e.target.value); }; return ( <form> <input type="text" value={name} onChange={handleChange} /> <p>Your name is: {name}</p> </form> ); } Design Pattern #6: Error boundaries Error Boundaries are React components that catch JavaScript errors in their child component tree during rendering, lifecycle methods, and event handlers. Instead of crashing the entire application, Error Boundaries display a fallback UI to handle errors gracefully . Error Boundaries display a fallback UI to handle errors gracefully This pattern is crucial for making React applications more robust and user-friendly. This pattern is crucial for making React applications more robust and user-friendly. Purpose The purpose of Error Boundaries is to prevent an entire application from crashing when a component encounters an error. Instead, they show a user-friendly fallback UI, allowing the rest of the application to remain functional. Tips Wrap Critical Components: Use Error Boundaries around components that are likely to fail (e.g., third-party integrations). Logging Errors: Log errors into services like Sentry or LogRocket for debugging. Fallback UI: Design a clear fallback UI to inform users that something went wrong. Wrap Critical Components: Use Error Boundaries around components that are likely to fail (e.g., third-party integrations). Wrap Critical Components: Logging Errors: Log errors into services like Sentry or LogRocket for debugging. Logging Errors: Fallback UI: Design a clear fallback UI to inform users that something went wrong. Fallback UI: Pros and Cons Pros ✅ Cons ❌ Prevents the entire app from crashing Cannot catch errors in event handlers or asynchronous code Provides a fallback UI for a better user experience Helps catch and log errors in production Pros ✅ Cons ❌ Prevents the entire app from crashing Cannot catch errors in event handlers or asynchronous code Provides a fallback UI for a better user experience Helps catch and log errors in production Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Prevents the entire app from crashing Cannot catch errors in event handlers or asynchronous code Prevents the entire app from crashing Prevents the entire app from crashing Cannot catch errors in event handlers or asynchronous code Cannot catch errors in event handlers or asynchronous code Provides a fallback UI for a better user experience Provides a fallback UI for a better user experience Provides a fallback UI for a better user experience Helps catch and log errors in production Helps catch and log errors in production Helps catch and log errors in production Best For Large Applications: Where errors in one component shouldn't crash the entire app. Third-Party Integrations: When embedding third-party widgets that may fail unpredictably. Complex UI Components: For components with dynamic content or heavy rendering logic. Large Applications: Where errors in one component shouldn't crash the entire app. Large Applications: Third-Party Integrations: When embedding third-party widgets that may fail unpredictably. Third-Party Integrations: Complex UI Components: For components with dynamic content or heavy rendering logic. Complex UI Components: Code Example React has a built in way to use Error Boundary design pattern. But it’s a bit outdated (still uses a class component). A better recommendation would to use a dedicated npm library: react-error-boundary React has a built in way to use Error Boundary design pattern. But it’s a bit outdated (still uses a class component). A better recommendation would to use a dedicated npm library: react-error-boundary react-error-boundary react-error-boundary import { ErrorBoundary } from "react-error-boundary"; <ErrorBoundary fallback={<div>Something went wrong</div>}> <App /> </ErrorBoundary> import { ErrorBoundary } from "react-error-boundary"; <ErrorBoundary fallback={<div>Something went wrong</div>}> <App /> </ErrorBoundary> Design Pattern #7: Lazy Loading (Code Splitting) Lazy Loading is a technique where components or parts of your app are loaded only when they are needed . Instead of loading everything at once when the app starts, lazy loading helps split the code into smaller chunks and load them on demand . This improves performance by reducing the initial load time of your application. loaded only when they are needed load them on demand improves performance How Does It Work in React? React supports lazy loading through the React.lazy() function and Suspense component. React.lazy() Suspense React.lazy(): This function lets you dynamically import a component. Suspense: Wraps around a lazily loaded component to show a fallback (like a loading spinner) while waiting for the component to load. React.lazy() : This function lets you dynamically import a component. React.lazy() Suspense : Wraps around a lazily loaded component to show a fallback (like a loading spinner) while waiting for the component to load. Suspense Purpose The purpose of lazy loading is to optimize the application's performance by reducing the initial bundle size. This leads to faster load times, especially for large applications where not all components are needed immediately. Tips Split Routes: Use lazy loading for routes to load only the components necessary for each page. Error Boundaries: Combine lazy loading with Error Boundaries to handle failures in loading components. Design Pattern #6: Error boundaries 🙂 Split Routes : Use lazy loading for routes to load only the components necessary for each page. Split Routes Error Boundaries : Combine lazy loading with Error Boundaries to handle failures in loading components. Design Pattern #6: Error boundaries 🙂 Error Boundaries Design Pattern #6: Error boundaries Pros and Cons Pros ✅ Cons ❌ Reduces initial load time Adds slight delays when loading components Improves performance for large apps Requires handling of loading states and errors Loads code on demand, saving bandwidth Complexity increases with too many chunks Pros ✅ Cons ❌ Reduces initial load time Adds slight delays when loading components Improves performance for large apps Requires handling of loading states and errors Loads code on demand, saving bandwidth Complexity increases with too many chunks Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Reduces initial load time Adds slight delays when loading components Reduces initial load time Reduces initial load time Adds slight delays when loading components Adds slight delays when loading components Improves performance for large apps Requires handling of loading states and errors Improves performance for large apps Improves performance for large apps Requires handling of loading states and errors Requires handling of loading states and errors Loads code on demand, saving bandwidth Complexity increases with too many chunks Loads code on demand, saving bandwidth Loads code on demand, saving bandwidth Complexity increases with too many chunks Complexity increases with too many chunks Best For Large Applications: Apps with many components or pages. Single Page Applications (SPA): Where different views are rendered dynamically. Non-Critical Components: Components that aren't needed during the initial render, such as modals or heavy widgets. Large Applications : Apps with many components or pages. Large Applications Single Page Applications (SPA) : Where different views are rendered dynamically. Single Page Applications (SPA) Non-Critical Components : Components that aren't needed during the initial render, such as modals or heavy widgets. Non-Critical Components Code Example // Profile.jsx const Profile = () => { return <h2>This is the Profile component!</h2>; }; export default Profile; // Profile.jsx const Profile = () => { return <h2>This is the Profile component!</h2>; }; export default Profile; // App.jsx import { Suspense, lazy } from 'react'; // Lazy load the Profile component const Profile = lazy(() => import('./Profile')); function App() { return ( <div> <h1>Welcome to My App</h1> {/* Suspense provides a fallback UI while the lazy component is loading */} <Suspense fallback={<div>Loading...</div>}> <Profile /> </Suspense> </div> ); } export default App; // App.jsx import { Suspense, lazy } from 'react'; // Lazy load the Profile component const Profile = lazy(() => import('./Profile')); function App() { return ( <div> <h1>Welcome to My App</h1> {/* Suspense provides a fallback UI while the lazy component is loading */} <Suspense fallback={<div>Loading...</div>}> <Profile /> </Suspense> </div> ); } export default App; Design Pattern #8: Higher-Order Component (HOC) A higher-order component takes in a component as an argument and returns a supercharged component injected with additional data or functionality. takes in a component as an argument and returns a supercharged component injected with additional data or functionality. HOCs are often used for logic reuse, such as authentication checks, fetching data, or adding styling. HOCs are often used for logic reuse, such as authentication checks, fetching data, or adding styling. authentication checks, fetching data, or adding styling. Signature of an HOC const EnhancedComponent = withSomething(WrappedComponent); const EnhancedComponent = withSomething(WrappedComponent); WrappedComponent: The original component that is being enhanced. EnhancedComponent: The new component returned by the HOC. Naming ConventionHOCs are often named with thewith prefix, such as withAuth, withLogging, or withLoading. WrappedComponent: The original component that is being enhanced. WrappedComponent : The original component that is being enhanced. WrappedComponent EnhancedComponent: The new component returned by the HOC. Naming ConventionHOCs are often named with thewith prefix, such as withAuth, withLogging, or withLoading. EnhancedComponent : The new component returned by the HOC. EnhancedComponent Naming Convention HOCs are often named with the with prefix, such as withAuth , withLogging , or withLoading . with withAuth withLogging withLoading Tips Pure Functions: Keep your HOCs pure — they should not modify the original WrappedComponent. Props Forwarding: Always pass down the props to the WrappedComponent to ensure it receives everything it needs. Pure Functions : Keep your HOCs pure — they should not modify the original WrappedComponent . Pure Functions WrappedComponent Props Forwarding : Always pass down the props to the WrappedComponent to ensure it receives everything it needs. Props Forwarding WrappedComponent Pros and Cons Pros ✅ Cons ❌ Promotes code reuse Can lead to "wrapper hell" (too many nested HOCs) Keeps components clean and focused on their main task Harder to debug due to multiple layers of abstraction Pros ✅ Cons ❌ Promotes code reuse Can lead to "wrapper hell" (too many nested HOCs) Keeps components clean and focused on their main task Harder to debug due to multiple layers of abstraction Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Promotes code reuse Can lead to "wrapper hell" (too many nested HOCs) Promotes code reuse Promotes code reuse Can lead to "wrapper hell" (too many nested HOCs) Can lead to "wrapper hell" (too many nested HOCs) Keeps components clean and focused on their main task Harder to debug due to multiple layers of abstraction Keeps components clean and focused on their main task Keeps components clean and focused on their main task Harder to debug due to multiple layers of abstraction Harder to debug due to multiple layers of abstraction Best For Cross-Cutting Concerns: Adding shared logic like authentication, logging, or theming. Reusable Enhancements: When multiple components need the same behavior. Complex Applications: Apps where common logic needs to be abstracted away for readability. Cross-Cutting Concerns : Adding shared logic like authentication, logging, or theming. Cross-Cutting Concerns Reusable Enhancements : When multiple components need the same behavior. Reusable Enhancements Complex Applications : Apps where common logic needs to be abstracted away for readability. Complex Applications Higher-Order Component (HOC) is an advanced React pattern Higher-Order Component (HOC) is an advanced React pattern advanced Code Example Here’s an example of a Higher-Order Component that adds a loading state to a component: // HOC - withLoading.js // it returns a functional component const withLoading = (WrappedComponent) => { return ({ isLoading, ...props }) => { if (isLoading) { return <div>Loading...</div>; } return <WrappedComponent {...props} />; }; }; export default withLoading; // HOC - withLoading.js // it returns a functional component const withLoading = (WrappedComponent) => { return ({ isLoading, ...props }) => { if (isLoading) { return <div>Loading...</div>; } return <WrappedComponent {...props} />; }; }; export default withLoading; // DataComponent.js const DataComponent = ({ data }) => { return <div>Data: {data}</div>; }; export default DataComponent; // DataComponent.js const DataComponent = ({ data }) => { return <div>Data: {data}</div>; }; export default DataComponent; // App.js import { useState, useEffect } from 'react'; import withLoading from './withLoading'; import DataComponent from './DataComponent'; // supercharching with HOC const DataComponentWithLoading = withLoading(DataComponent); const App = () => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { setTimeout(() => { setData('Here is the data!'); setLoading(false); }, 2000); }, []); return ( <div> <h1>My App</h1> <DataComponentWithLoading isLoading={loading} data={data} /> </div> ); }; export default App; // App.js import { useState, useEffect } from 'react'; import withLoading from './withLoading'; import DataComponent from './DataComponent'; // supercharching with HOC const DataComponentWithLoading = withLoading(DataComponent); const App = () => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { setTimeout(() => { setData('Here is the data!'); setLoading(false); }, 2000); }, []); return ( <div> <h1>My App</h1> <DataComponentWithLoading isLoading={loading} data={data} /> </div> ); }; export default App; Design Pattern #9: State Management with Reducers When the app’s state is more complex instead of using useState to manage your application's state, you can use reducers . state is more complex useState reducers Reducers allow you to handle state transitions in a more predictable and organized way . more predictable and organized way A reducer is simply a function that takes the current state and an action, then returns the new state. A reducer is simply a function that takes the current state and an action, then returns the new state. function that takes the current state and an action, then returns the new state. Basics Reducer Function : A pure function that takes state and action as arguments and returns a new state. Reducer Function state action const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } }; const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; default: return state; } }; Action : An object that describes what kind of state update should happen. Actions usually have a type field and may include additional data (payload). Action type Dispatch : A function used to send actions to the reducer, triggering a state update. Dispatch Purpose This pattern is useful when the state logic becomes too complex for useState . It centralizes state updates, making your code easier to manage, debug, and scale. useState Tips Keep Reducers Pure: Ensure your reducer function has no side effects (no API calls or asynchronous code). Use Constants for Action Types: To avoid typos, define action types as constants. Keep Reducers Pure : Ensure your reducer function has no side effects (no API calls or asynchronous code). Keep Reducers Pure Use Constants for Action Types : To avoid typos, define action types as constants. Use Constants for Action Types Pros and Cons Pros ✅ Cons ❌ Simplifies complex state logic Adds boilerplate code (actions, dispatch, etc.) Centralizes state updates for easier debugging Can be overkill for simple state management Makes state transitions predictable Requires learning curve for beginners Pros ✅ Cons ❌ Simplifies complex state logic Adds boilerplate code (actions, dispatch, etc.) Centralizes state updates for easier debugging Can be overkill for simple state management Makes state transitions predictable Requires learning curve for beginners Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Simplifies complex state logic Adds boilerplate code (actions, dispatch, etc.) Simplifies complex state logic Simplifies complex state logic Adds boilerplate code (actions, dispatch, etc.) Adds boilerplate code (actions, dispatch, etc.) Centralizes state updates for easier debugging Can be overkill for simple state management Centralizes state updates for easier debugging Centralizes state updates for easier debugging Can be overkill for simple state management Can be overkill for simple state management Makes state transitions predictable Requires learning curve for beginners Makes state transitions predictable Makes state transitions predictable Requires learning curve for beginners Requires learning curve for beginners Best For Complex State Logic: When state transitions depend on multiple conditions. Medium to Large Applications: Apps with multiple components sharing state. Complex State Logic : When state transitions depend on multiple conditions. Complex State Logic Medium to Large Applications : Apps with multiple components sharing state. Medium to Large Applications Code Example Here’s an example of state management with useReducer in a counter app: useReducer import { useReducer } from 'react'; // Step 1: Define the reducer function const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; case 'RESET': return { count: 0 }; default: return state; } }; // Step 2: Define the initial state const initialState = { count: 0 }; // Step 3: Create the component const Counter = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <h1>Count: {state.count}</h1> <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button> <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button> <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button> </div> ); }; export default Counter; import { useReducer } from 'react'; // Step 1: Define the reducer function const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { count: state.count + 1 }; case 'DECREMENT': return { count: state.count - 1 }; case 'RESET': return { count: 0 }; default: return state; } }; // Step 2: Define the initial state const initialState = { count: 0 }; // Step 3: Create the component const Counter = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <h1>Count: {state.count}</h1> <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button> <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button> <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button> </div> ); }; export default Counter; In modern React development, Redux is the library that uses reducers for state management. In modern React development, Redux is the library that uses reducers for state management. Redux Design Pattern #10: Data management with Providers (Context API) The provider pattern is very useful for data management as it utilizes the context API to pass data through the application's component tree. This pattern is an effective solution to prop drilling , which has been a common concern in react development. utilizes the context API solution to prop drilling Context API is the solution to prop drilling Context API is the solution to prop drilling solution to prop drilling Providers allow you to manage global state in a React application, making it accessible to any component that needs it. This pattern helps avoid prop drilling (passing props through many layers) by offering a way to "provide" data to a component tree. prop drilling Basics Context: A React feature that allows you to create and share state globally. Provider: A component that supplies data to any component in its child tree. Consumer: A component that uses the data provided by the Provider. useContext Hook: A way to access context values without needing a Consumer. Context : A React feature that allows you to create and share state globally. Context Provider : A component that supplies data to any component in its child tree. Provider Consumer : A component that uses the data provided by the Provider . Consumer Provider useContext Hook: A way to access context values without needing a Consumer . useContext Consumer Purpose The purpose of this pattern is to simplify data sharing between deeply nested components by creating a global state accessible via a Provider . It helps keep code clean, readable, and free of unnecessary prop passing. Provider Tips Use Context Wisely: Context is best for global state like themes, authentication, or user settings. Combine with Reducers: For complex state logic, combine Context with useReducer for more control. Split Contexts: Instead of one giant context, use multiple smaller contexts for different types of data. Use Context Wisely : Context is best for global state like themes, authentication, or user settings. Use Context Wisely Combine with Reducers : For complex state logic, combine Context with useReducer for more control. Combine with Reducers useReducer Split Contexts : Instead of one giant context, use multiple smaller contexts for different types of data. Split Contexts Pros and Cons Pros ✅ Cons ❌ Reduces prop drilling Not ideal for frequently changing data (can cause unnecessary re-renders) Centralizes data for easier access Performance issues if context value changes often Simple to set up for small to medium-sized apps Pros ✅ Cons ❌ Reduces prop drilling Not ideal for frequently changing data (can cause unnecessary re-renders) Centralizes data for easier access Performance issues if context value changes often Simple to set up for small to medium-sized apps Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Reduces prop drilling Not ideal for frequently changing data (can cause unnecessary re-renders) Reduces prop drilling Reduces prop drilling Not ideal for frequently changing data (can cause unnecessary re-renders) Not ideal for frequently changing data (can cause unnecessary re-renders) Centralizes data for easier access Performance issues if context value changes often Centralizes data for easier access Centralizes data for easier access Performance issues if context value changes often Performance issues if context value changes often Simple to set up for small to medium-sized apps Simple to set up for small to medium-sized apps Simple to set up for small to medium-sized apps Best For Global State: Sharing themes, authentication status, language settings, etc. Avoiding Prop Drilling: When data needs to be passed through multiple component layers. Medium Complexity Apps: Apps that need simple global state management. Global State : Sharing themes, authentication status, language settings, etc. Global State Avoiding Prop Drilling : When data needs to be passed through multiple component layers. Avoiding Prop Drilling Medium Complexity Apps : Apps that need simple global state management. Medium Complexity Apps Code Example Here’s an example of data management with a ThemeProvider : ThemeProvider // ThemeContext.jsx import { createContext, useState } from 'react'; export const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); }; // ThemeContext.jsx import { createContext, useState } from 'react'; export const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); }; // ThemeToggleButton.jsx import { useContext } from 'react'; import { ThemeContext } from './ThemeContext'; const ThemeToggleButton = () => { const { theme, toggleTheme } = useContext(ThemeContext); return ( <button onClick={toggleTheme}> Switch to {theme === 'light' ? 'dark' : 'light'} mode </button> ); }; export default ThemeToggleButton; // ThemeToggleButton.jsx import { useContext } from 'react'; import { ThemeContext } from './ThemeContext'; const ThemeToggleButton = () => { const { theme, toggleTheme } = useContext(ThemeContext); return ( <button onClick={toggleTheme}> Switch to {theme === 'light' ? 'dark' : 'light'} mode </button> ); }; export default ThemeToggleButton; // App.js import { ThemeProvider } from './ThemeContext'; import ThemeToggleButton from './ThemeToggleButton'; const App = () => { return ( <ThemeProvider> <div> <h1>Welcome to the App</h1> <ThemeToggleButton /> </div> </ThemeProvider> ); }; export default App; // App.js import { ThemeProvider } from './ThemeContext'; import ThemeToggleButton from './ThemeToggleButton'; const App = () => { return ( <ThemeProvider> <div> <h1>Welcome to the App</h1> <ThemeToggleButton /> </div> </ThemeProvider> ); }; export default App; In React 19, you can render <Context> as a provider instead of <Context.Provider> In React 19, you can render <Context> as a provider instead of <Context.Provider> const ThemeContext = createContext(''); function App({children}) { return ( <ThemeContext value="dark"> {children} </ThemeContext> ); } const ThemeContext = createContext(''); function App({children}) { return ( <ThemeContext value="dark"> {children} </ThemeContext> ); } Design Pattern #11: Portals Portals allow you to render children into a different part of the DOM tree that exists outside the parent component's hierarchy . render children outside the parent component's hierarchy This is useful for rendering elements like modals, tooltips, or overlays that need to be displayed outside the normal DOM flow of the component. modals, tooltips, or overlays Even though the DOM parent changes, the React component structure stays the same. Even though the DOM parent changes, the React component structure stays the same . React component structure stays the same Purpose The purpose of this pattern is to provide a way to render components outside the parent component hierarchy, making it easy to manage certain UI elements that need to break out of the flow, without disrupting the structure of the main React tree. Tips Modals and Overlays: Portals are perfect for rendering modals, tooltips, and other UI elements that need to appear on top of other content. Avoid Overuse: While useful, portals should be used only when necessary, as they can complicate the component hierarchy and event propagation. Modals and Overlays : Portals are perfect for rendering modals, tooltips, and other UI elements that need to appear on top of other content. Modals and Overlays Avoid Overuse : While useful, portals should be used only when necessary, as they can complicate the component hierarchy and event propagation. Avoid Overuse Pros and Cons Pros ✅ Cons ❌ Keeps the component tree clean and avoids layout issues Can complicate event propagation (e.g., click events may not bubble) Pros ✅ Cons ❌ Keeps the component tree clean and avoids layout issues Can complicate event propagation (e.g., click events may not bubble) Pros ✅ Cons ❌ Pros ✅ Pros ✅ Pros Cons ❌ Cons ❌ Cons Keeps the component tree clean and avoids layout issues Can complicate event propagation (e.g., click events may not bubble) Keeps the component tree clean and avoids layout issues Keeps the component tree clean and avoids layout issues Can complicate event propagation (e.g., click events may not bubble) Can complicate event propagation (e.g., click events may not bubble) Best For Overlays: Modals, tooltips, or any UI element that needs to appear on top of other content. Breaking DOM Flow: When elements need to break out of the standard component hierarchy (e.g., notifications). Overlays : Modals, tooltips, or any UI element that needs to appear on top of other content. Overlays Breaking DOM Flow : When elements need to break out of the standard component hierarchy (e.g., notifications). Breaking DOM Flow Code Example // Modal.jsx import { useEffect } from 'react'; import ReactDOM from 'react-dom'; const Modal = ({ isOpen, closeModal, children }) => { // Prevent body scrolling when modal is open useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden'; } return () => { document.body.style.overflow = 'unset'; }; }, [isOpen]); if (!isOpen) return null; return ReactDOM.createPortal( <> {/* Overlay */} <div style={overlayStyles} onClick={closeModal} /> {/* Modal */} <div style={modalStyles}> {children} <button onClick={closeModal}>Close</button> </div> </>, document.getElementById('modal-root') ); }; const overlayStyles = { ... }; const modalStyles = { ... }; export default Modal; // Modal.jsx import { useEffect } from 'react'; import ReactDOM from 'react-dom'; const Modal = ({ isOpen, closeModal, children }) => { // Prevent body scrolling when modal is open useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden'; } return () => { document.body.style.overflow = 'unset'; }; }, [isOpen]); if (!isOpen) return null; return ReactDOM.createPortal( <> {/* Overlay */} <div style={overlayStyles} onClick={closeModal} /> {/* Modal */} <div style={modalStyles}> {children} <button onClick={closeModal}>Close</button> </div> </>, document.getElementById('modal-root') ); }; const overlayStyles = { ... }; const modalStyles = { ... }; export default Modal; // App.js import { useState } from 'react'; import Modal from './Modal'; const App = () => { const [isModalOpen, setIsModalOpen] = useState(false); return ( <div> <h1>React Portals Example</h1> <button onClick={() => setIsModalOpen(true)}>Open Modal</button> <Modal isOpen={isModalOpen} closeModal={() => setIsModalOpen(false)}> <h2>Modal Content</h2> <p>This is the modal content</p> </Modal> </div> ); }; export default App; // App.js import { useState } from 'react'; import Modal from './Modal'; const App = () => { const [isModalOpen, setIsModalOpen] = useState(false); return ( <div> <h1>React Portals Example</h1> <button onClick={() => setIsModalOpen(true)}>Open Modal</button> <Modal isOpen={isModalOpen} closeModal={() => setIsModalOpen(false)}> <h2>Modal Content</h2> <p>This is the modal content</p> </Modal> </div> ); }; export default App; // index.html <body> <div id="root"></div> <div id="modal-root"></div> </body> // index.html <body> <div id="root"></div> <div id="modal-root"></div> </body> What’s in the end? Learning and mastering design patterns is a crucial step toward becoming a senior web developer . 🆙 crucial step toward becoming a senior web developer These patterns are not just theoretical; they address real-world challenges like state management, performance optimization, and UI component architecture. By adopting them in your everyday work, you'll be equipped to solve a variety of development challenges and create applications that are both performant and easy to maintain. Liked the article? 😊 You can learn more at my personal Javascript blog ➡️ https://jssecrets.com/ . https://jssecrets.com/ Or you can see my projects and read case studies at my personal website ➡️ https://ilyasseisov.com/ . https://ilyasseisov.com/ Happy coding! 😊