preface

To be honest, the state management solutions of the three front-end frameworks (ReDUx, VUex, and Service + RXJS) are without a doubt the worst in terms of development experience, which has led to a lot of redux improvements

However, they are the wheels of other people’s homes. Out of the attitude of being responsible for their own products, we do not dare to use them at will. We still try to use the original Redux, after all, Redux has been tested by numerous products, and its reliability can be guaranteed

Instead of building a wheel from scratch, we could just add a few dozen lines of code and tweak Redux a little to improve the development experience at the lowest cost

Of course, these optimizations may seem a little crude to you or not be your pain point

But that doesn’t matter, this article just provides a feasible way to optimize Redux at a minimum cost

Don’t be afraid to experiment, and don’t be afraid to tinker with code to change a design that doesn’t make sense to you

The body of the

It is officially recommended that action and Reducer be placed in different files, but this results in cumbersome file switching

According to the general file classification, we have action and Reducer files

Action and Reducer obviously correspond one by one and there is no need to divide them into different files

Take the 🌰 child of a counter

The file directory structure is as follows

  • store
    • action.js
    • reducer.js
// action.js
export const ADD ='add'

//reducer.js
const counter ={ count: 0 }

export default function reducer(state = counter, action) {
  switch (action.type) {
    case 'ADD':
      return {
        ...state,
        count:state.count+1
      }
      break;

    default:
      return state
      break; }}// Component use
import {ADD} from './action.js'

store.dispatch(ADD)
Copy the code

When we want to look at the component logic store.dispatch(ADD), we need to

  1. Open the Action file and search for the constant ADD
  2. Open the Reducer file and search for the constant ADD

There are two file switching operations here, which are completely unnecessary in my opinion. The above actions and reducer are essentially to change the same state (counter object). Since they are to change the same state, why not put them together with the state they want to change?

We can do this by creating a file with the name of the state counter that we need to change and putting the corresponding actions and reducer into it

The file directory structure is as follows

  • store
    • counter.js

Counter. Js is as follows

const initialState = {
  count: 10
}

function reducer(state = counter, action) {
  switch (action.type) {
    case 'add':
      return {
        ...state,
        count:state.count+1
      }
      break;

    default:
      return state
      break; }}Copy the code

Reducer switch writing is cumbersome

When reducer is large, switch is cumbersome

We can write a tool method to convert the Reducer Switch style to the object style and the action to the property name of the object

// Tool methods
const handleActions = ({ state, action, reducers}) = >
  Object.keys(reducers)
    .includes(action.type)
    ? reducers[action.type](state,action)
    : state


//counter.js
const initialState = {
  count: 10
}

const reducers = {
  add(state, action) {
    return{
      ...state,
      count:state.count+1
    }
  },
  minus(state, action) {
    return{
      ...state,
      count:state.count- 1}}},export default (state = initialState, action) => handleActions({
  state,
  action,
  reducers,
})

Copy the code

Reducer must be a pure function and must return new references. When the object level is deep, it is cumbersome to write

We can optimize this by introducing immer, a small library of immutable data structures

The first step is to introduce immer

import produce from "immer"
Copy the code

The second step is to modify the handleActions tool function

export const handleActions = ({ state, action, reducers}) = >
  Object.keys(reducers)
    .includes(action.type)
    ? produce(state, draft => reducers[action.type](draft, action))
    : state

// Add this line
produce(state, draft => reducers[action.type](draft, action))
Copy the code

And then we just say reducer

  1. You do not need to manually return each time
  2. There is no need to manually generate a new reference

The following

/ / before modification
const reducers = {
  add(state, action) {
    return{
      ...state,
      count:state.count+1
    }
  },
  minus(state, action) {
    return{
      ...state,
      count:state.count- 1}}},/ / after transforming
const reducers = {
  add(state, action) {
    state.count++
  },
  minus(state, action) {
    state.count--
  },
}
Copy the code

Just to explain a little bit about our new line of code

produce(state, draft => reducers[action.type](draft, action))
Copy the code

This involves the use of immer

The first argument to produce is the object you want to manipulate, in this case, operation state

The second argument is a function that immer passes as an argument a copy of the object that you operate on without affecting the original object

So the function we wrote in the reducers object is equivalent to writing the second parameter of Produce, and in the handleActions utility function, we have also returned through the ternary expression, This allows you to implement the reducers object without having to manually return functions

Adding a namespace

When the project gets big, we may have naming conflicts in the actions we write. The solution is to use the current file name as the namespace

For example, we have the following directory structure

  • store
    • counter.js

One day we will need to add a todo-list module

  • store
    • counter.js
    • todoList.js

To prevent naming conflicts between counter and actions in todoList, we can modify the handleActions tool function

import produce from "immer"

const getKey = (str, flag) = > {
  const i = str.indexOf(flag)
  return str.substring(i + 1, str.length + 1)}export const handleActions = ({ state, action, reducers, namespace = ' ' }) = >
  Object.keys(reducers)
    .map(key= > namespace + '/' + key)
    .includes(action.type)
    ? produce(state, draft => reducers[getKey(action.type, '/')](draft, action))
    : state

export default (state = initialState, action) => handleActions({
  namespace: 'counter'.// Add a namespace
  state,
  action,
  reducers,
})
Copy the code

Components used like this

store.dispatch('counter/add')// Add method for counter module
store.dispatch('todoList/add')//todoList module add method
Copy the code

The sample code

Points to be optimized

  • Actions are based on strings, the editor can’t do smart hints and are prone to spelling errors
  • Support for ts

To be continued…