DEV Community

Cover image for Mastering React Design Patterns: Creating a Tabs Component
Giuseppe Ciullo
Giuseppe Ciullo

Posted on • Updated on

Mastering React Design Patterns: Creating a Tabs Component

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

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
}
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

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>
  )
}

Enter fullscreen mode Exit fullscreen mode

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
StackBlitz: https://stackblitz.com/edit/github-suyart

LinkedIn: https://www.linkedin.com/in/josephciullo/

Top comments (2)

Collapse
 
yosuvarv profile image
Yosuva

Explained very well๐Ÿ™Œ

Collapse
 
akashrchandran profile image
Akash R Chandran

I have to try this ๐Ÿ˜