Quick Summary:
React Design Patterns are core enablers of React library that enable React Developers to create complex React apps in an easily maintainable manner, which are also flexible, deliver better results, and drive better performance. Learn how to leverage design patterns in React to optimize your React.js project.
Apart from having an active developer community and unique features, a true testament of a good web app framework is the presence of design patterns for easing the development process for its developers.
And React checks all three checkboxes with ease. With over 38.4k forks and 187k stars (at the time of writing) on GitHub, and a community with millions of developers, React comes packed with promising features like Live Reloading, Hot Reloading, Reusable Components, and more. And to leverage this platform to its full extent with ease, developers can leverage React patterns.
Developers use Design Patterns in React to save development time and reduce coding efforts by applying pre-tested proven solutions to solve common React development problems.
React Design Patterns are used to simplify large React apps, helping reduce considerable stress from your React development team and let them work on separate components and share logic between them.
There are many benefits of using React component design patterns. They, not only speed up React development processes but also makes the code easier to maintain and read. Here are different ways design patterns help React developers in their React projects:
React is known for its flexible development environment as it doesn’t impose a set of rules or isn’t opinionated, unlike other popular web app frameworks. Though this opens up endless possibilities for React developers to mix and match different development approaches, it can also work as a limitation when multiple React developers collaborate on a project.
Design patterns provide the much-needed structure to your React app developers team or the developer’s team that you may have hired from react js development company. It provides standard terminology and solutions to common problems.
This way, we can ensure all developers know what method to follow to solve a common problem, avoid conflicts or errors in code, and optimize the entire process.
Frameworks only release design Patterns after extensive research and thorough testing. The same is true for React. Hence, design patterns for React not only help ease the development process but also ensure that React developers are following React development best practices.
Hence design patterns are a powerful tool at the disposable of React developers enabling them to create unique, robust, and scalable apps.
Since React is such an open platform with many possible solutions to one problem, React has different components that may share common functionality. This is referred to as cross-cutting concerns. Cross-cutting concerns are used across multiple React app modules.
They represent functionalities for secondary requirements such as security, logging, data transfer, and other concerns essential to almost every React component. This is why they are also known as system-wide concerns.
There are many design patterns for React, and they all help solve many React development problems and challenges. However, it is impossible to cover each of them, and it can be overwhelming to bifurcate the most efficient ones from the rest.
Hence, we worked hard for you and curated a list of Advance React Design Patterns you should know and use in 2022. Without further ado, let’s dive right into it:
If you are an experienced React developer, chances are you would have realized the need to utilize the same logic in various situations such as:
You can solve all these React cross-cutting concerns with HOC! Higher-Order Components is a popular React design pattern used for logic between various components without needing to rewrite the code manually. A HOC is considered a pure function as it has zero side effects.
HOC is a JavaScript function that takes a component as an argument and returns another component after injecting or adding additional data and functionality to the returned component. Higher-Order Components’ fundamental concept is based upon React’s nature, which prefers composition over inheritance. Many popular React frameworks use this advanced React pattern like connect from Redux.
If you are using React with Redux for your project and you pass your component through connect function, the passed component will get injected with some data from the Redux store, and these values will be passed as Props. You can use the same function ‘connect’ for passing any component to get the same power-up which is access to the Redux store.
import React, { Component } from "react";
const higherOrderComponent = (DecoratedComponent) => {
class HOC extends Component {
render() {
return <DecoratedComponent />;
}
}
return HOC;
};
The Provider pattern is next on the list of advanced React design patterns used in React. Its purpose is to share global data across various components of the React component tree.
React’s Provider Pattern comes with a Provider component that holds the global data, which it can pass down the component tree in the app by using its Consumer component/custom Hook.
Again, this pattern can be used with other React integrations such as React-Redux and MobX. To understand how React Provider Design Pattern works, we first need to understand React’s Context API.
Prop Drilling is one of the most common React development concerns React developers face. Prop drilling is when the React app data(props) are passed down to two different components until it reaches the component where the prop is required.
It becomes a concern when unrelated components share the data through which the props need to be passed down to the actual component that needs the prop. The Provider design pattern Reactjs is a solution to this React concern.
It helps developers store the data in a centralized location known as React Context object that uses React Context API and the Redux store. The Context Provider or the Redux Store can pass this data directly to any component that needs it without undergoing props drilling.
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import store from './store'
import App from './App.'
const rootElement = document.getElementById('root')
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
)
The entire idea behind this React design pattern was to find an easier way to reuse your React app components. Presentational and Container Component Pattern works on the belief that it is easier to manage components if they are divided into two buckets – Presentational and Container Components.
So, what are the differences between Presentational and Container Component Patterns in React?
Presentational Component
const usersList = ({users}) = {
return (
ul
{users.map((user) = (
li key={user.id}
{user.username}
li
))}
ul
);
};
Container Component
class Users extends React.Component {
state = {
users: []
};
componentDidMount() {
this.fetchUsers();
}
render() {
return (); // ... jsx code with presentation component
}
}
If you are going to use the Presentational and Container Component design pattern, we’d suggest for you first start building your app with just the presentational components. Doing so will help you assess if you are passing down too many props to the intermediate components that don’t have any use for these props but still need to forward them to the components below them.
When this issue arises, it will as the child component needs access to that data passed as props time, and again, you can introduce container components. Doing so will help you separate the data and the behavior props to the leaf components without putting a lot of stress on the components that have no use in this data in the middle of the tree.
Hooks got introduced in React 16.8 version, and they have brought a revolution in how developers approach building React components. Using the React Hook API enables React functional components to get access to common React features like context, state, props, lifecycle, and refs.
This increases the potential of functional components as they can now perform all tasks that originally were only supported by class components.
Now the purpose React hooks serves is similar to Presentational and Container component design patterns, to enable separation of concerns. However, presentational and container pattern generally creates giant components with a massive logic split across different lifecycle methods.
Such components can be difficult to maintain and read. Containers are also classes; hence they cannot be easily composed. Hence when we put functional components on steroids through React hooks, they are able to perform all functions of containers without their limitations.
Since they are pure JavaScript functions, functional components in React are easily composable. Using containers also has other class-related problems like auto binding and using this function.
Using the container method is generally not optimal because…
import React, { Component } from "react";
class Profile extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
user: {},
};
}
componentDidMount() {
this.subscribeToOnlineStatus(this.props.id);
this.updateProfile(this.props.id);
}
componentDidUpdate(prevProps) {
// compariation hell.
if (prevProps.id !== this.props.id) {
this.updateProfile(this.props.id);
}
}
componentWillUnmount() {
this.unSubscribeToOnlineStatus(this.props.id);
}
subscribeToOnlineStatus() {
// subscribe logic
}
unSubscribeToOnlineStatus() {
// unscubscribe logic
}
fetchUser(id) {
// fetch users logic here
}
async updateProfile(id) {
this.setState({ loading: true });
// fetch users data
await this.fetchUser(id);
this.setState({ loading: false });
}
render() {
// ... some jsx
}
}
export default Profile;
setState()
to call props, and it only changes the first level of any state object.onHandleChange()
adds up many lines of code
And in comparison, using React hooks design pattern:
import React, { useState, useEffect } from "react";
function Profile({ id }) {
const [loading, setLoading] = useState(false);
const [user, setUser] = useState({});
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
updateProfile(id);
subscribeToOnlineStatus(id);
return () => {
unSubscribeToOnlineStatus(id);
};
}, [id]);
const subscribeToOnlineStatus = () => {
// subscribe logic
};
const unSubscribeToOnlineStatus = () => {
// unsubscribe logic
};
const fetchUser = (id) => {
// fetch user logic here
};
const updateProfile = async (id) => {
setLoading(true);
// fetch user data
await fetchUser(id);
setLoading(false);
};
return; // ... jsx logic
}
export default Profile;
useEffect
replaces lifecycle methods like componentDidMount
and componentWillInmount
, helping developers write and maintain cleaner and concise code.
Although popularly, React hooks seem like they have replaced container components; if your app is relatively smaller or medium-sized, or if you don’t want to adapt to React hooks, you can still make use of container component design pattern for your React app.
One of the common React patterns is React compound component pattern that helps establish seamless communication between a parent and children’s components via a flexible and expressive API.
Using this React design pattern, the parent component can interact and share state with the children components implicitly. It provides an effective method for various components to share states and handle logic. Compound components are best suitable for React apps where you need to build declarative UI.
The compound component in React is a pattern that encloses the state and behavior of a group of components while giving the rendering controls of its variables to the external user.
For instance, if we consider <select> and <option> tags in HTML to create a drop-down menu:
<select>
<option value="USA">USA</option>
<option value="UK">UK</option>
<option value="INDIA">INDIA</option>
</select>;
Here, the <select> tag is being used with the <option> tag for creating a drop-down menu for selecting items from a list in HTML. If we observe, here, the <select> tag is handling the state of the UI, and the <option> tags are set as per how the <select> element should work.
Next, we should look at an example where we build a custom React select component with a list of options where the user can select one option at a time –
const Option = ({ children, onClick, active }) => {
return (
<div
style={
active ? { backgroundColor: "grey" } : { backgroundColor: "white" }
}
onClick={onClick}
>
<p>{children}</p>
</div>
);
};
const Select = ({ options }) => {
const [selectedOption, setSelectedOption] = useState(null);
return options.map(({ key, value }) => (
<Option
active={selectedOption === key}
onClick={() => setSelectedOption(value)}
>
{key}
</Option>
));
};
const App = () => (
<Select
options={[
{ key: "Oliver", value: "oliver" },
{ key: "Eve", value: "eve" },
]}
/>
);
There are a few issues that need to be addressed here –
In this code example, select and option are two independent components. This means that we can make use of the options component without the select component, which is not what we want, as options should be a part of the select menu.
Another issue with this approach is Prop Drilling. As you can see, we are passing props through many levels here. The App component here has no need to know about the options. Only the Select component should handle the Option state and behavior.
React compound component best practices
Now that we know the issues with this method, we need to ensure:
import React, { useState, useContext } from "react";
const SelectContext = React.createContext();
const Select = ({ children }) => {
const [activeOption, setActiveOption] = useState(null);
return (
<SelectContext.Provider value={{ activeOption, setActiveOption }}>
{children}
</SelectContext.Provider>
);
};
const Option = ({ key, children }) => {
const { activeOption, setActiveOption } = useContext(DropDownContext);
if (!activeOption || !setActiveOption) {
throw new Error(
"Context is undefined. Option should be used within the scope of a Select component!!"
);
return (
<p>Option should be used within the scope of a Select component!!</p>
);
}
return (
<div
style={
activeOption === key
? { backgroundColor: "grey" }
: { backgroundColor: "white" }
}
onClick={() => setActiveOption(key)}
>
<p>{children}</p>
</div>
);
};
Select.Option = Option;
export default function App() {
return (
<Select>
<Select.Option key="oliver">Oliver</Select.Option>
<Select.Option key="eve">Eve</Select.Option>
</Select>
);
}
You will notice the code differences here compared to our previous code example.
Only use Compound Components when:
For any React app, there are many instances where developers need to render elements based on certain conditions for creating React screens. This React Design pattern can be used at various instances of your React app based on your needs. A practical example of React conditional rendering will be if we need to integrate an authentication option in our React app.
For instance, if we are providing an authentication option, we need to make a ‘log out’ button that remains visible when the user is Logged In to the app and a Login/SignUp button for when the user is signed out or is visiting the app for the first time. This process of rendering UI elements for specific conditions or executing logic is known as a conditional rendering design pattern in React.
One of the most basic conditional rendering logic used in React is using an if statement. This type of conditional rendering is used when we want to render some element only when one specific condition is passed.
For instance, you have something that you don’t want to render in your React component as it doesn’t have the required props ready. A perfect example of this will be a React component list, which shouldn’t render the HTML elements in view if the list of items is missing or absent in the first place. We can set this conditional rendering using a simple JavaScript statement for returning the app to the previous state.
const users = [
{ id: "1", firstName: "Robin", lastName: "Wieruch" },
{ id: "2", firstName: "Dennis", lastName: "Wieruch" },
];
function App() {
return (
<div>
<h1>Hello Conditional Rendering</h1>
<List list={users} />
</div>
);
}
function List({ list }) {
if (!list) {
return null;
}
return (
<ul>
{list.map((item) => (
<Item key={item.id} item={item} />
))}
</ul>
);
}
function Item({ item }) {
return (
<li>
{item.firstName} {item.lastName}
</li>
);
}
Now, if the information passed from props will be undefined or null, the components will return null in conditional rendering. Hence, a react component returning a null value instead of JSX renders nothing.
Suppose/else is one of the most popular conditional rendering methods in React design patterns. It is ideal for situations where developers need to execute more than a single line of code inside if or else block or outside the scope of JSX.
For instance, you need to set and define accessibility features of different users based on their roles:
if (userProfile.role === "superadmin") {
initSuperAdminFunction();
initSuperAdminComponent();
// other block of codes;
} else if (userProfile.role === "admin") {
initAdminFunction();
initAdminComponent();
// other block of codes;
} else {
initUserComponent();
// other block of codes;
}
React Design Pattern If/else Best Practices:
Ternary Operator is nothing else but kind of a shortcut for the if/else conditional rendering design pattern. Using a ternary operator can enable developers to write the conditional rendering inline or in a single line of code.
For instance, lets see how ternary operator would help assigning variable values:
// Conditional rendering with common if else
let isDrinkTea;
if (role === "developer") {
isDrinkTea = true;
} else {
isDrinkTea = false;
}
// Conditional rendering with ternary operator
let isDrinkTea = role === "developer" ? true : false;
Now the same example can be continued with a function for return value:
// Conditional rendering with common if else
function isDrinkTea(role) {
if (role === "developer") {
return true;
} else {
return false;
}
}
// Conditional rendering with ternary operator
function isDrinkTea(role) {
return role === "developer" ? true : false;
}
In the code above, we compared setting the conditional rendering with if/else vs setting it with the ternary operator. You can clearly see the difference, as we could set the same logic in one line with the Ternary operator.
React Design Pattern Ternary Best Practices
So far, we understood how to use the if function, if/else function and how to reduce the code length of the if/else statement with the ternary operator. However, there is a simpler alternative to using the ternary operator. You can make use of a short-circuit AND operator for replacing a ternary operator like:
Instead of using the ternary operator,
{
isShow SmallComponent null;
}
We can use short-circuit && operator
{
isShow && SmallComponent ;
}
For the ternary operator, when there is no value assigned to the ‘else’ condition, you need to write the ‘: null’ expression to avoid an error. When using short-circuit && operator, we don’t need to write a null expression.
Note: You cannot replace if/else or if-else if-else statement with short circuit && operator.
React Design Pattern Short-Circuit & Best Practices
React also has a switch/case statement, another conditional operator often used as an alternative to if/else statements. However, the use case for this is a little different.
For instance, if you are working on a blogging app and you need to display different layouts for different user roles:
function App(props) {
const { role } = props;
if (role === "author") {
return <AuthorLayout>Let’s write something</AuthorLayout>;
}
if (role === "admin") {
return <AdminLayout>Here are the latest reports </AdminLayout>;
}
if (role === "moderator") {
return <ModeratorLayout>These are the ongoing events</ModeratorLayout>;
}
return <GuestLayout>Your current feed</GuestLayout>;
}
The above code can be refactored using switch/case statement like:
function App(props) {
const { role } = props;
switch (role) {
case "author":
return <AuthorLayout> Let’s write something </AuthorLayout>;
case "admin":
return <AdminLayout> Here are the latest reports </AdminLayout>;
case "moderator":
return <ModeratorLayout> These are the ongoing events </ModeratorLayout>;
default:
return <GuestLayout>Your current feed</GuestLayout>;
}
}
If we compare the two codes here, you can see in the JavaScript function you need to repeatedly use the break statement, whereas React component has the return statement to stop the switch operation.
However, if you wish to use switch without return, you will need to use a break statement after each component line to prevent the code from failing. This would look like this:
function App(props) {
const { role } = props;
switch (role) {
case "author":
return <AuthorLayout> Let’s write something </AuthorLayout>;
case "admin":
return <AdminLayout> Here are the latest reports </AdminLayout>;
case "moderator":
return <ModeratorLayout> These are the ongoing events </ModeratorLayout>;
default:
return <GuestLayout>Your current feed</GuestLayout>;
}
}
You can use either React design pattern; hence you can use whichever suits your case.
It can be used anywhere in React app for handling multiple conditional renderings with only one variable to evaluate the condition.
These are some of the most common and advanced React component patterns that give developers access to utilize React in all its strength and glory for creating scalable, robust, and secure React apps.
Make sure to understand these React patterns in detail to ensure you follow the best practices and apply the right pattern at the right place to optimize your React project.
This article was written by Saurabh Barot and has been reposted with permission.
Also published here.