Over 2,000 mentors available, including leaders at Amazon, Airbnb, Netflix, and more. Check it out
Published

Supercharge Your React Skills: 5 Advanced Patterns to Master in 2023

It can become tough to manage state, handle asynchronous input, and maintain a scalable architecture. We’ll look at five advanced React patterns in this article that will assist you in overcoming these difficulties
Faris Aziz

Engineering Manager & Staff Frontend Engineer, faziz-dev.com

React has become one of the most popular front-end libraries in recent years due to its simplicity and flexibility. Yet, when applications expand in complexity, it can become tough to manage state, handle asynchronous input, and maintain a scalable architecture. We’ll look at five advanced React patterns in this article that will assist you in overcoming these difficulties:

  1. Backend for Frontend Pattern (BFF)
  2. Hooks Pattern
  3. Higher-Order Component Pattern
  4. Observer Pattern
  5. Compound Pattern


Backend for Frontend Pattern (BFF)

The backend for frontend (BFF) pattern allows you to develop a React application with a separate backend that manages all of your API queries and data processing. This aids in maintaining the simplicity and cleanness of your front-end code and can enhance application performance.

Implementing this is even easier when using a framework like Next.js, which has an integrated API route system that enables you to quickly construct an API service for your frontend application. This is only one possibility, and using it to implement the BFF pattern in your application is not required.

When may it be advantageous to use the BFF pattern to your React application, then? One example is having a frontend application that is both large and sophisticated, with several API calls, data processing snd aggregation responsibilities, such as a dashboard. By separating your heavy processing logic from your frontend, you create a more scalable and maintainable architecture.

Using the BFF pattern may also help you avoid duplicating code and promote code reuse across your apps if you have many frontend applications that share comparable data and API queries.

W/out BFF

import { useState } from 'react';

function MyComponent() {
const [data, setData] = useState([]);

const processData = (rawData) => {
// super long and complex data processing here
};

const loadData = () => {
const rawData = [
{ id: 1, title: 'Smartphone', category: 'electronics' },
{ id: 2, title: 'Laptop', category: 'electronics' },
{ id: 3, title: 'Chair', category: 'furniture' },
{ id: 4, title: 'Table', category: 'furniture' },
];

const processedData = processData(rawData);
setData(processedData);
};

loadData();

return (
<div>
{data.map((item) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
}

W/ BFF

import { useEffect, useState } from 'react';
import axios from 'axios';

const API_URL = 'https://my-bff-service.com';

function MyComponent({ data }) {
return (
<div>
{data.map((item) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
}

function MyBFFComponent() {
const [data, setData] = useState([]);

useEffect(() => {
axios.get(`${API_URL}/my-data`).then((response) => {
setData(response.data);
});
}, []);

return <MyComponent data={data} />;
}

In this example, we’re making a request to our BFF service, which pre-processes our data so that we don’t need to on the frontend and as a benefit, we reduce the load on the client side. However, using this pattern isn’t always beneficial, we have to take into account the latency of the API request. Offloading our data processing to an API is only advantageous when the client-side processing and data aggregation slows down our frontend significantly, such as too many network requests, complex transformations on large data sets or other similar cases.

Hooks Pattern

The hooks pattern in React is a great feature that allows us to reuse stateful behaviour across several components. Hooks are functions that enable us to leverage React’s state and other capabilities in functional components.

The `useState` hook, which we used in the preceding example, is one of the most frequent hooks. There are, however, many additional hooks available, such as the `useEffect` hook, which enables us to perform side effects in our components, and the `useContext` hook, which enables us to access global data across our application.

Here’s an example of how we could use functional components to construct a custom hook to handle getting data from an API:

import { useState, useEffect } from 'react';
import axios from 'axios';

function useFetch(url) {
const [data, setData] = useState([]);
useEffect(() => {
axios.get(url).then((response) => {
setData(response.data);
});
}, [url]);
return data;
}

function MyComponent() {
const data = useFetch('https://my-api.com/my-data');
return (
<div>
{data.map((item) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
}

In this example, we’ve created a custom hook called useFetch, which takes a URL as a parameter and returns the data from the API. We’re then using this hook in our `MyComponent` component to fetch and display our data. By creating custom hooks, we can reuse stateful logic across multiple components and create a more modular and maintainable architecture.

Note: while React hooks may be declared and look like normal functions, they are unique because they are the only type of functions that are capable of “hooking” into React’s state and lifecycles, they can be easily spotted because they’re always prefixed with `use`

Higher-Order Component Pattern

The higher-order component (HOC) design pattern is a strong tool for React developers wishing to reuse and combine code. HOCs are functions that accept a component as input and return a new component with enhanced capabilities.

One popular application use case for HOCs is to perform authentication. For example, we might write a functional component that checks for authentication and then either renders the specified component or redirects to a login page. This component may then be wrapped in a HOC to add authentication features to any other component we want to secure.

This pattern helps abstract common functionality that may be required in several components, making our code more modular and easier to maintain. Moreover, HOCs may be reused across multiple components, avoiding code duplication.

Ultimately, HOCs may be a good tool for improving the functionality and reusability of our React components, and they are especially effective for integrating cross-cutting issues like authentication.

import React from 'react';

function withAuth(WrappedComponent) {
return function WithAuth(props) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
// Check if the user is logged in
const isLoggedIn = true; // Replace with actual authentication logic
setIsLoggedIn(isLoggedIn);
}, []);
if (!isLoggedIn) {
return <p>You must be logged in to access this content.</p>;
}
return <WrappedComponent {...props} />;
};
}
function MyComponent() {
return <p>This is protected content that requires authentication.</p>;
}
export default withAuth(MyComponent);

In this example, we’ve written a HOC called `withAuth` that takes a component as input and produces a new component that includes authentication logic. This HOC is then used in our `MyComponent` component to secure our content and demand authentication.

Observer Pattern

The observer pattern allows us to construct a one-to-many relationship between objects by automatically propagating changes in one object to all of its subscribers. This approach is useful in React apps for controlling state and data flow.

We can utilize the built-in `useContext` hook to create the observer pattern in React, which allows us to transmit data down the component tree without having to provide props explicitly.

Here’s an example of how we could use functional components to construct an observer in our application:

import { createContext, useContext, useState, useEffect } from 'react';

const MyContext = createContext([]);

function MyProvider(props) {
const [data, setData] = useState([]);
useEffect(() => {
// Fetch data from the API
const newData = [...]; // Replace with actual data
setData(newData);
}, []);
return <MyContext.Provider value={data}>{props.children}</MyContext.Provider>;
}

function MyObserver() {
const data = useContext(MyContext);
return (
<div>
{data.map((item) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
}

function App() {
return (
<MyProvider>
<MyObserver />
</MyProvider>
);
}

In this example, we’ve constructed a ‘MyProvider’ provider component that receives data from an API and sends it down to its children using the ‘useContext’ hook. The data from the provider is then shown using the ‘MyObserver’ component. We can construct a more flexible and maintainable architecture for controlling state and data flow in our React application by employing the observer pattern.

Compound Pattern

A compound pattern is a design pattern that combines many patterns to form a more sophisticated architecture. To construct a scalable and robust frontend architecture, we may integrate patterns like the BFF pattern, hooks pattern, HOC pattern, and observer pattern all into one solution.

Here’s an example of how we could use functional components to mix these patterns in our application:

import { createContext, useContext, useState, useEffect } from 'react';
import axios from 'axios';

const API_URL = 'https://my-bff-service.com';
const MyContext = createContext([]);

function withAuth(WrappedComponent) {
return function WithAuth(props) {
const [isLoggedIn, setIsLoggedIn] = useState(false);

useEffect(() => {
// Check if the user is logged in
const isLoggedIn = true; // Replace with actual authentication logic
setIsLoggedIn(isLoggedIn);
}, []);

if (!isLoggedIn) {
return <div>You must be logged in to access this content.</div>;
}

return <WrappedComponent {...props} />;
};
}

function useFetch(url) {
const [data, setData] = useState([]);

useEffect(() => {
axios.get(url).then((response) => {
setData(response.data);
});
}, [url]);

return data;
}

function MyComponent() {
const data = useFetch(${API_URL}/my-data);

return (
<div>
{data.map((item) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
}

function MyObserver() {
const data = useContext(MyContext);

return (
<div>
{data.map((item) => (
<div key={item.id}>{item.title}</div>
))}
</div>
);
}

function App() {
return (
<MyProvider>
<MyComponent />
<MyObserver />
</MyProvider>
);
}

In this example, we’ve combined all the aforementioned patterns into one application. We’re using the `MyProvider` component to provide data to our `MyComponent` and `MyObserver` components. We’re also using the `withAuth` HOC to protect the content in our MyComponent component, and the `useFetch` hook to fetch data from our BFF service. All these patterns combines into one has simplified all the complex interfaces of our codebase now we can create new functionality with ease and without duplication.

In conclusion, the five advanced React patterns we have discussed in this article can help you build more complex and robust applications. By leveraging all these patterns, you can better manage state, handle asynchronous data, and spread processing load better. By applying them to your own applications, you can take your React skills to the next level and create applications that are both efficient, clean and reliable.

Find an expert mentor

Get the career advice you need to succeed. Find a mentor who can help you with your career goals, on the leading mentorship marketplace.