Redux Reducers Explained - How to Use Them Correctly
Redux Reducers Explained - How to Use Them Correctly

In this video you will learn deeper about reducers in Redux, how to write better code in them and how to combine them.
Let's jump right into it.

As you can see we have a reducer with 1 single action inside.

The first thing that I want to do is improve the structure of our reducer. For now our whole reducer is an array of users. It may be fine but normally we want to store more than 1 property inside a reducer. As an example we can move our username that we are typing inside reducer.

const initialState = {
  users: [],
  username: '',
};

const reducer = (state = initialState, action) => {}

As you can see instead of setting state to empty array we created initialState object. It serves 2 purposes: First of all this is our default state and secondly we see from the single glance all possible properties that are planned in our reducer.


Now let's move username from component to redux.

  handleUser = (e) => {
    this.props.dispatch({ type: "CHANGE_USERNAME", payload: e.target.value });
  };

Now let's look on our actions. We want to handle additionally change username action

const reducer = (state = initialState, action) => {
  if (action.type === 'ADD_USER') {
    return {
      ...state,
      username: "",
      users: [...state.users, state.username],
    };
  } else if (action.type === 'CHANGE_USERNAME') {
    return {
      ...state,
      username: payload
    };
  }

  return state;
};

The interesting part here that we have username always inside reducer so we don't need to pass it inside ADD_USER action anymore. Also we need to add username to our mapStateToProps.


Also there is super super important point that all reducer update must return new object. So we can't really modify the state directly but we must always return a new object. This is because of how Redux compares the previous and current state to see if something changed. Also we must write reducers as a pure function. Which means there should not be some side effects like feature data, accessing DOM properties or something else. Only state updates based on the previous state and action.


const mapStateToProps = (state) => {
  return {
    users: state.users,
    username: state.username,
  };
};

As you can see everything is working as previously but now we have 2 actions in reducer and bigger state


Now we can improve our reducers even more. As you can see if else conditions are quite verbose and we normally write switch case instead.

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "ADD_USER":
      return {
        ...state,
        username: "",
        users: [...state.users, state.username],
      };
    case "CHANGE_USERNAME":
      return {
        ...state,
        username: action.payload,
      };
    default:
      return state;
  }
};

As you can see, this code is much easier to read.


Now we are coming to the point that actually we want to have more than one reducer. Just imagine that we have redux start for all our pages like for example login, register, feed, single post and much more. As you can imagine it will be a mess if we write all this actions and properties in a single reducer.

The point is that createStore allows only a single reducer but there is a way to combine our reducers. Let's say that we want to create 1 more page with posts. So actually we can create 1 more reducer and pack all properties of that page there and rename our reducers.js to users for example.

src/store/reducers/posts.js

const initialState = {
  posts: [],
};

const reducer = (state = initialState, action) => {
  console.log("posts reducer", state, action);
  return state;
};

export default reducer;

As you can see we created an empty reducer with posts property. We can of course add here changing of the state depending on the action. We also moved all files in additional reducers folder.

But the question is now how to can use several reducers simultaneously? For this we need to use combineReducers function.

src/store/reducers/index.js

import { combineReducers } from "redux";

import posts from "./posts";
import users from "./users";

export default combineReducers({
  posts,
  users,
});

src/index.js

import reducer from "./store/reducers";
...

const store = createStore(reducer, composeWithDevTools());

As you can see inside our main reducer that we want to inject inside createStore we use combineReducers function from Redux. It just makes an object with properties like we name keys.

If we check in browser our page is broken but we can see that our state looks correctly now. We have 2 different namespaces: users and posts.

Now let's adjust our mapStateToProps because we changed our keys again.

const mapStateToProps = (state) => {
  return {
    users: state.users.users,
    username: state.users.username,
  };
};

As you can see now everything is working as expected.


But here is one more important thing to remember. As you can see we wrote a console.log inside posts reducer. And we can see in browser that we are getting this console log on every action that is dispatched. This is correct and happens because all our actions are global. It doesn't matter that we created additional reducer. All actions are global and they can change the state of any reducer. This is important to remember.

So here are important points to remember.

  • Defining of initialState in reducer always brings clarity.
  • Switch in reducers looks much better than if else
  • CombineReducers is an amazing possibility to scale you project indefinitely.

Also if you want to improve your programming skill I have lots of full courses regarding different web technologies.

📚 Source code of what we've done