In React.js, there are several design patterns commonly used to structure and organize code. Here are some popular design patterns you can utilize in React.js applications:
- Container-Component Patter: This pattern separates the concerns of data fetching and state management from the presentation logic. Container component handle data fetching and state management, while presentational components focus on rendering the UI based on the received props.
import React, { useState, useEffect } from 'react';
// Container component
const UserContainer = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
// Simulate fetching users from an API
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => setUsers(data))
.catch(error => console.log(error));
}, []);
return (
<div>
<h1>User List</h1>
<UserList users={users} />
</div>
);
};
// Presentational component
const UserList = ({ users }) => {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
export default UserContainer;
In this example, we have a container component “UserContainer” responsible for fetching user data from an API using the “useState” and “useEffect” hooks. The fetched users are stored in the “users” state variable.
Inside the “useEffect” hook, we simulate an API call to fetch the users. Once the response is received, the “setUsers” function is called to update the “users” state.
The “UserContainer” component renders a “UserList” component, which is the presentational component responsible for rendering the list of users received as porps. It iterates over the “users” array using “map” and renders a list item for each user.
By separating the data fetching and state management logic into the container component “UserContainer” and the presentation logic into the presentational component “UserList”, we achieve a clear separation of concerns and improve code maintainability and reusability.
- Render Props Pattern: This pattern involves passing a function as a prop to a component, allowing the component to access and render the result of that function. It providers a flexible way to share code and behavior between components.
import React from 'react';
// Render Props component
const MouseTracker = ({ render }) => {
const handleMouseMove = (event) => {
const { clientX, clientY } = event;
render(clientX, clientY);
};
return <div onMouseMove={handleMouseMove}>Move the mouse!</div>;
};
// Usage
const App = () => {
return (
<div>
<h1>Render Props Example</h1>
<MouseTracker
render={(x, y) => (
<p>
Mouse position: {x}, {y}
</p>
)}
/>
</div>
);
};
export default App;
In this example, we have a component called “MouseTracker” that accepts a “render” prop. Inside the “MouseTracker” component, the “handleMouseMove” function is called whenever the mouse moves. It extracts the “clientX” and “clientY” coordinates from the event and passes them as arguments to the “render” function.
In the “App” component, we render the “MouseTracker” component and provide the “render” prop with a function. This function receive the “x” and “y” coordinates from the “MouseTracker” component and returns the JSX that we want to render based on those coordinates.
By utilizing the Render Props Pattern, we can pass in a function as prop to a component, allowing the component to invoke that function and provide it with relevant data. This pattern enables us to share code and behavior between components in a flexible and reusable manner.
- Higher-Order Component Pattern: HOCs are functions that take a component and return a new enhanced component with additional props or behavior. They enable code reuse and provide a way to modify component behavior without changing the original component’s implementation
import React, { Component } from 'react';
// Higher Order Component
const withLogging = (WrappedComponent) => {
class WithLogging extends Component {
componentDidMount() {
console.log(`Component ${WrappedComponent.name} mounted.`);
}
componentWillUnmount() {
console.log(`Component ${WrappedComponent.name} will unmount.`);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return WithLogging;
};
// Usage
const MyComponent = () => {
return <div>My Component</div>;
};
const MyComponentWithLogging = withLogging(MyComponent);
export default MyComponentWithLogging;
In this example, we have a higher order component called “withLogging” that takes a “WrappedComponent” as input. Inside “withLogging”, a new component called “withLogging” is defined. This component extends “component” and adds additional logging functionality in the “componentDidMount” and “componentWillUnmount” lifecycle methods.
The “componentDidMount” method logs a message when the wrapped component is mounted, and the “componentWillUnmount” method logs a message when the wrapped component is about to unmount.
The “render” method of the “WithLogging” component renders the “WrappedComponent” and passes along any props received by “WithLogging”.
To use the higher order component, we defined our original component “MyComponent”, and then we create a new component “MyComponentWithLogging” by invoking “withLogging” function and passing “MyComponent” as an argument.
Now, when “MyComponentWithLogging” is rendered, the logging functionality from the higher order component will be applied to “MyComponent”. This allows us to add additional behavior or functionality to components without modifying their original implementation.
- Compound Components Pattern: This pattern involves creating a set of components that work together to achieve a specific functionality. The components share state and behavior while providing a more intuitive and reusable API for the user
import React, { useState } from 'react';
// Parent Component
const Accordion = ({ children }) => {
const [activeIndex, setActiveIndex] = useState(0);
const handleItemClick = (index) => {
setActiveIndex(index);
};
return (
<div>
{React.Children.map(children, (child, index) => {
if (child.type === AccordionItem) {
return React.cloneElement(child, {
active: index === activeIndex,
onItemClick: () => handleItemClick(index),
});
}
return child;
})}
</div>
);
};
// Child Component
const AccordionItem = ({ active, label, children, onItemClick }) => {
return (
<div>
<button onClick={onItemClick}>{label}</button>
{active && <div>{children}</div>}
</div>
);
};
// Usage
const App = () => {
return (
<Accordion>
<AccordionItem label="Section 1">
<p>Content for Section 1</p>
</AccordionItem>
<AccordionItem label="Section 2">
<p>Content for Section 2</p>
</AccordionItem>
<AccordionItem label="Section 3">
<p>Content for Section 3</p>
</AccordionItem>
</Accordion>
);
};
export default App;
In this example, we have a parent component called “Accordion” and a child component called “AccordionItem”. The “Accordion” component is responsible for managing the state of the active item and rendering the “AccordionItem” components.
Inside the “Accordion” component, we use “React.Children.map” to iterate over the “children” prop. We check if the child is of type “AccordionItem” and clone it using “React.cloneElement”. We pass additional props to each “AccordionItem”, including “active” (based on the active index) and “onItemClick” (which updates the active index).
The “AccordionItem” component renders a button for the label and conditionally renders the content based on the “active” prop.
In the usage example, we render the “Accordion” component and define multiple “AccordionItem” components as its children. Each “AccordionItem” represents a section with a label and content.
By using the Compound Component pattern, we can encapsulate the logic and state management in the parent “Accordion” component while allowing flexible rendering and customization of the child “AccordionItem” components.
- Provider Pattern: The provider pattern is used to propagate data or functionality to nested components without the need for passing props manually. It involves creating a provider component that wraps the consuming components and provides a context that can be accessed by the nested components.
import React, { createContext, useState } from 'react';
// Create the context
const ThemeContext = createContext();
// Provider component
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Child component that consumes the context
const ThemedButton = () => {
return (
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => (
<button
onClick={toggleTheme}
style={{ backgroundColor: theme === 'light' ? '#ffffff' : '#000000', color: theme === 'light' ? '#000000' : '#ffffff' }}
>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
);
};
// Usage
const App = () => {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
};
export default App;
In this example, we create a context using the “createContext” function. We define a “ThemeProvider” component that servers as the provider of the theme value and toggle function. Inside the provider component, we use the “useState” hook to manage the theme state. The “toggleTheme” function is responsible for toggling between light and dark themes.
We wrap the “children” prop with the “ThemeContext.Provider” component, providing the theme and toggle Theme values as the context value.
The “ThemeButton” component is a child component that consumes the context using the “ThemeContext.Consumer” component. Inside the consumer, we render a button and access the theme and toggleTheme values from the context. The button’s appearance is determined by the current theme value.
In the usage example, we render the “ThemeProvider” component as the top-level provider, and the “ThemeButton” component is a child that consumes the theme context.
By using the Provider pattern with the Context API, we can provide data and functions to multiple levels of nested components without passing props explicitly. This allows components to access and update shared state or values easily
- Singleton Pattern: The Singleton pattern is used when you want to ensure that only a single instance of a component or service is created and shared across the application. It can be useful for managing shared state or resources.
// singleton.js
class Singleton {
constructor() {
// Initialize the singleton instance
this.data = [];
}
addData(item) {
this.data.push(item);
}
getData() {
return this.data;
}
}
// Create and export a singleton instance
export default new Singleton();
In this example, we define a “Singleton” class that represents our singleton object. The class has a constructor to initialize any necessary state or properties. We also define some methods (“addData” and “getData”) to manipulate and access the data within the singleton.
By creating a new instance of the “Singleton” class using the “new” keyword and exporting it, we ensure that the same instance is shared across the entire application.
Now, in any other component or module where you want to use the singleton, you can import and access it:
// SomeComponent.js
import singleton from './singleton';
// Use the singleton instance
singleton.addData('Hello');
console.log(singleton.getData()); // Output: ['Hello']
By importing the “singleton” instance from the “singleton.js” file, you can directly access its methods and properties.
Keep in mind that while the Singleton pattern can be useful in certain scenarios, it is important to use it judiciously and consider other alternatives like component composition or state management libraries such as Redux or MobX, depending on the complexity of your application.