Stop drowning in unnecessary meetings. Work on only what matters. Get Started.

Setup Redux for Large React Projects - without Tears

Setup Redux for Large React Projects -  without Tears

Hiya!!!

For small React projects, it's cool passing props between React components, from the Arctic to Antarctica 😃. With increasing complexity, we pass a bit more props and possibly throw in React Context to manage some state data between nested sibling components.

As the project grows, the need for a proper state management tool becomes unavoidable. This is the sweet spot for a tool like Redux in React projects. However, setting up Redux is considered herculean due to the amount of boilerplate required.

In this post, we'll set up a redux store suitable for large projects without shedding tears. This post's scope doesn't cover setting up Thunk middleware for async actions or persisting a store.

We'll focus mostly on store creation, project composition, entity reusability, and manageability. A considerable concern amongst developers using Redux in React projects is the convolution of the codebase with the increase in state variables.

What is Redux?

Quick director cut for those unfamiliar with Redux. Redux is a robust state container for JavaScript applications. Peep the barrels in the banner image. Like they hold wine possibly in your cellar, Redux creates a box for application data in your JavaScript application.

Redux creates an application data store and provides logic to retrieve and modify the stored data. Wondering how state management works on the bare minimum? I wrote this post about it using HTML and JavaScript.

With accompanying tools like React-Redux, state management in React apps becomes seamless. With state hoisted from components into the application layer, interactivity in your frontend app becomes almost limitless.

Prerequisites

Knowledge of JavaScript and React.js is required to follow through with this post. We'll create a simple counter app on CodeSandbox with data from the application state.

Create a React Project

Create a new CodeSandbox using the React starter by CodeSandBox. You can find the final code for this post here.

The created project comes with boilerplate code with the app entry point in src/index.js and the home page in src/App.js. Basic CSS styles are written in src/styles.css.

Create the base HTML structure for the counter app with 2 buttons and a counter display in App.js. Modify the file to:

import React from "react";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <h2>Counter is 0</h2>
      <button>Increment</button>
      <button>Decrement</button>
    </div>
  );
}

The buttons are currently inactive, and the counter display in the h2 isn't interactive.

Install Dependencies

The minimum package requirements for us are redux, react-redux, and redux-actions. Redux-actions is used to create actions neatly.

Add these dependencies in CodeSandbox using the "Dependencies" section of the Explorer. Once the modules are installed, the project automatically reloads.

Setup File Structure

A big part of setting up Redux for large projects is in the file structure. The structure should allow for easy maintainability and consider the separation of concerns in projects with multiple independent features.

For this setup, we'll make the following setup considerations:

  • Each application feature/section will hold its Redux setup in a folder
  • Each folder will have its own "feature" reducer.
  • Each file in the folder represents a specific action with its reducer.
  • A single file will hold the reducer for the app
  • A single file will contain the store setup.

Still complex? Let's create the folders to hold the setup for the "counter" feature.

In src, create a new folder named redux. In src/redux, create two files named store.js and reducer.js. Proceed to create a folder for the feature named counter.js in src/redux.

The final project structure looks like this:

Note: An essential part of this file structure is reusability. We'll utilize named exports in most cases to ensure we keep functions within the same context together.

The base of Redux lies in 3 entities - Constants, Actions, and Reducers. You've probably heard these before, and to put them under the microscope:

  • Constants (type) are unique identifiers for each action, and they are contained in the action object.
  • Actions specify what portion of the state needs to be modified. Action definitions are functions that take data as a parameter and return an object containing the constant of the action and the provided data (payload) if required and defined.
  • Reducers define how the state is modified. Reducers are functions that take the old state and the action and then specify the operation to be carried out on the state.

Each feature folder will hold files for various actions and reducers on the feature, with all the individual reducers merged into a single "feature reducer". Each file will contain an exported constant, action, and reducer. Let's get right to it!

Create Constants, Actions, and Reducers

In redux/counter create three files named increment-counter-action.js, decrement-counter-action.js, and index.js. Index.js is the entry point for the feature reducer.

In an e-commerce project, a feature folder could be 'catalog', and the files will be setProductList.js, setSearchQueryand setFavoriteProduct.

In increment-counter-action.js, we'll create the constant, action, and reducer with:

import { createAction } from "redux-actions";

// create constant
export const IncrementCounterRdxConst = "Counter/IncrementCounter";

// create action
export const IncrementCounterAction = createAction(
  IncrementCounterRdxConst,
  (payload) => payload
);

// create reducer
export const IncrementCounterReducer = (state, action) => {
  return state + 1;
};

We use the createAction function to create an action object with a specified constant and defined payload. The payload is an optional argument. Notice the separation of constant and action? These are also individually exported for usage outside the file. The reducer for this action simply increments the current state value by 1.

Repeat similarly for the decrement-counter-action.js file.

import { createAction } from "redux-actions";

export const DecrementCounterRdxConst = "Counter/DecrementCounter";

// create action
export const DecrementCounterAction = createAction(DecrementCounterRdxConst);

// Create reducer
export const DecrementCounterReducer = (state, action) => {
  return state - 1;
};

Create Feature Reducer

Next, we need to create a reducer for the feature, which uses a switch statement on the action constant propagated to return a specific reducer.

In src/redux/counter/index.js we have:

import {
  DecrementCounterReducer,
  DecrementCounterRdxConst
} from "./decrement-counter-action";
import {
  IncrementCounterReducer,
  IncrementCounterRdxConst
} from "./increment-counter-action";

export const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case IncrementCounterRdxConst:
      return IncrementCounterReducer(state, action);
    case DecrementCounterRdxConst:
      return DecrementCounterReducer(state, action);
    default:
      return state;
  }
};

We simply created a reducer function that returns any of the created reducers depending on the provided action. Notice the flexibility in importing the constants and reducer? Everything is separated and clear as the highways during the COVID-19 lockdown! 😁

The counterReducer is passed the initial state value, in this case, 0. You can define the initial state value; however it fits your use case.

With a new action, we simply add a new case here, import the constant and reducer from a single file.

Now we have the feature reducer done, we need to add this along with other feature reducers in a root reducer.

Create a Root Reducer

In larger projects, there are multiple features or scoped states belonging to separate portions of the app. Each has a folder and a feature reducer setup. In src/redux/reducer.js, we combine all of the feature reducers into a root reducer. Do this with:

import { combineReducers } from "redux";
import { counterReducer } from "./counter";

export const rootReducer = combineReducers({
  counter: counterReducer
  // other feature reducers come in here
});

The combineReducers function merges all the reducers into a root reducer. This is the state definition of your application. i.e., state.counter returns the data in the counter state. Depending on your state data structure, you can have multiple nested values and access them accordingly.

Create the store

With the actions and reducers setup, we'll create the store. In src/redux/store.js create a store with:

import { rootReducer } from "./reducer";
import { createStore } from "redux";

export const appStore = createStore(rootReducer);

You may wonder why the extra file just to create a store. In larger projects, other redux setups go in here. This includes setting up middlewares and adding persistent storage to the store. For this project, we'll have just the store created.

react-redux features a Provider component with which we will wrap our root element. This provides the store to the application. Modify src/index.js to do this with:

import React from "react";
import ReactDOM from "react-dom";
import {Provider} from 'react-redux'
import {appStore} from './redux/store'

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <Provider store={appStore}>
  <React.StrictMode>
    <App />
  </React.StrictMode>
  </Provider>,
  rootElement
);

We wrapped all the app with Provider and passed it props of the imported store.

Consume State

If you made it this far, you have setup redux and can utilize the app state's data.

react-redux provides two super useful hooks to store and retrieve data from redux. They are useSelector and useDispatch, to retrieve data and dispatch actions respectively. We'll modify src/App.js to utilize these hooks in making the counter interactive. Do this with:

import React from "react";
import "./styles.css";
import { useSelector, useDispatch } from "react-redux";
import { IncrementCounterAction } from "./redux/counter/increment-counter-action";
import { DecrementCounterAction } from "./redux/counter/decrement-counter-action";

export default function App() {
  const counter = useSelector((state) => state.counter);
  const dispatch = useDispatch();

  const _handleIncrement = () => {
    dispatch(IncrementCounterAction());
  };

  const _handleDecrement = () => {
    dispatch(DecrementCounterAction());
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <h2>Counter is {counter}</h2>
      <button onClick={_handleIncrement}>Increment</button>
      <button onClick={_handleDecrement}>Decrement</button>
    </div>
  );
}

Here, we imported the actions which will be dispatched on increment and decrement. We created two functions to handle the dispatch. The counter data is also retrieved from the redux state with an initial value of 0. In the case there is a payload, this is passed as an argument to the action call.

The increment and decrement functions are passed to the onClick handler of the buttons to increment and decrement the counter variable, respectively.

You can test out the buttons on end.

Here's what it looks like now:

You can find the CodeSandbox demo here.

Summary

In this post, we set up a redux store with constants, reducers, and actions. The store data is utilized in a counter application.

A key takeaway from this post is in the redux store setup and the separation of concerns. This ensures that when the application size increases and state variables abound, setting up new actions and reducers are seamless. Also, the codebase is maintainable.

Here's to becoming better, and wearing a mask, for now. 😃

William.


About the author

I love solving problems, which has led me from core engineering to developer advocacy and product management. This is me sharing everything I know.

Related Blogs

Practical Steps to Deploy a Node.js App with NGINX and SSL
Emeni Deborah

Emeni Deborah

Tue Sep 06 2022

Practical Steps to Deploy a Node.js App with NGINX and SSL

Read Blog

icon
Create a Crowdfunding Smart Contract using Solidity
Scofield Idehen

Scofield Idehen

Mon May 23 2022

Create a Crowdfunding Smart Contract using Solidity

Read Blog

icon
Deploy and Host an Appwrite Docker Application
Divine Odazie

Divine Odazie

Sat Apr 23 2022

Deploy and Host an Appwrite Docker Application

Read Blog

icon
image
image
icon

Join Our Technical Writing Community

Do you have an interest in technical writing?

image
image
icon

Have any feedback or questions?

We’d love to hear from you.

Building a great product is tough enough, let's handle your content.

Building a great product is tough enough, let's handle your content.

Create 5x faster, engage your audience, & never struggle with the blank page.