Mastering React Design Patterns: Creating a Tabs Component
Tags: react, tutorial, typescript, webdev
Published: 2023-09-18 16:25:45

In the world of web development, **React** is a popular choice for creating interactive and efficient user interfaces. 

One of the key strengths of **React** lies in its ability to create reusable components, which can significantly enhance development productivity and code maintainability. In this pursuit, the adoption of design patterns holds significant importance. Design patterns, offer structured solutions to address common challenges in React application development.

In this article, we will explore two powerful React design patterns: the **Compound Components Pattern** and the **React Provider Pattern**. 

We will learn how each of these patterns helps make React components clearer and easier to work with, which will improve your ability to design and build successful React apps.

## Compound Components Pattern

The Compound Components Pattern is a way to design and structure components in React. In this pattern, you have a single parent component that manages multiple child components. These child components work together to create a more complex user interface. Each child component has a specific role, and they rely on each other to function correctly.

**Advantages of the Compound Components Pattern:**

1. **Easier Reuse:** Compound components make it simpler to reuse sets of related components together in different parts of your application.

2. **Clearer Code:** This pattern helps make your code easier to understand because it groups components that work together closely.

3. **Simpler Usage:** Users of your components can interact with them more easily by arranging child components within a parent component, which simplifies how components are used.

4. **Flexible and Expandable:** You can add new child components or modify existing ones without changing the overall structure of the compound component. This is helpful as your application grows.

### React Provider Pattern

The **Provider Pattern** is a design pattern commonly used in React to manage and share state or data across different parts of an application. It involves creating a provider component that holds the shared state and rendering it as a parent to components that need access to this state. In addition to the provider component, the pattern often includes a custom hook that allows child components to conveniently access and interact with the provided state.

**Advantages of the Provider Pattern:**

The Provider Pattern in React offers several advantages:

1.  **Efficient State Management:** The Provider Pattern simplifies the management of shared state or data across different parts of an application. It ensures that components have easy access to this state without the need for complex prop drilling.
    
2.  **Enhanced Reusability:** By centralizing state management within a provider component, you can reuse this state in multiple places throughout your application. This promotes code reusability and reduces redundancy.
    
3.  **Clear Separation of Concerns:** The pattern encourages a clear separation of concerns. State management logic resides within the provider component, while other components can focus on rendering and user interactions.

### Tabs Component

By merging these pattern, we'll develop a "Tabs" component that's versatile and easy to use.

**TabsContext.tsx**

```tsx
import {
  Dispatch,
  ReactNode,
  SetStateAction,
  createContext,
  useContext,
  useState,
} from 'react'

type TabsContextProps = {
  currentIndex: number
  setCurrentIndex: Dispatch<SetStateAction<number>>
}

type TabsProviderProps = {
  children: ReactNode
}

const initialContext: TabsContextProps = {
  currentIndex: 0,
  setCurrentIndex: () => {},
}

const TabsContext = createContext<TabsContextProps>(initialContext)

export default function TabsProvider({ children }: TabsProviderProps) {
  const [currentIndex, setCurrentIndex] = useState<number>(0)

  return (
    <TabsContext.Provider value={{ currentIndex, setCurrentIndex }}>
      {children}
    </TabsContext.Provider>
  )
}

export function useTabsContext(): TabsContextProps {
  const context = useContext(TabsContext)
  if (context === undefined) {
    throw new Error('useTabs must be used within a TabsProvider')
  }
  return context
}
```

In `TabsContext.tsx` we define the **context** and **provider** components, which are fundamental to the **React Provider Pattern**. The **context** holds the shared state (in this case, the current tab index), and the **provider** ensures its availability to child components. The custom hook `useTabsContext` facilitates easy access to this shared state.

**Tabs.tsx**

```tsx
import React from 'react'

import TabsProvider, { useTabsContext } from './TabsContext'

type TabTitlesProps = {
  items: {
    id: string
    title: string
  }[]
}

type TabContentProps = {
  items: {
    id: string
    content: React.ReactNode
  }[]
}

type TabsComposition = {
  Titles: (props: TabTitlesProps) => React.ReactNode
  Contents: (props: TabContentProps) => React.ReactNode
}

type TabsProps = {
  children: React.ReactNode
}

type TabsWrapper = (props: TabsProps) => React.ReactNode

const Tabs: TabsWrapper & TabsComposition = ({ children }) => {
  return <TabsProvider>{children}</TabsProvider>
}

Tabs.Titles = ({ items }) => {
  const { currentIndex, setCurrentIndex } = useTabsContext()
  return (
    <div role="tablist">
      {items.map(({ id, title }, index) => (
        <button
          key={id}
          id={`tab-control-${id}`}
          role="tab"
          aria-controls={`tab-content-${id}`}
          aria-selected={currentIndex === index}
          onClick={() => {
            setCurrentIndex(index)
          }}
        >
          {title}
        </button>
      ))}
    </div>
  )
}

Tabs.Contents = ({ items }) => {
  const { currentIndex } = useTabsContext()
  const { id, content } = items[currentIndex]
  return (
    <div
      key={id}
      id={`tab-content-${id}`}
      role="tabpanel"
      aria-labelledby={`tab-control-${id}`}
    >
      {content}
    </div>
  )
}

export default Tabs
```

In `Tabs.tsx`, we build a **tabbed interface** using the **Compound Components Pattern**. Inside `Tabs`, we define two child components: `Tabs.Titles` and `Tabs.Contents`. `Tabs.Titles` generates buttons for **tab titles**, and `Tabs.Contents` displays the **content** of the selected tab. The magic happens when these child components work together within the `TabsProvider` context, sharing the current tab's **id**. So, when you click on a **tab title**, it seamlessly updates the displayed **content**, providing a user-friendly tabbed experience.

**App.tsx**

```tsx
import Tabs from './components/Tabs'

//tabData mock:
const tabData = [
  { id: 'tab1_unique_id', title: 'Tab 1', content: 'Content for Tab 1' },
  { id: 'tab2_unique_id', title: 'Tab 2', content: 'Content for Tab 2' },
  { id: 'tab3_unique_id', title: 'Tab 3', content: 'Content for Tab 3' },
]

export default function App() {
  return (
    <div className="tabs-wrapper">
      <Tabs>
        <Tabs.Titles items={tabData.map(({ id, title }) => ({ id, title }))} />
        <Tabs.Contents
          items={tabData.map(({ id, content }) => ({
            id,
            content: <p>{content}</p>,
          }))}
        />
      </Tabs>
    </div>
  )
}

```

In `App.tsx`, we begin defining mock data named `tabData` to represent tab titles and content. Inside the `App` component, we use the custom `Tabs` component. In `Tabs.Titles`, we pass an `items` prop that maps over `tabData` to generate tab titles with unique identifiers. Similarly, in `Tabs.Contents`, we pass an `items` prop that maps over `tabData` to create tab content elements, each wrapped in a `<p>` tag.

##Conclusion

In this guide, we've explored two important design patterns in React: the **Compound Components Pattern** and the **Provider Pattern**. We've learned how these patterns can make React components better and why choosing the right pattern depends on your project's needs.

It's important to remember that the **Compound Components Pattern** is great for creating complex and modular interfaces, while the **Provider Pattern** is valuable for efficient state management. We encourage you to try out these patterns in your future React projects. Mastering these techniques will not only improve your component design skills but also help you build stronger and more maintainable React applications.

**Github Repo**: [https://github.com/JBassx/react-patterns-tabs](https://github.com/JBassx/react-patterns-tabs)
**StackBlitz**: [https://stackblitz.com/edit/github-suyart](https://stackblitz.com/edit/github-suyart)

**LinkedIn**: [https://www.linkedin.com/in/josephciullo/](https://www.linkedin.com/in/josephciullo/)