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:
Easier Reuse: Compound components make it simpler to reuse sets of related components together in different parts of your application.
Clearer Code: This pattern helps make your code easier to understand because it groups components that work together closely.
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.
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:
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.
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.
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 React, {
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)
const TabsProvider: React.FC<TabsProviderProps> = ({ children }) => {
const [currentIndex, setCurrentIndex] = useState<number>(0)
return (
<TabsContext.Provider value={{ currentIndex, setCurrentIndex }}>
{children}
</TabsContext.Provider>
)
}
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.
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: 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">
{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
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>
<Tabs.Titles items={tabData.map(({ id, title }) => ({ id, title }))} />
<Tabs.Contents
items={tabData.map(({ id, content }) => ({
id,
content: <p>{content}</p>,
}))}
/>
</Tabs>
)
}
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.
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