React Design Patterns- A Comprehensive Guide
Last Updated on
May 28, 2024
Traditional web development was complicated once but with the arrival of React on the scene, the process has been simplified significantly. It also offers great ease of use thanks to the reusable components and its extensive ecosystem.
React ecosystem provides a large range of tools; some are used to fulfill various development requirements whereas others help resolve different types of issues. React Design Patterns are quick and reliable solutions for your typical development problems.
In ReactJS, you can find a large number of design patterns leveraged by Reactjs development companies and each serves a unique purpose in a development project. This article talks about some “need-to-know” design patterns for React developers.
But before diving into the topic, it is important to know what are the design patterns in React and how they are useful for app development.
1. What is a Reactjs Design Pattern?
ReactJS design patterns are solutions to common problems that developers face during a typical software development process. They are reusable and help reduce the size of the app code. There is no need to use any duplication process to share the component logic when using React design patterns.
While working on a software development project, complications are bound to arise. But with reliable solutions from Design patterns, you can easily eliminate those complications and simplify the development process. This also enables you to write easy-to-read code.
2. Benefits of Using React Design Patterns in App Development
If you have any doubt about the effectiveness of React development, it is because you might have yet to take a look at the benefits of React Design Patterns. Their advantages are one of those factors that make React development more effective.
2.1 Reusability
React Design Patterns offer reusable templates that allow you to build reusable components. Therefore, developing applications with these reusable components saves a lot of your time and effort. More importantly, you don’t have to build a React application from the ground up every time you start a new project.
2.2 Collaborative Development
React is popular for providing a collaborative environment for software development. It allows different developers to work together on the same project. If not managed properly, this can cause some serious issues. However, the design patterns provide an efficient structure for developers to manage their projects effectively.
2.3 Scalability
Using Design patterns, you can write React programs in an organized manner. This makes app components simpler. So, even if you are working on a large application, maintaining and scaling it becomes easy. And because every component here is independent, you can make changes to one component without affecting another.
2.4 Maintainability
Design patterns are referred to as the solutions for typical development issues because they provide a systematic approach to programming. It makes coding simple which is not only helpful in developing the codebase but also in maintaining it. This is true even if you are working on large React projects.
React Design Patterns make your code more decoupled and modular which also divides the issues. Modifying and maintaining a code becomes easy when dealing with small chunks of code. It is bound to give satisfactory results because making changes to one section of the code will not affect other parts in a modular architecture. In short, modularity promotes maintainability.
2.5 Efficiency
Although React has a component-based design, it can provide faster loading time and quick updates, thanks to the Virtual DOM. As an integral aspect of the architecture, Virtual DOM aims to improve the overall efficiency of the application. This also helps offer an enhanced user experience.
Moreover, design patterns such as memoization save results of expensive rendering so that you do not have to conduct unnecessary rerenders. Re-renderings take time but if the results are already cached then they can be immediately delivered upon request. This helps improve the app’s performance.
2.6 Flexibility
Due to its component-based design, applying modifications to the React apps is convenient. Using this approach allows you to try out various combinations of the components to build unique solutions. Components design patterns also allow you to craft a suitable user interface. Your application needs such flexibility to succeed in the marketplace.
Unlike other famous web development frameworks, React doesn’t ask you to adhere to specific guidelines or impose any opinions. This offers ample opportunities for developers to express their creativity and try to mix and match various approaches and methodologies in React development.
2.7 Consistency
Adhering to React design patterns provides a consistent look to your application and makes it user-friendly. The uniformity helps offer a better user experience whereas simplicity makes it easy for users to navigate across the app which increases user engagement. Both of these are important factors to boost your revenues.
3. Top Design Patterns in React that Developers Should Know
Design patterns help you resolve issues and challenges arising during development projects. With so many efficient design patterns available in the React ecosystem, it is extremely difficult to include them all in a single post. However, this section sheds some light on the most popular and effective React design patterns.
3.1 Container and Presentation Patterns
Container and presentation patterns allow you to reuse the React components easily. Because this design pattern divides components into two different sections based on the logic. The first is container components containing the business logic and the second one is presentation components consisting of presentation logic.
Here, the container components are responsible for fetching data and carrying out necessary computations. Meanwhile, presentation components are responsible for rendering the fetched data and computed value on the user interface of the application or website.
When using this pattern for React app development, it is recommended that you initially use presentation components only. This will help you analyze if you aren’t passing down too many props that won’t be of any use to the intermediate components and will be further passed to the components below them.
If you are facing this problem then you have to use container components to separate the props and their data from the components that exist in the middle of the tree structure and place them into the leaf components.
The example for container and presentation pattern:
import React, { useEffect, useState } from "react"; import UserList from "./UserList"; const UsersContainer = () => { const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const getUsers = async () => { setIsLoading(true); try { const response = await fetch( "https://jsonplaceholder.typicode.com/users" ); const data = await response.json(); setIsLoading(false); if (!data) return; setUsers(data); } catch (err) { setIsError(true); } }; useEffect(() => { getUsers(); }, []); return <UserList isLoading={isLoading} isError={isError} users={users} />; }; export default UsersContainer; // the component is responsible for displaying the users import React from "react"; const UserList = ({ isLoading, isError, users }) => { if (isLoading && !isError) return <div>Loading...</div>; if (!isLoading && isError) return <div>error occurred.unable to load users</div>; if (!users) return null; return ( <> <h2>Users List</h2> <ul> {users.map((user) => ( <li key={user.id}> {user.name} (Mail: {user.email}) </li> ))} </ul> </> ); }; export default UserList; |
-
{users.map((user) => (
- {user.name} (Mail: {user.email}) ))}
3.2 Component Composition with Hooks
First introduced in 2019, Hooks gained popularity with the advent of React 16.8. Hooks are the basic functions designed to fulfill the requirements of the components. They are used to provide functional components access to state and React component lifecycle methods. State, effect, and custom hooks are some of the examples of Hooks.
Using Hooks with components allows you to make your code modular and more testable. By tying up the Hooks loosely with the components, you can test your code separately. Here is an example of Component composition with Hooks:
// creating a custom hook that fetches users import { useEffect, useState } from "react"; const useFetchUsers = () => { const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const controller = new AbortController(); const getUsers = async () => { setIsLoading(true); try { const response = await fetch( "https://jsonplaceholder.typicode.com/users", { method: "GET", credentials: "include", mode: "cors", headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, signal: controller.signal, } ); const data = await response.json(); setIsLoading(false); if (!data) return; setUsers(data); } catch (err) { setIsError(true); } }; useEffect(() => { getUsers(); return () => { controller.abort(); }; }, []); return [users, isLoading, isError]; }; export default useFetchUsers; |
Now, we have to import this custom hook to use it with the StarWarsCharactersContainer component.
Now, we have to import this custom hook to use it with the UsersContainer component.
import React from "react"; import UserList from "./UserList"; import useFetchUsers from "./useFetchUsers"; const UsersContainer = () => { const [users, isLoading, isError] = useFetchUsers(); return <UserList isLoading={isLoading} isError={isError} users={users} />; }; export default UsersContainer; |
3.3 State Reducer Pattern
When you are working on a complex React application with different states relying on complex logic then it is recommended to utilize the state reducer design pattern with your custom state logic and the initialstate value. The value here can either be null or some object.
Instead of changing the state of the component, a reducer function is passed when you use a state reducer design pattern in React. Upon receiving the reducer function, the component will take action with the current state. Based on that action, it returns a new State.
The action consists of an object with a type property. The type property either describes the action that needs to be performed or it mentions additional assets that are needed to perform that action.
For example, the initial state for an authentication reducer will be an empty object and as a defined action the user has logged in. In this case, the component will return a new state with a logged-in user.
The code example for the state reducer pattern for counter is given below:
import React, { useReducer } from "react"; const initialState = { count: 0, }; const reducer = (state, action) => { switch (action.type) { case "increment": return { ...state, count: state.count + 1 }; case "decrement": return { ...state, count: state.count - 1 }; default: return state; } }; const Counter = () => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: "increment" })}>Increment</button> <button onClick={() => dispatch({ type: "decrement" })}>Decrement</button> </div> ); }; export default Counter; |
3.4 Provider Pattern
If you want your application to stop sending props or prop drilling to nested components in the tree then you can accomplish it with a provider design pattern. React’s Context API can offer you this pattern.
import React, { createContext } from "react"; import "./App.css"; import Dashboard from "./dashboard"; export const UserContext = createContext("Default user"); const App = () => { return ( <div className="App"> <UserContext.Provider value="React User"> <Dashboard /> </UserContext.Provider> </div> ); }; export default App; //Dashboard component import React, { useContext } from "react"; import { UserContext } from "../App"; const Dashboard = () => { const userValue = useContext(UserContext); return <h1>{userValue}</h1>; }; export default Dashboard; |
The above provider pattern code shows how you can use context to pass the props directly to the newly created object. Both the provider and the consumer of the state have to be included in the context. In the above code, the dashboard component using UserContext is your consumer and the app component is your provider.
Take a look at the visual representation given below for better understanding.
When you don’t use the provider pattern, you have to pass props from component A to component D through prop drilling where components B and C act as intermediary components. But with the provider pattern, you can directly send the props from A to D.
3.5 HOCs (Higher-Order Components) Pattern
If you want to reuse the component logic across the entire application then you need a design pattern with advanced features. The higher-order component pattern is the right React pattern for you. It comes with various types of features like data retrieval, logging, and authorization.
HOCs are built upon the compositional nature of the React functional components which are JavaScript functions. So, do not mistake them for React APIs.
Any higher-order component in your application will have a similar nature to a JavaScript higher-order function. These functions are of pure order with zero side effects. And just as the JavaScript higher-order functions, HOCs also act as a decorator function.
The structure of a higher-order React component is as given below:
const MessageComponent = ({ message }) => { return <>{message}</>; }; export default MessageComponent; const WithUpperCase = (WrappedComponent) => { return (props) => { const { message } = props; const upperCaseMessage = message.toUpperCase(); return <WrappedComponent {...props} message={upperCaseMessage} />; }; }; export default WithUpperCase; import React from "react"; import "./App.css"; import WithUpperCase from "./withUpperCase"; import MessageComponent from "./MessageComponent"; const EnhancedComponent = WithUpperCase(MessageComponent); const App = () => { return ( <div className="App"> <EnhancedComponent message="hello world" /> </div> ); }; export default App; |
3.6 Compound Pattern
The collection of related parts that work together and complement each other is called compound components. A card component with many of its elements is a simple example of such a design pattern.
The functionality provided by the card component is a result of joint efforts from elements like content, images, and actions.
import React, { useState } from "react"; const Modal = ({ children }) => { const [isOpen, setIsOpen] = useState(false); const toggleModal = () => { setIsOpen(!isOpen); }; return ( <div> {React.Children.map(children, (child) => React.cloneElement(child, { isOpen, toggleModal }) )} </div> ); }; const ModalTrigger = ({ isOpen, toggleModal, children }) => ( <button onClick={toggleModal}>{children}</button> ); const ModalContent = ({ isOpen, toggleModal, children }) => isOpen && ( <div className="modal"> <div className="modal-content"> <span className="close" onClick={toggleModal}> × </span> {children} </div> </div> ); const App = () => ( <Modal> <ModalTrigger>Open Modal</ModalTrigger> <ModalContent> <h2>Modal Content</h2> <p>This is a simple modal content.</p> </ModalContent> </Modal> ); export default App; |
Compound components also offer an API that allows you to express connections between various components.
3.7 Render Prop Pattern
React render prop pattern is a method that allows the component to share the function as a prop with other components. It is instrumental in resolving issues related to logic repetition. The component on the receiving end could render content by calling this prop and using the returned value.
Because they use child props to pass the functions down to the components, render props are also known as child props.
It is difficult for a component in the React app to contain the function or requirement when various components need that specific functionality. Such a problematic situation is called cross-cutting.
As discussed, the render prop design pattern passes the function as a prop to the child component. The parent component also shares the same logic and state as the child component. This would help you accomplish Separation of concerns which helps prevent code duplication.
Leveraging the render prop method, you can build a component that can manage user components single-handedly. This design pattern will also share its logic with other components of the React application.
This will allow the components that require the authentication functionality and its state to access them. This way developers don’t have to rewrite the same code for different components.
Render Prop Method Toggle code example:
import React from "react"; class Toggle extends React.Component { constructor(props) { super(props); this.state = { on: false }; } toggle = () => { this.setState((state) => ({ on: !state.on })); }; render() { return this.props.render({ on: this.state.on, toggle: this.toggle, }); } } export default Toggle; import React from "react"; import Toggle from "./toggle"; class App extends React.Component { render() { return ( <div> <h1>Toggle</h1> <Toggle render={({ on, toggle }) => ( <div> <button onClick={toggle}> {on ? "Click to turn off" : "Click to turn on"} </button> <p>The toggle is {on ? "on" : "off"}.</p> </div> )} /> </div> ); } } export default App; |
3.8 React Conditional Design Pattern
Sometimes while programming a React application, developers have to create elements according to specific conditions. To meet these requirements, developers can leverage the React conditional design pattern.
For example, you have to create a login and logout button if you want to add an authentication process to your app. The process of rendering those elements is known as a conditional rendering pattern. The button would be visible to first-time users.
The most common conditional statements used in this pattern are the if statement and suppose/else statement. The ‘if statement’ is used when it is required to pass at least one condition. Meanwhile, the developer uses the suppose/else statement when more than one condition has to be passed.
You can easily refactor the code given above with the help of the switch/case statement in the following way:
const MyComponent = ({ isLoggedIn }) => { if (isLoggedIn) { return <LoggedInComponent />; } else { return <LoggedOutComponent />; } }; export default MyComponent; Example with the help of the switch statement in the following way: const MyComponent = ({ status }) => { switch (status) { case "loading": return <LoadingComponent />; case "error": return <ErrorComponent />; case "success": return <SuccessComponent />; default: return null; } }; export default MyComponent; |
4. Conclusion
The React design patterns discussed in this article are some of the most widely used during development projects. You can leverage them to bring out the full potential of the React library. Therefore, it is recommended that you understand them thoroughly and implement them effectively. This would help you build scalable and easily maintainable React applications.
View Comments
This blog post offers a clear introduction to React design patterns, explaining their benefits and showcasing some of the most useful ones for React developers.