post cover

HOW TO USE REDUX WITHOUT LOSING YOUR MIND

November 19, 2019

TLDR; You can use redux without a lot of boilerplate with just one action changing the whole reducer state. Consistency of the structure can be reached with immutable-helper and typescript.

This is just my opinion and my work-flow with redux library. I don't say it is the best way for using it - every framework and the way of using brings its pros and cons.

For establishing this work-flow you will need:

  • React with typescript
  • Redux
  • immutability-helper npm package

Okay so you probably know redux and what it is used for. A global state - one source of truth. When it is changed, appropriate components get rerendered with a new data from the store. This is used mainly to avoid passing handlers and data back and forth between components and their parents.

There are an actions, reducers and the store. React components should dispatch actions to store. Then redux finds appropriate reducer to handle the action by its type. Then new state is returned from reducer and store will notify all components to rerender.

diagram shows how redux works with react

If you have ever worked with redux you have always end up with a structure similar to this.

diagram of how according to docs should be actions mapped on reducers

Every action has its own case handler inside switch in a reducer. Like so:

  const reducer = (state = defaultState, action) => {
    switch (action.type) {
      case ActionType.LOAD_POSTS_REQUEST:
        return {
          ...state,
          posts: action.payload,
        }
      case ActionType.LOAD_POSTS_REQUST_SUCC:
        return {
          ...state,
          somethings,
        }
      case ...:
        return {
          ...state,
          somethings,
        }
      default: 
        return state;
    }
  }

Sometimes, if your state is a little bit nested and if you want to change one attribute inside nested structure, you end up doing this maybe:

  case ActionType.UPDATE_SOMETHING_NESTED:
    return {
      ...state,
      some_attr: {
        ...state.some_attr,
        another_nested: {
          ...state.some_attr.another_nested,
          a: 3,
        }
      }
    }

You can’t just do state.some_attr.another_nested.a = 3;, you want to keep your data immutable and you want to have different reference for the structures, that has changed. As you can see, if you do immutability here with vanilla options, your code starts to looks like the one above or even worse. Trust me, you don’t want this. We get to this problem later with our immutability-helper

What is wrong with this approach

A lot of boilerplate code for me. I didn’t like this approach from the first moment I have started using it. A code always ended up with a few hundred of lines and I started to get lost inside my own code.

A good practice is also keeping a file named ActionTypes where you have your action types stored and then you would use them over your actions and reducers as variables.

  export const FETCH_ALL_POSTS = "FETCH_ALL_POSTS";
  export const FETCH_ALL_POSTS_SUCC = "FETCH_ALL_POSTS_SUCC";
  export const FETCH_ALL_POSTS_ERR = "FETCH_ALL_POSTS_ERR";
  // ... and so on and so on

What I ended up was a big unnecessary file with a lot of these types. Do you know where I did a lot of bugs? When CTRL+C and CTRL+V these types and forget to change a string. So I ended up searching for bug for a next 30 minutes.

  // EASY TO MAKE A MISTAKE
  // SEE THE THIRD FETCH_ALL_POSTS_ERR

  export const FETCH_ALL_POSTS = "FETCH_ALL_POSTS";
  export const FETCH_ALL_POSTS_SUCC = "FETCH_ALL_POSTS_SUCC";
  export const FETCH_ALL_POSTS_ERR = "FETCH_ALL_POSTS_SUCC"; // <-- DO YOU SEE THAT SUCC ??? :)

Then there is so called action creators. Action creators are functions, that create an object (Action) for you: Such as this:

  // Again copying all 
  const fetchAllPostsActionCreator = () => ({
    type: ActionTypes.FETCH_ALL_POSTS
  });
  const fetchAllPostsSuccActionCreator = (payload) => ({
    type: ActionTypes.FETCH_ALL_POSTS_SUCC,
    payload,
  });
  const fetchAllPostsErrActionCreator = (error) => ({
    type: ActionTypes.FETCH_ALL_POSTS_ERR,
    error,
  });

Do you like that long function namings? I don’t like them. We use action creators because we do not want to create an action objects by hand - so we can’t forget what ActionType has to go there or what name of payload and other attributes and so on. The thought behind it is OK.

But, do you see how many code is being generated, and we didn’t do anything yet. But we do it for safety and consistency, testability and other things some manager/team leader wants right?

So disadvantages for me was:

  • A lot of useless code being generated
  • copy+paste mistakes, hard to find these bugs
  • small flexibility, you need to work with existing actions/actionCreators or make another few and generate a few hundreds of code
  • I’m not sure about this one, but as I know, if you dispatch some action to the store then redux must iterate over all reducers and find appropriate reducer-switch-case for this one of type function. So when you start to have like 200-300 different reducer-cases, then redux needs to iterate over every these reducers and every switch case which exists on every dispatch that happens. This can lead to slower response as application gets bigger.

Solution

Have only one reducer switch case and set state from an action.

new implementation only one to one action to reducer mapping

Other functions interacting with action

I strongly recommend using Typescript here.

// actions.ts

const setPostsState = (newState: PostsState) => store.dispatch({
  action: "SET_POSTS_STATE", // Only one action type for this reducer
  payload: newState,
});

Noticed how I wrapped this function with store.dispatch ? I did it because I wanted an action to be really the action in the real sense of meaning. When I want to setPostsState, I really want to set the fu*king state. I don’t want action type, I don’t want action creator, I don’t want an object… I just want to set store state and move on with things.

With this, I don’t have to wrap it like this store.dispatch(SetPostsStateAction) … I ended up in a lot of cases where my store just didn’t updated because I called action creator without dispatch and so on. So I kept it simple and had dispatch function included in this one. Simple.

  // reducer.ts

  const defaultState: PostsState = {
    posts: [],
    fetch: {
      error: null,
      loading: false,
    }
  };

  const postsReducer = (state: PostsState = defaultState, action: PostsActions) => {
    switch (action.type) {
      case "SET_POSTS_STATE":
        return action.payload;
      default:
        return state;
    }
  }

Advantages:

  • No boilerplate, less code
  • Only one action for one reducer, one access point - Faster than 300 iteration reducer-switch-cases
  • Flexible, every component or function can just set what it needs
  • (With typescript, it is safe to change state from anywhere in the app)

Immutability-helper

Immutability-helper is package that can updates nested structure. When structure is updated a new immutable object with a new reference is returned. Only structures that really have changed have different reference. Then redux can easily does a reference comparison and knows whether something has changed or not. Syntax is at the beggining a little weird, but when you get used to, you will not want to set immutable state differently.

  import update from "immutability-helper";
  
  const { postsState } = store.getState();
  
  newState = update(newState, {
    fetch: {
      loading: {
        $set: true,
      }
    },
  });

  // now we can use our set state function
  setPostsState(newState);

One more thing: You can do this safely from anywhere from your code. One disadvantage is, that when type of the posts state will change, you need eventually update every access setting point in your app. If for some reason will be loading inside state object renamed to loadingRequest, you need to update it everywhere and typescript doesn’t let you compile your code.

You can’t see a real advantage here because our state is too small. But when your state starts to have more attributes and these attributes are nested. Trust me, you won’t heard a word against it.

If you see some misunderstaning, error, mistake, grammar mistake or just have a question then I would be more than pleased if you would write a comment below. Thank you.


Join the Newsletter

Name
E-Mail