If you want to jump directly to the architecture specifications, please go to 'Architecting the Redux project' section. You can also take a look at the repo which I created for this tutorial from here.

Contents

Prerequisites

Node v10.x.x

Project Setup

We will be using create-react-app with typescript to start, but this architecture can be applied to any existing project as well:

$ yarn create react-app my-app --typescript

Then we can go to the project directory and start the app:

$ yarn start

Installing Dependencies

Obviously, we need redux related dependencies to be installed in code base on another command line in the project root directory:

$ yarn add redux@4.0.4 react-redux@7.1.0 redux-saga@1.0.5 redux-saga-test-plan@4.0.0-beta.4 typesafe-actions@4.4.2 enzyme@3.10.0 redux-mock-store@1.5.3 enzyme-adapter-react-16@1.14.0

Let's add development dependencies as well:

$ yarn add --dev @types/react-redux@ 7.1.1 @types/enzyme@3.10.3 @types/redux-mock-store@1.0.1 @types/enzyme-adapter-react-16@1.0.5

Integrating Redux to a project

After installation, we need to make a couple of changes on existing code to prepare code base for Redux architecture. First, please clear all markup inside of return . Then add markup for the redux provider with redux store and container. Final App.tsx will look like this:

Next, we will create state folder inside of src directory to keep our store configuration file and initialisation for Redux store.

$ mkdir src/state && touch src/state/index.ts

To be able to configure middlewares based on environment, let's create an index file which will import store configuration. In this tutorial, we will stay in the development environment:

Next, we need to create the development configuration of the store initialiser in the same directory:

For state modularisation purposes, we will follow feature-based folder structure approach. Which is called Ducks. You can learn more about this pattern here: freeCodeCamp: https://www.freecodecamp.org/news/scaling-your-redux-app-with-ducks-6115955638be/

So, lets create our first ducks folder which will hold other ducks (feature folders):

$ mkdir src/state/ducks && touch src/state/ducks/index.ts

Then we need to create root reducer that defines the application state:

Architecting the Redux project

Redux architecture revolves around a strict unidirectional data flow.

This means that all data in an application follows the same lifecycle pattern, making the logic of your app more predictable and easier to understand. It also encourages data normalization, so that you don’t end up with multiple, independent copies of the same data that are unaware of one another. — https://redux.js.org/basics/data-flow

Common Redux architecture with Redux-Saga

In this architecture data flow is as follows:

Component handles an event and dispatches an action

Redux calls the reducer functions

Related reducer function updates the state

Containers that are listening for the change of that state slice, gets notified

Containers updates Component with the new state and props

Component gets re-rendered if props are changed

Meanwhile, sagas are listening to certain actions and handling side effects. By product, it will dispatch new actions.

Let's continue by implementing data flow. First, we need one public API to consume. For this, we will use JSONPlaceholder.

To hold api URL, we need to create .env file. Don't forget to restart the npm server after adding the following lines:

To keep it simple, we will only implement GET request in this tutorial. But you can easily do POST by creating new actions that carry payload to send to API endpoint. To make it type safe, you should also change action types based on that. Based on that, our scalable architecture and folder structure looks like this:

src

├── App.css

├── App.test.tsx

├── App.tsx

├── components

│ ├── post.tsx

│ └── postList.tsx

├── containers

│ ├── __tests__

│ │ └── postList.container.spec.tsx

│ └── postList.tsx

├── index.css

├── index.tsx

├── logo.svg

├── react-app-env.d.ts

├── serviceWorker.ts

└── state

├── configureStore.dev.ts

├── ducks

│ ├── index.ts

│ └── post

│ ├── __tests__

│ │ ├── __mockData__

| | | └── postsData.json

| │ ├── actions.spec.ts

│ │ ├── reducers.spec.ts

│ │ └── sagas.spec.ts

│ ├── actions.ts

│ ├── reducers.ts

│ ├── sagas.ts

│ └── types.ts

├── index.ts

├── middlewares

│ └── saga.ts

└── utils

└── apiCaller.ts

Since we will have operations for posts in our API, the folder name is going to be called 'post'. Also will create test and mock data folder inside it as follows:

$ mkdir src/state/ducks/post src/state/ducks/post/__tests__ src/state/ducks/post/__tests__/__mockData__

In addition, we need to store our related types for that feature in this folder. Add related types into the types file:type

We are going to need some mock data for our post feature, which looks like this:

Actions

We will start by writing our actions for our post feature. For simplicity, we won't cover error handling in this tutorial. We will start writing tests first:

Then, writing actual actions:

Reducers

We need reducers to shape our state. Let's start implementing from tests:

Actual reducer will look like this:

By doing so, redux has set up and ready to use. Next step is how to deal with the side effects.

Middleware

Since we want our actions and reducers pure, we need to deal with that somewhere else. The best practice is to use some kind of middleware that can listen to actions and dispatch other actions to Redux. There are some options but for this tutorial, we are going to use Redux-Saga. This is also where we will execute our Ajax calls.

To hold our middlewares we will create a new folder called middlewares inside of state directory:

$ mkdir src/state/middlewares && touch src/state/middlewares/sagas.ts

Let's create a saga middleware for to use it in our store initialisation:

To use Redux-saga, we need to add our new middleware to store configuration as follows:

As always, we will start from writing our tests. Writing tests for sagas is quite easy and pretty straight forward with redux-saga-test-plan package:

Next, we will write our first saga to listen to certain actions:

We also need to create one root saga to fork all the feature sagas in our code base:

Ajax Calls

We will start by creating a new folder which will hold our state utilities:

$ mkdir src/state/utils && touch src/state/utils/apiCaller.ts

To make calls to APIs, we will use the built-in method fetch .We will implement this by creating a utility function called apiCaller .

Containers

As a best practice, we should differentiate container component and presentation components. Container components bridge between Redux store and presentation components. Container components are getting data from redux store and providing the data and actions to the presentational components. They tend to be stateful.

Since containers can listen to more than one state slice, they should go to their own directory.

$ mkdir src/containers src/containers/__test__

Let start writing tests for container first:

Next is actual container code:

Let's finish it up by creating presentations components:

$ mkdir src/components

With that, we completed Redux data flow and architecture. No matter how many features you add to this code base, the complexity will stay almost the same.

What is next?

There are some tools and dependencies you can install and use as a middleware to ease your development and help newcomers to onboard faster.

redux-logger

redux-devtools-extension

Conclusion

As we move forward, we put good effort into architecting projects according to our needs. It makes our projects more sustainable. While doing that we learn and generalise what we have learnt so that we can re-use and improve in our future projects.

Redux has come a long way and still going strong. Redux is still popular and widely used in many projects in production today. This architecture that we use still up to date and does the job right. I hope this tutorial will help you with your next project.

Cheers!