Mastering React Design Patterns: Creating a Tabs Component

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.


import React, {
} 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)

const TabsProvider: React.FC<TabsProviderProps> = ({ children }) => {
  const [currentIndex, setCurrentIndex] = useState<number>(0)

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

export default TabsProvider

export const 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.


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: React.FC<TabTitlesProps>
  Contents: React.FC<TabContentProps>

type TabsProps = {
  children: React.ReactNode

const Tabs: React.FC<TabsProps> & TabsComposition = ({ children }) => {
  return <TabsProvider>{children}</TabsProvider>

Tabs.Titles = ({ items }) => {
  const { currentIndex, setCurrentIndex } = useTabsContext()
  return (
    <div role="tablist">
      {{ id, title }, index) => (
          aria-selected={currentIndex === index}
          onClick={() => {

Tabs.Contents = ({ items }) => {
  const { currentIndex } = useTabsContext()
  const { id, content } = items[currentIndex]
  return (

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.


import React from 'react'

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' },

const App: React.FC = () => {
  return (
      <Tabs.Titles items={{ id, title }) => ({ id, title }))} />
        items={{ id, content }) => ({
          content: <p>{content}</p>,

export default App

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.


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: StackBlitz: