Redux Best Practices

Redux is a library used for global state management, meaning if you have a piece of state that you want to access in different places of your app, you can use Redux to manage that state and make it easily accessible anywhere. The global state can be anything from logged-in user information to UI state such as theme, etc.


Redux is being used a lot together with React apps, however, it is possible to use Redux with any other library. Make sure you check out this tutorial on how to use Redux with React.


This guide can be beneficial if you have started learning redux lately, but even if you are an experienced developer working with Redux already. Additionally following this guide is going to improve your general skills with Redux by a lot and make you ready to work on bigger codebases that companies have.


Below I have brought up some of the best practices when using Redux that when followed, will help you make apps that scale well and have better performance.

Redux Toolkit

Redux Toolkit is a wrapper library written for the original Redux for the sake of making it easier, and faster to use Redux by reducing the boilerplate and introducing features such as thunks out of the box without having to install third-party libraries.


It is fully written in Typescript and is highly recommended that Redux Toolkit is used instead of the original Redux when using it for managing global state.


It offers a flexible API, making it possible for making the state management scalable without having to worry about the limits of the library. One of the key concepts of Redux is immutability, and Redux Toolkit makes it easier than ever to keep the immutability by already setting up the ImmerJS library internally, so the developer does not have to worry about accidentally modifying the state directly.


One of the most noticeable things for developers when switching to Redux Toolkit is the way reducers are created. Previously we used action creators and created a reducer where we handled all the actions.


Now we can use the createSlice API to create reducers and it will eliminate the need to write action creators by creating them for you.


An example of createSlice can be seen below:

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

const { actions, reducer } = createSlice({
  name: "user",
  initialState: { user: { id: 0 } },
  reducers: {
    userLoggedIn: (state, action: PayloadAction<number>) => {
      state.user.id = action.payload;
    },
  },
});

// Later you can dispatch actions by accessing the fields inside `actions`.
dispatch(actions.userLoggedIn(5));

It also introduces a whole new feature that is not in the original Redux, and that is the RTK Query which handles API calls, we are going to talk about it in a later section.


By using Redux Toolkit, the logic of your app will be simplified and it will be easier for devs to maintain the codebase. If you haven’t already switched to Redux Toolkit, then you should probably consider it.


The library can be installed by using the following command

npm i @reduxjs/toolkit

Proper Folder Structure

A good folder structure is crucial for scalability no matter what app you are building, and is not any less true for apps using Redux. However, choosing the right folder structure can be really hard, I’m going to bring the most preferred ways of structuring your Redux app, one that has been tested and is very scalable for building massive web applications.


It is a feature-based structure and it means structuring the app based on features, for example, if one of the features you have is the checkout process for e-commerce, then you might want to group all related global states for that process in a single reducer created using createSlice that we mentioned above.


You will have separated files for the reducers, selectors, thunks, types, etc., however, everything related to a feature will be put inside the same folder. Take the example below:

/src
	index.tsx
	/pages
	...
	/features
		/checkout
			// The slice mainly contains the reducers functionality
			// and exports the actions. Would be created using createSlice API.
			checkoutSlice.ts

			// This file would be used for writing the types
			// if you are using typescript.
			// Example here would be the store interface.
			checkoutTypes.ts

			// Here would go all the selectors that would
			// be used to select data from the redux store for this feature.
			checkoutSelectors.ts

			// Here would go all the thunks that would be used by this feature.
			checkoutThunks.ts

			// In this file there would be any functions which you would
			// use in your features store.
			// Can be anything that will make the code more readable,
			// say a function which does modifications to the payload
			// before saving to the store.
			checkoutHelpers.ts

			// It is not limited to only these files, you can
			// create anything you want, ex. you can have epics file, constants, etc.

However, no folder structure is perfect, and finally, it is up to you how you structure everything. As Redux is used to maintain a global state, it can get really messy if we start putting everything in Redux. So it is a good practice to keep Redux as small as possible with only data that really needs to be used in multiple places of the app. When developing an app, you should really take the time to think about where the data should live and not just put everything in Redux.


In libraries like React it has started being easier to separate data, for example, you can use the local component state for managing component UI data, or even context providers if you need some specific data across a page. Context providers should also be considered when saving UI data. It can really help implementing features with the principle of single responsibility in mind. Say for example you can have 1 context provider for the Theme, and another for UI changes in other components (ex. navigation, layout, etc.)

Event actions

When an action is dispatched, all of the reducers will be notified and every single one of them will check if they can handle the action. Not only that, but the Redux dev tools will be a mess trying to see what action was fired when. That’s why we should try to think of actions as events that happened and not what the action is changing. For example, we should rather have an action called userLoggedIn than setUserId.


Normally in the userLoggedIn event action, you could put what more data in the payload than in the setUserId. For example, in the userLoggedIn you could set all the user data in Redux, including any side effects you want to have when the user logs in (ex. changing the status to authenticated). Additionally, you can have multiple reducers responding to a single action, and with createSlice now it is even easier to respond to other external actions using the extraReducers field, see the example below:

// userSlice.ts
export const { actions: userActions } = createSlice({
  name: "user",
  //...
  reducers: {
    userLoggedIn: (state, action) => {
      state = action.payload;
    },
  },
});

// environmentSlice.ts
export const environmentSlice = createSlice({
  name: "environment",
  //...
  extraReducers: (builder) => {
    builder.addCase(userActions.userLoggedIn, (state, action) => {
      state.authenticationStatus = "loggedIn";
    });
  },
});

Thinking of actions as events we can keep the number of dispatched actions lower and debugging will always be faster as the dev tools will be cleaner.

Selecting small pieces of State

Hooks like useSelector will cause the component using it to rerender every time the returned value has changed. Meaning if you select the whole user object, whenever something in the object changes, even if you didn’t use it in the component, it is still going to rerender the component. So it is always preferred to only select what you really need from the state. So instead of selecting the whole user object, we would select only the user firstName. This way there will be a rerender only when the firstName changes.


Example:

// Will cause a rerender no matter if you are not using the whole user object.
const selectUser = ({ user }) => user;

// Preferred way, as this will cause a rerender only when firstName changes.
const selectUserFirstName = ({ user: { firstName } }) => firstName;

RTK Query for the API requests

RTK Query is a feature from Redux included in the Redux Toolkit library to make it easier to make API requests and handle caching. RTK Query aims to totally replace thunks for making API requests.


RTK Query introduces a new API called createApi that can be used to create the necessary functions for making the requests and handling caching. It includes features such as cache invalidation, prefetching, code splitting, etc.


It is written to work with React out of the box as it creates hooks that can be used directly in the components for making the requests.


Let's take a look at a simple example of how to create simple query hooks we can use in React.

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

// createApi should only be used once per app
const api = createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: "/"
  }),
  endpoints: (build) => ({
		// Building a query function that will later be transformed in a hook
    getPost: build.query({
      query: (id) => ({ url: `post/${id}` })
    })
  })
});

// Hook exported to be used in React
export const useGetPostQuery = api.endpoints.getPost.useQuery

// MyComponent.tsx
const { data, isLoading, ... } = useQuery();

RTK Query can be very useful, however, compared to other libraries such as React Query or SWR, it is more complicated and requires more boilerplate. So you should really do your research on what you need for your app before deciding on whether to use RTK Query or other alternatives. If you are building apps from the start which don’t need to work much with Redux then you can surly go with React Query or SWR, but if you already have a massive application that uses Redux as its main state management solution and you can’t afford to introduce a new library, then RTK Query can be more suitable.



Are you interested in reading more about Frontend in general? Then make sure you follow us on Twitter to get the latest updates.