Back

React Architecture Patterns for Your Projects

React Architecture Patterns for Your Projects

React is an un-opinionated framework in the front-end ecosystem. Its versatile nature does not provide a way to organize and structure a web application. The way an application is organized and structured is how a developer or a team of developers interact with it. A project with no logical structure means that everyone can do whatever they want inside it. A well-structured project requires developers to think deeper about their implementation and at the same time keep things organized. It also makes the codebase easy to navigate, modify and scale to add new features.

The architecture of a project is essential. An organized codebase is how a team of developers gets productive within the given structure in the long run. In this article, let’s look at some of the ways to keep the lifecycle of a React application healthy and well organized and what patterns you can follow.

Always have a starting point in your React application. This starting point comprises various folders such as assets, components, services, utils (reusable utility functions), pages, etc. Each of these folders contains various files required to fulfill the directory’s purpose.

For example, a bare minimum React project created using the create-react-app command-line utility to create a new React app comes with a /src directory. The starting point of this default React app (index.js file) is situated in this directory. A common practice is to use the /src directory as a familiar convention. That means all the different folders for components, pages, etc., are created inside this directory.

Using this convention, the top-level directory structure where your React app’s codebase lives can be viewed as:

└── /src
    ├── /assets
    ├── /components
    ├── /context
    ├── /hooks
    ├── /pages
    ├── /services
    ├── /utils
    └── App.js
    ├── index.js

The important part here is not to make the directory name a hard and fast rule but to follow a neat organization that anyone in the codebase can understand. That means, depending on the directory names you like to follow, the /services directory can be called /api.

Common Modules

One advantage of React being un-opinionated is it doesn’t care how you divide your modules. When developing a page in a React app, consider dividing it into modular pieces. This will help you reduce the complexity and create structures that are open for reusable or shared across the application.

Shareable code in React app should be divided under its own domain. A common module can be reusable custom components, custom hooks, business logic, constants, and utility functions. These reusable pieces are shared across the application to be used on more than one page component. Having a folder for them in your application’s directory structure is a good starting point.

Add Custom Components in their own folders

Examples of reusable components can be buttons, input fields, and content containers like cards. All of these components live within the /components directory. Each component will live inside its own sub-directory. For example, if you are creating reusable Buttons, here is an example of a directory structure you can consider:

└── /src
    ├── /components
    |   ├── /Button
    |   |   ├── Button.js
    |   |   ├── Button.styles.js
    |   |   ├── Button.test.js
    |   |   ├── Button.stories.js
    |   ├──index.js

Here is a brief explanation of what each file inside the Button directory contains:

  • Button.js file that contains the actual React component
  • Button.styles.js file, let’s assume you are using styled components, contains corresponding styles
  • Button.test.js file that contains tests
  • Button.stories.js the Storybook file

In the above structure, also notice that there is an index.js file located in the /components directory. In this scenario, this file acts as an index of all components that are part of the namesake directory. This file will look like the following:

// /src/components/index.js

import { Button } from './Button/Button';
import { InputField } from './InputField/InputField';
import { Card } from './Card/Card';

export { Button, Card, InputField };

Create Custom Hooks

A reusable React Hook is like a reusable working part. Just like you create custom components, creating a custom hook can help reduce code complexity.

Consider an example. In your React app, you have two different pages representing a login and a signup form. Each of these pages contains text input fields where users can enter their credentials and submit the form using a button. One of the input fields used in both forms is for users to enter their password. The password field contains an icon that allows the app user to toggle between the field’s visibility. Suppose you write the same code to implement this behavior in both login and signup forms. In that case, you will be duplicating the code.

A solution to this problem is to create a custom hook to toggle the icon and the field’s visibility based on the icon. Here is an example of a custom hook:

// /src/hooks/useTogglePasswordVisibility/index.js

export const useTogglePasswordVisibility = () => {
  const [passwordVisibility, setPasswordVisibility] = useState(true);
  const [rightIcon, setRightIcon] = useState('eye');

  const handlePasswordVisibility = () => {
    if (rightIcon === 'eye') {
      setRightIcon('eye-off');
      setPasswordVisibility(!passwordVisibility);
    } else if (rightIcon === 'eye-off') {
      setRightIcon('eye');
      setPasswordVisibility(!passwordVisibility);
    }
  };

  return {
    passwordVisibility,
    rightIcon,
    handlePasswordVisibility
  };
};

The custom hook above starts with a use naming convention. Even though React isn’t strict about naming conventions, it is important to follow them in this scenario. This hook also handles its own state and method and can be re-used on a password field, whether inside the login or a signup form.

The /hooks directory structure can be similar to components where a reusable hook lives inside their subdirectories:

└── /src
    ├── /hooks
    |   ├── /useTogglePasswordVisibility
    |   |   ├── index.js
    |   |   ├── useTogglePasswordVisibility.test.js
    |   ├──index.js

Use Absolute Imports

By default, a React app that uses the before mentioned directory structure might lead you to use import paths in the following manner:

// some file

import { Button } from '../../components';

You can configure your React application by adding support for importing modules using absolute paths. In a plain React app, this can be done by creating and configuring a jsconfig.json file at the root of your project.

Here is an example of a simple configuration inside the jsconfig.json file:

{
  "compilerOptions": {
    "baseUrl": "src"
  },
  "include": ["src"]
}

It makes it a lot easier to import components within the project and also, at the same time moving files without the need to change the import statements. If you want to import a module located at /src/components, you can import it as follows:

import { Button } from 'components';

If you have a custom Webpack configuration inside a file called webpack.config.js. In that case, you can customize this further to use a prefix like @components or ~components.

module.exports = {
  resolve: {
    extensions: ['js'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components')
      '@hooks': path.resolve(__dirname, 'src/hooks'),
    }
  }
};

This will allow you to import using a prefix. For example, a module located at /src/components/Button can be imported as:

import { Button } from '@components';

Note: If you’re using TypeScript, you will have a file called tsconfig.json. You can configure this file. For more information, check out Create React App documentation here.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Separate business logic from UI

The /pages directory will contain the UI for most of the application and the structure of each page in the form of React components. These components are naturally coupled with the business logic they represent. It is a common behavior that you will find in any React application. However, avoid unnecessary complexity in a UI component, you can separate the business logic from it.

One way to separate the UI and the logic is to create a custom hook, for example, for making an API request. Here is an example of making an API request to get items from an endpoint:

import { useQuery } from 'react-query';

export const moviesApi = {
  nowPlaying: () =>
    fetch(`${BASE_URL}/movie/now_playing?api_key=${API_KEY}&page=1`).then(res =>
      res.json()
    )
};

export const useNowPlayingMovies = () => {
  const { isLoading: nowPlayingLoading, data: nowPlayingData } = useQuery(
    ['movies', 'nowPlaying'],
    moviesApi.nowPlaying
  );

  return {
    nowPlayingLoading,
    nowPlayingData
  };
};

In the above snippet, take note that the API request to fetch data from the endpoint /movie/now_playing happens in two parts. The first part is the API request itself using JavaScript’s fetch method that gets the data in JSON format. The second part is where the useQuery hook from React Query library is configured and wrapped inside a custom hook.

You can store or create this custom hook file inside the dedicated /hooks directory or create another directory for API-related hooks such as /api or /services.

The Utils directory

Having a utils directory is completely optional. If your application tends to have some global utility functions, it could be a good idea to separate them from the UI components. A /utils directory may contain app-wide constants and helper methods. For example, more than one UI component in your application may require some validation logic. Separating this validation business logic in its own file under the /utils directory will help you create separate flows.

Avoiding creating a single Context for everything

Usually, when you pass props from a parent to a child component, it can be as simple as passing them from one to another. However, the complexity arises when there are many components in between. This creates an inconvenient way to pass props.

React Context provides a way to pass data through the component tree without prop drilling. It is an alternative approach that lets a parent component provide props to the entire tree below it. This way, sharing data becomes easy, and one can avoid passing unnecessary props.

In your React application, you should have separate Contexts for different things. For example, your React app uses themes for UI components, but some of these components use internationalization or i18n.

The i18n context doesn’t care if the theme exposes a certain dark or light mode value. In this scenario, you will have two different context providers. This helps you separate data logically.

const App = () => {
    return (    
    <ThemeProvider>
        <QueryClientProvider>
            <Routes />
        </QueryClientProvider>
    </ThemeProvider>
    )
}

Also, if a Context is used in one of the components, you do not need to wrap the application’s root with that Context’s provider. If a section of the application doesn’t require specific data, you don’t have to share it.

Conclusion

Patterns or practices described in this article are based on one’s experience. They may not work for every large-scale application of every kind. Take the parts that resonate with you or you think will help you get things more organized in your React project and leave out the rest. The important part when it comes to organizing a React app is to structure it in a manner that makes sense to you, and if you happen to work in a team with other people, it should make sense to them too.