Essential React Design Patterns for Scalable Apps

This article explores various reactjs design patterns, Compound Component pattern, Render Props, Higher-Order Components (HOCs), Custom Hooks, and Controlled Components. Each approach is discussed in detail, highlighting its benefits, drawbacks, and best use cases, helping you choose the right pattern for your React projects.

Compound Component Pattern

The Compound Component pattern in React is a powerful design technique that allows you to create more flexible and reusable UI components by breaking them down into smaller, composable parts. This pattern is particularly useful for creating components with complex behavior, where different parts need to work together seamlessly.

  1. Flexibility: Users can compose and arrange child components in any order, giving them more control over the UI structure.
  2. Reusability: Child components can be reused in different contexts within the parent component or even in other components.
  3. Encapsulation: The parent component encapsulates the shared state and behavior, keeping the child components simple and focused on their specific tasks.
  4. Readability: The pattern promotes a clean and declarative API, making the code easier to understand and maintain.

Example: Building a Compound Component

Letā€™s walk through an example of building a Tabs component using the Compound Component pattern.

1. Creating the Parent Component (Tabs)

The Tabs component will manage the state of which tab is currently active and provide the necessary context to the child components.

import React, { createContext, useContext, useState } from "react";

const TabsContext = createContext();

const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0);

  const value = {
    activeTab,
    setActiveTab,
  };

  return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>;
};

2. Creating Child Components (TabList, Tab, TabPanels, TabPanel)

Each child component will access the TabsContext to interact with the state managed by the Tabs component.

const TabList = ({ children }) => {
  return <div className="tab-list">{children}</div>;
};

const Tab = ({ children, index }) => {
  const { activeTab, setActiveTab } = useContext(TabsContext);

  return (
    <button
      className={`tab ${activeTab === index ? "active" : ""}`}
      onClick={() => setActiveTab(index)}
    >
      {children}
    </button>
  );
};

const TabPanels = ({ children }) => {
  return <div className="tab-panels">{children}</div>;
};

const TabPanel = ({ children, index }) => {
  const { activeTab } = useContext(TabsContext);

  return activeTab === index
    ? <div className="tab-panel">{children}</div>
    : null;
};

3. Using the Compound Component

Now you can compose the Tabs component with its child components to create the desired UI.

const App = () => {
  return (
    <Tabs>
      <TabList>
        <Tab index={0}>Tab 1</Tab>
        <Tab index={1}>Tab 2</Tab>
        <Tab index={2}>Tab 3</Tab>
      </TabList>
      <TabPanels>
        <TabPanel index={0}>Content for Tab 1</TabPanel>
        <TabPanel index={1}>Content for Tab 2</TabPanel>
        <TabPanel index={2}>Content for Tab 3</TabPanel>
      </TabPanels>
    </Tabs>
  );
};

Render Props

Render Props is a pattern where a component takes a function as a prop and uses that function to render content. This pattern provides a way to share logic between components while keeping the UI flexible.

const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0);

  return children({ activeTab, setActiveTab });
};

const App = () => (
  <Tabs>
    {({ activeTab, setActiveTab }) => (
      <>
        <div className="tab-list">
          <button onClick={() => setActiveTab(0)}>Tab 1</button>
          <button onClick={() => setActiveTab(1)}>Tab 2</button>
        </div>
        <div className="tab-panels">
          {activeTab === 0 && <div>Content for Tab 1</div>}
          {activeTab === 1 && <div>Content for Tab 2</div>}
        </div>
      </>
    )}
  </Tabs>
);

Pros:

Cons:

Higher-Order Components (HOCs)

Higher-Order Components are functions that take a component as an argument and return a new component. HOCs are used to add behavior or data to a component, making them a powerful tool for code reuse.

const withTabs = (Component) => {
  return (props) => {
    const [activeTab, setActiveTab] = useState(0);

    return (
      <Component {...props} activeTab={activeTab} setActiveTab={setActiveTab} />
    );
  };
};

const TabsComponent = ({ activeTab, setActiveTab }) => (
  <>
    <div className="tab-list">
      <button onClick={() => setActiveTab(0)}>Tab 1</button>
      <button onClick={() => setActiveTab(1)}>Tab 2</button>
    </div>
    <div className="tab-panels">
      {activeTab === 0 && <div>Content for Tab 1</div>}
      {activeTab === 1 && <div>Content for Tab 2</div>}
    </div>
  </>
);

const EnhancedTabs = withTabs(TabsComponent);

const App = () => <EnhancedTabs />;

Pros:

Cons:

Custom Hooks

Custom Hooks allow you to extract logic from components into reusable functions. This approach can achieve a similar effect as compound components by sharing state and behavior between components without the need for a hierarchical component structure.

const useTabs = () => {
  const [activeTab, setActiveTab] = useState(0);
  return { activeTab, setActiveTab };
};

const TabsComponent = () => {
  const { activeTab, setActiveTab } = useTabs();

  return (
    <>
      <div className="tab-list">
        <button onClick={() => setActiveTab(0)}>Tab 1</button>
        <button onClick={() => setActiveTab(1)}>Tab 2</button>
      </div>
      <div className="tab-panels">
        {activeTab === 0 && <div>Content for Tab 1</div>}
        {activeTab === 1 && <div>Content for Tab 2</div>}
      </div>
    </>
  );
};

const App = () => <TabsComponent />;

Pros:

Cons:

Controlled Components

Controlled Components follow a pattern where the parent component fully manages the state and behavior of its children. The children are stateless and rely on the parent for state updates.

const Tabs = ({ activeTab, onTabChange, children }) => {
  return React.Children.map(
    children,
    (child, index) =>
      React.cloneElement(child, { activeTab, onTabChange, index }),
  );
};

const Tab = ({ index, onTabChange, children }) => (
  <button onClick={() => onTabChange(index)}>{children}</button>
);

const TabPanel = ({ index, activeTab, children }) => (
  activeTab === index ? <div>{children}</div> : null
);

const App = () => {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <Tabs activeTab={activeTab} onTabChange={setActiveTab}>
      <Tab>Tab 1</Tab>
      <Tab>Tab 2</Tab>
      <TabPanel>Content for Tab 1</TabPanel>
      <TabPanel>Content for Tab 2</TabPanel>
    </Tabs>
  );
};

Pros:

Cons:

Conclusion

Each of these approaches offers different advantages and trade-offs, depending on your use case:

Choosing the right pattern depends on your specific needs, the complexity of your UI, and your teamā€™s familiarity with each approach.

Latest blog posts

Explore the world of programming and cybersecurity through our curated collection of blog posts. From cutting-edge coding trends to the latest cyber threats and defense strategies, we've got you covered.