paint-brush
Senior Web Developers Are Using These 11 Design Patterns to Write Squeaky Clean React Codeby@ilyasseisov
200 reads New Story

Senior Web Developers Are Using These 11 Design Patterns to Write Squeaky Clean React Code

by Ilyas SeisovDecember 25th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Mastering React design patterns is a key step toward becoming a senior web developer. These 11 essential patterns set a benchmark for code quality standards. Improve scalability, write cleaner code, and take your React skills to the next level. Read to elevate your development game! 🚀
featured image - Senior Web Developers Are Using These 11 Design Patterns to Write Squeaky Clean React Code
Ilyas Seisov HackerNoon profile picture

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:


  1. Make Your Code Easy to Read: Clear patterns mean other developers (or future you) can understand your code faster.
  2. Reduce Bugs: Structured code leads to fewer mistakes.
  3. Boost Efficiency: You don't have to solve the same problems over and over.
  4. Improve Collaboration: Teams can work more effectively with shared patterns.
  5. 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.

  1. React.lazy(): This function lets you dynamically import a component.
  2. 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 Convention
    HOCs 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! 😊