Best Practices and Design Patterns in React.js for High-Quality Applications

Ori Baram
20 min readMar 30, 2023

Hey there, if you love using React.js like we do, you know it’s an awesome library for creating user interfaces. It’s simple, flexible, and super speedy. But as your app gets bigger, managing components, state, and logic becomes more of a challenge. To keep things running smoothly and make your code easy to understand, it’s important to follow best practices and design patterns.

In this guide, we’ll show you some of the most useful practices and patterns for working with React.js. We’ll cover things like how to structure your folders, separate concerns, and design components. These tips will make your life as a developer a whole lot easier. They will help you build applications that are easy to maintain, scale, and perform well. We’ll show you lots of real-life code examples to make things super clear and give you practical advice on how to put these best practices to work in your own projects. Plus there is a bonus at the end — a GitHub repo with all the code mentioned in this article!

So, let’s get started and make our React apps even better!

1. Best Practices for Building Scalable React Applications

If you are building a React application and want to make sure it stays organized and scalable, then you need to follow some best practices! In this section, we’ll cover four essential best practices that will help you build maintainable, scalable, and efficient React applications. These practices include organizing your project files in a feature-based folder structure, keeping your components focused and small, using appropriate naming conventions, and separating the responsibilities of Pages and presentational components. By following these practices, you can create applications that are easier to navigate, understand, and maintain.

1.1. Folder Structure

Having an organized folder structure is super important if you want to keep your project hierarchy clear and make it easy to navigate. Check out this example of a feature-based folder structure:

Instead of organizing components by file types, we group them by features. This makes it a lot simpler to find and manage related files, like a component’s JavaScript and CSS files.

1.2. Keep Components Small and Focused

When you’re building an application, it’s super important to create components that are easy to understand, maintain, and test. That’s why it’s a good idea to keep your components focused and small. If a component is starting to get too big, don’t sweat it — just break it down into smaller, more manageable components!

For instance, let’s say you have a UserProfile component that’s starting to feel overwhelming. You could break it down into smaller components like ProfilePicture, UserName, and UserBio. This will make each component simpler to handle and reuse.

Check out this example:

1.3. Naming Conventions

When you give your components, props, and state variables names that make sense, it helps other people (and future you!) understand your code more easily. Plus, it makes your code easier to maintain in the long run. Here are some tips to help you name things like a pro:

  • For components, use PascalCase (like UserProfile.js)
  • For variables and functions, use camelCase (like getUserData())
  • And for constants, use UPPERCASE_SNAKE_CASE (like API_URL)

Note that this is very subjective and it up to you to decide by which naming convention you want to follow. The most important thing is to be consistent with your naming style.

1.4. Pages (container components) and Presentational Components

In React, there are two types of components: Pages (container components) and presentational components. Pages handle tasks like fetching data from external sources (like APIs), managing state and logic, and passing data down to the presentational components using props. Meanwhile, presentational components are responsible for rendering UI elements and displaying data passed down from their parent components. By separating these responsibilities, we can create more modular and reusable components.

Example — TodoApp Page:

TodoList Component:

Okay, so when it comes to React components, we can divide them into two types: Pages (or container components) and presentational components. Pages handle stuff like getting data from APIs and managing state and logic, while presentational components show the data that they get from their props and define how things look. By doing this, we can keep our code more organized, easier to understand, and simpler to test.

1.5. Keep it DRY with Array Mapping

Using an array of items and mapping over them can be a nifty trick to avoid code repetition in your React components. Imagine you’re building a navigation bar with multiple links, each with its own title, path, and icon. Rather than writing out the same structure and code for each link, you can create an array of objects that hold all the necessary data and dynamically render them with a map function.

Check out this sample code that demonstrates how you can easily create an array of links and map over them to render a navigation bar:

In the example above, we demonstrated how creating an array of items and mapping over them can help you avoid code repetition in your React components. This technique is not only useful for rendering navigation bars, but also for rendering forms with multiple input fields.

By creating an array of objects that contain the necessary data for each input field, you can map over them to render the input fields dynamically. This can greatly simplify your code and make it more maintainable, especially when dealing with forms that have many input fields.

Here’s an example of this:

This cool example shows how to use an array of input objects to create dynamic input fields in React. Each input object includes the label, type, and name for each input field. By using the map() function, you can easily loop over the array and render each input field dynamically with the data from each input object.

Using arrays and mapping over them is a great way to create dynamic content in your React components without repeating the same code over and over. This technique makes your components more scalable, reusable and easier to maintain.

2. Separation of Concerns

Separation of concerns is super important in software development. It’s all about making sure each part of your app has one job to do. When it comes to React.js, separating the logic from the view can help make your code more maintainable, reusable, and scalable. So let’s take a closer look at some examples and techniques to help us achieve this separation and make our React apps even better.

2.1. Custom Hooks

Custom hooks let you take stateful logic out of your components and use it across multiple components. Custom hooks are just JavaScript functions with a special naming convention — they have to start with “use” (like, useForm). These hooks can use other built-in or custom hooks, and they can export an object or an array with all the properties and methods you want to use in your components. Want to see some real-life examples of how custom hooks can make your React.js code even better? Let’s dive in!

Example: Custom Hook for Form Input Handling

Imagine you’ve got a form with loads of input fields. Handling the state and input for each field can get pretty tricky and repetitive. But fear not — we can make things easier with a custom hook called useForm. This hook lets you extract the state and logic for all those fields and keep things neat and tidy.

First up, we use useState to create a values object that holds all our form input values. Then, we make a handleChange method that updates the values object whenever an input changes. Finally, we add a resetForm method that sets everything back to square one.

By popping values, handleChange, and resetForm in an array, we can use these properties and methods in all our components and make our form component way easier to maintain and reuse.

Now, we can use the useForm custom hook in our form component:

The useForm custom hook is the way to go if you want to handle form inputs in any React.js app with ease. Whether it’s a login form or a registration form, you name it.

Another example: Custom Hook for Fetching Data

Another thing we can do with custom hooks is fetch data from an API. It’s pretty common actually. All we need is to make a custom hook called useFetch that takes care of the state and side effects related to fetching data. Easy-peasy!

Now, we can use the useFetch custom hook in any component that requires data from an API:

Thanks to the useFetch custom hook, we can easily handle the state and logic for fetching data from the view in our UserList component. This makes our code way easier to maintain and reuse. With useFetch, we can keep all the state and side effects related to fetching data in one tidy spot, then use the resulting data in multiple components. This separation of concerns means we can easily change our data fetching logic without messing with the components that depend on it. All in all, it makes our code more maintainable and scalable.

2.2. Services

If you want to make your React apps more modular, using services is a great way to go. Basically, services are functions or classes that handle business logic like calling APIs, playing around with data, or other helpful tasks. You can import these services and use them in your components or custom hooks. It’s an easy way to separate logic from the view and keep things organized.

Example: API Service

Let’s say we need to work with a RESTful API to do some CRUD operations on user data. To make things simpler, we can create a service that takes care of all of that. It’s a handy way to communicate with the API and get our tasks done efficiently.

Now, we can use the API service in our components or custom hooks:

Thanks to the API service, we can keep all the API interaction logic separate from the view in our UserList component. That way, we can easily maintain and reuse our code without any fuss. It’s a great way to make sure everything stays organized and easy to manage.

Example: Utility Service

Let’s say we need to do some useful stuff like format dates, calculate sums, or make unique IDs. We can create a handy utility service that’ll take care of all that. It’s like our own little helper that we can call on whenever we need to get some useful work done.

Cool! We can easily use our utility service in our components or custom hooks to make our code more efficient and reusable. It’s a great way to keep things organized and make sure everything runs smoothly.

By using the utility service, we’ve kept the utility logic separate from the view, making our TransactionList component easier to manage and reuse.

When to use a Custom Hook and when to use a Service Function

When deciding whether to use a custom hook or a service function, it’s important to consider the specific use case and requirements of your application. Custom hooks are more appropriate when you need to manage state or context, or when you want to reuse functionality across multiple components. On the other hand, service functions are better suited for reusable logic that does not depend on state or context.

In practice, you may find that some functionality fits better as a custom hook, while other functionality works better as a service function. For example, here are some more examples of when you might choose to use each approach:

Custom Hooks:

  • Managing complex state: If you need to manage complex state across multiple components, a custom hook can be a great way to encapsulate that logic and make it reusable. For example, if you need to manage a user’s preferences and match them with your product, a custom hook like useUserSettings could help track the state of the user's preferences and provide functionality for checking the match with your product. The custom hook utilizes useState to track the state of the user's preferences and the match with your product, and provides a function to check the match based on the user's preferences and your product's categories.
  • Context-dependent functionality: A custom hook can help access the context from your application and provide reusable functionality. For example, if you need to perform user authentication and manage the user’s state across multiple components, a custom hook like useLoggedUser could help you access the user's state and provide functionality for updating the state. The custom hook can utilize the LoggedUserContext to access the user's state and provides functions for setting a new user, setting an existing user, updating the user's state, etc.

Service Functions:

  • Task-specific functionality: If you need to perform a specific task that doesn’t depend on context or state, a service function can be a more appropriate approach. For example, if you need to validate data like an email address or a password, or formatting data such as currency values or dates, or sorting data such as a list of products or search results, service functions could provide these functionalities without requiring state or context.
  • Standalone functionality: If you need to provide functionality that can be used across multiple applications or platforms, a service function can be a good way to encapsulate that logic and make it reusable. For example, if you need to provide a function for image processing, or file storage, or email sending, or payment processing, service functions could provide these functionalities in a way that can be used across multiple applications or platforms.

3. Component Design Patterns

When you’re building React apps, it’s important to make components that are easy to work with, reusable, and don’t give you a headache when you’re trying to update them. That’s where design patterns come in. In this section, we’re going to check out some design patterns that can help you make flexible and reusable components. These patterns will help you divide big components into smaller, more manageable pieces, making them easier to understand, test, and maintain.

3.1. Higher-Order Components (HOCs)

Higher-Order Components (HOCs) are pretty neat! They’re functions that take a component and return a new component with more features or behaviors. Basically, they let you reuse component logic and handle things like authentication or data fetching.

For example, you can use an HOC to check if a user is authenticated and then wrap a component that needs authentication with it. If the user is authenticated, the wrapped component will show up. But if not, they’ll be sent to the login page. Here’s an example to give you an idea of what it looks like:

Alright, so in this example we got this function called requireAuth, which gets a component as input and returns a new one that checks if the user is logged in. If not, it takes the user to the login page. Otherwise, the wrapped component is rendered with the extra props from the HOC.

Now, we can also use HOCs to wrap a component that needs some data fetching. Basically, the HOC will fetch the data and pass it down to the component as props.

Check it out:

So in this example, the withDataFetching function takes a component and gets data from a URL to pass it down as props. If there’s an error, it goes into the error prop, and if the data’s still loading, the isLoading prop is set to true.

There are plenty of other rad HOCs you can use with React too! One of them is memo. Instead of causing unnecessary re-renders even if a component’s props remain the same, thereby slowing down your React app, memo allows you to optimize your component so that it only re-renders if the props it receives have changed, making your app more efficient and improving performance. This can be a real game-changer for optimizing performance in your React app.

But there are also some other HOCs that are commonly used in popular React libraries. For example, there’s connect from the react-redux library, which connects a component to the Redux store, so it can access the store’s state and dispatch functions. This means you can easily create components that react to changes in the Redux store. Another one is withFirebase from the react-redux-firebase library, which provides a Firebase instance to a component, so it can interact with the Firebase backend.

HOCs are pretty cool because they let you reuse component logic and deal with problems that affect the whole app. It’s like killing two birds with one stone! HOCs make it so your components are nice and tidy, they’re easy to take care of, they’re focused and easy to maintain.

3.2. Render Props

Render props are a fancy term in React that lets you share code between components by passing a function as a prop to a child component, and it can be more flexible than HOCs in some cases.

This function, when called, returns a React element (usually JSX). The idea behind render props is to let the parent component take care of rendering some part of the component. So, the child component can focus on providing the necessary functionality, while the parent component determines how the rendered content should look like.

Let’s see an example of how render props work in real life:

Imagine you’re building an online store app, and you need to display a list of products in different parts of your app. You want to fetch the products and display them in different ways, such as a grid view or a list view. Here’s how you can achieve this using the render props pattern:

First, create a ProductFetcher component that fetches the product data and accepts a render prop (a function):

In this example, the ProductFetcher component is responsible for fetching the products and managing the loading state. It accepts a render prop, which is a function, and calls it with the products as an argument.

Now, let’s create two separate components for displaying the products in grid and list views:

Finally, use the ProductFetcher component and pass the appropriate render function to display the products in different formats:

So, in this example, we have the ProductFetcher component that fetches and manages the product data. The ProductGrid and ProductList components are responsible for displaying the products in different formats. With the help of the render prop, the ProductFetcher component fetches the product data, and the parent component (in this case, App) determines how the products should be displayed.

By using the render props pattern, we get some sweet benefits, like:

  1. Separation of concerns: The ProductFetcher component is only responsible for fetching and managing the loading state of products. It doesn’t have to worry about how the products are displayed.
  2. Reusability: The ProductFetcher component can be used throughout the app to fetch and display products in different formats without much hassle.
  3. Flexibility: The parent component can decide how the products should be displayed, giving us the freedom to customize the UI without tinkering with the fetching logic.

3.3. Compound Components

Compound components are a cool design trick that helps you create better and more flexible components. You can group together related components and control their behavior from a parent component, which can make your code much more organized and user-friendly.

One popular example of this is an accordion component, which usually has lots of panels that expand and collapse when you click them. With compound components, you can build a reusable accordion component that’s both easy to customize and easy to use.

Let’s check out an example of how to make an Accordion component using compound components:

In this example, we got this Accordion component, where we render a list of children and loop through them with React.Children.map to decide when to display them. We use React.cloneElement to give AccordionHeader and AccordionContent some extra props to control how they behave.

Here’s how all of these component would look in a component called MyAccordion:

You must agree with me that this component is way more comprehensive, easy to reason about and clean.

And if that’s not enough for you, we also got a Tabs component that shows different content when you click on different tabs. Just like with the Accordion, we can use compound components to make it easier to build and use. Want to see an example of how it’s done?

Check it out:

In this example we got a Tabs component that displays a list of tabs with different content. We use React.cloneElement to give the Tab and TabContent components some extra props for controlling their behavior.

And here’s how it will look combined all together:

Compound components are really cool and can make your components super flexible and expressive. Instead of just having a bunch of separate components, you can group them together in a parent component to make things way more intuitive and user-friendly. Stuff like Accordions, Tabs, and Dropdowns can get pretty complicated in a web app, but with compound components, you can simplify everything and make it easy-peasy for your users to navigate.

3.4. Optimize Rendering with React.memo

React.memo, that we already mentioned, is like a secret weapon for optimizing functional components. It makes sure that the component only re-renders when the props change, which can be a huge performance boost, especially when you’re dealing with complex or expensive-to-compute props. Basically, it helps you avoid unnecessary renders and keeps your app running smoothly.

Check out this example:

In this example, the UserProfileName component will only re-render when its name prop changes, avoiding unnecessary updates and improving performance.

3.5. Use React.lazy for Code Splitting and Lazy Loading

React.lazy is a really neat tool that can help speed up your app and make it more efficient. It’s a built-in feature that allows you to split up your components and load them only when you actually need them. This is great if you have a really big app with tons of different components that aren’t all needed right away. With React.lazy, you can pick and choose which components to load, and exactly when to load them. This means that your app can start up faster, use less memory, and generally be much more efficient. Plus, it’s super easy to use — all you need to do is wrap your component in a special function, and React will take care of the rest.

Let’s say you’re building an e-commerce app with React. You have a product listing page that displays all the products in your inventory. Each product has an image, name, price, and description.

You also have a product details page that displays more information about a single product, such as reviews, ratings, and related products.

If you load all the code for the product details page on the product listing page, it could slow down the initial load time and cause your app to feel sluggish. That’s where React.lazy comes in.

Here’s how you can use React.lazy to lazily load the ProductDetails page only when the user clicks on a product:

First, you create a separate file for the ProductDetails component:

Next, you wrap the ProductDetails component in a call to React.lazy:

This tells React to lazily load the ProductDetails component only when it’s needed.

Finally, you use the ProductDetails component in your product listing page, and wrap it in a Suspense component to handle the loading state:

When the user clicks on a product, the handleProductClick function sets the selectedProduct state to the clicked product. If selectedProduct is not null, the ProductDetails component is rendered inside a Suspense component.

The Suspense component displays a loading state until the ProductDetails component is loaded.

That’s it! With React.lazy, you can lazily load components when you need them, making your app faster and more efficient.

3.6. Use Context and React.createContext for Global State Management

The Context API is a great way to share state across multiple components without having to pass props down through the component tree. This can greatly simplify your state management and make your code more maintainable.

Take a look at this sample:

So, we have a ThemeContext and a ThemeProvider component that we wrap around the app. It’s great because all components can use the useTheme hook to access and update the theme without passing it around like a hot potato.

Here’s an example for this:

3.7. Reducers for Managing Complex State

Reducers are like a super handy tool that can help you manage complex state in a predictable and testable way. They’re a functional programming pattern that works great with useReducer hook or libraries like Redux. Basically, you pass them the current state and an action, and they return the new state.

Example: Todo App

So, here’s what you do first: make a function that’s gonna be the reducer and the actions you want:

Then, you can use the todoReducer with the useReducer hook. It’s as simple as that:

3.8. Use Keys When Rendering Lists

When you’re rendering a list of stuff in React, make sure to give each item a unique key. This helps React work its magic and make things faster and smoother. Here’s how you do it:

In this example, we give each item in the todos array a unique key, which helps React render things faster and avoid messing up the DOM, optimizing rendering and avoid unnecessary DOM updates.

By sticking to these best practices, you’ll be able to create React apps that are way easier to manage, scale, and keep running smoothly. By breaking things down into smaller, more focused components, optimizing rendering, and only loading the things you actually need, and handling state efficiently, you’ll be well on your way to creating a top-notch React app. So make sure to keep these tips in mind as you tackle your own React projects to ensure they’ll last for the long haul.

Conclusion

To wrap it up, all the cool design patterns and best practices we’ve talked about are super important for making awesome React.js apps. If you want to write clean, easy-to-read code that’s simple to maintain and scale up, then you gotta organize your folders right, split up your components into custom hooks, services, and presentational parts, use the appropriate names for things, and work with HOCs, render props, compound components, and reducers. And don’t forget the basics like keeping your components small, optimizing your rendering with React.memo, code splitting with React.lazy, using the Context API, and using keys when you’re doing list rendering. Following all these tips and tricks is the way to a robust and efficient application that both you, fellow developers and your users will love!

All the code mentioned in this article can be found in this repository.

--

--

Ori Baram

A programmer & instructor in JavaScript, HTML, CSS, dedicated to learning, teaching, evolving, and sharing a passion for coding with the world.