For some reason, the technology stack was switched from Vue to React, and the React family bucket that I hadn’t seen in a year was picked up.

React has officially supported Chinese documentation, Router concepts and APIS are less, but it is still easy to understand. As for Redux, WHEN I learned React before, I felt the difficulty and complexity of Redux. Moreover, when I first touched Vuex and then Redux, I felt the complex and abstract configuration of Redux. Most Of the Chinese documents were translated by myself. I will study again through the English document.

Unidirectional data flow

First look at the two official documents Redux Vuex for one-way data flow description, can be said to be very similar, even with the same picture 😅

Both Redux and Vuex are state management tools designed to maintain a single data flow when multiple components share state.

Comparison of core concepts

Vuex

When Vuex first came out, Yuda also said that Vuex absorbed many parts of Redux, and simplified many concepts and characteristics of Redux.

Vuex is actually a Flux specialized for Vue, mainly to match Vue’s own responsive mechanism. Of course, some features of Redux are adopted, such as single-state trees and apis for easy testing and hot reloading, but some features that are not suitable for Vue are also selectively abandoned. What are the main differences between Vuex and Redux? What are the advantages and disadvantages of each? – especially the rain creek

Vuex is an officially maintained, Vue dedicated state management tool that takes a lot from Redux and is highly integrated with Vue. Compared to Redux, Vuex abandoned the concept of action and added a method to asynchronously modify state, named action

  • State: a single state tree where modular modules are stored on the same store instance
  • Getter: New data derived from the state data and cached before the corresponding dependent data changes
  • Mutation: The only way to change state. Multiple Mutaions are available, only synchronization is supported, and the original state is updated directly in mutation, with Vuex notifying the update
  • Action: Can call mutation asynchronously and throw promise to continue the chain operation in the business layer

Redux

Redux is a common state management tool in JS, and Redux officially maintains a react-redux interface between Rereact and Redux.

  • State: There is only one store instance in an application and the data is immutable
  • Action: an abstract concept used to describe changes in state (It’s just a normal JS object)
  • Reducer: the reducer is the only method to change state. It must be a pure function. Determine the fields corresponding to action. Redux advocates immutable data. The reducer returns a new state each time

Simple summary

In my opinion, the reason why Redux is much more difficult than Vuex lies in the two concepts of Action and Reducer. Action, as a concept describing state changes, is only an object with {type,payload}. There e is only one reducer that can change state operations. You need to switch the same Reducer based on the action.

On the asynchronous side, Vuex directly provides the concept of action, the API is designed in a way that I think is straightforward, and Redux needs to be reinforced with middleware such as Redux-Thunk.

For a VUer, Redux is a Vuex that has only one mutationReducer and sends mutations to the only mutationReducer.

For reacter, Vuex means that there are multiple reducermutations, each of which contains the Redux of the corresponding action, When changing state, you only need the reducermutation corresponding to DispatchCommit.

Create a comparison of store methods

Vuex

import { createStore } from 'vuex'

const store = createStore({
  state: {
    count: 1.user: null
  },
  mutations: {
    changeCount(state, count) {
      state.count = count
    },
    changeUser(state, user) {
      state.user = user
    }
  },
  actions: {
    login({ commit, state }, form) {
      return ajax.login(form).then(res= > {
        commit('changeUser', res)
        return res
      })
    }
  }
})
Copy the code

Mutations can also use the same format as Redux.

mutations: {
  increment (state, action) {
    state.count += action.payload
  }
}
Copy the code

Initialization of Vuex is relatively simple. State stores data, mutations synchronously modify state, and actions asynchronously commit calls mutation. In mutation, state is also the response of inheritance Vue, and the original state can be modified. The fact that mutation is called as a string in COMMIT also makes Vuex unfriendly to TS and IDE jumps.

Redux

import { createStore } from 'redux'

const state = {
  color: 'red'.count: 1
}

const reducer = (state: State, { type, payload }) = > {
  switch (type) {
    case 'x':
      // ...
      return { ...state, x: '... ' }
    case 'y':
      // ...
      return { ...state, y: '... ' }
    case 'z':
      // ...
      return { ...state, z: '... ' }
    // ...}}const store = createStore(reducer, state, middleware)
Copy the code

One of the simplest Redux instances is to combine reducer and state using the createStore. Since data is immutable in Redux, reducer, as a pure function, needs to return a new state object to replace the original state. Redux action is an object that tells the Reducer how to operate the store. In the code, the action is an object {type, payload}. The reducer determines the type of the action and then changes the state accordingly.

Because this layer of action, may let a lot of people in the introduction of Redux when difficult to understand, but also may produce many and Redux thought different writing, such as as I directly Redux as localStorage to use. The action object {type, payload} is passed as key: value. The Redux only has three concepts: state, getter, and setter 😅

const reducer = (state, { type, payload }) = > {
  // Return the old state and set state.type to payload
  return { ...state, [type]: payload }
}

dispatch({ type: 'count'.payload: count + 1 })
Copy the code

Comparison of business layer usage

Vuex

<template>
  <h1>{{ $store.state.count }}</h1>
</template>

<script>
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()

    function add() {
      store.commit('changeCount', store.state.count + 666)
    }

    function reset() {
      store.dispatch('login').then(() => {
        // ...
      })
    }

    return { add, reset }
  }
}
</script>
Copy the code

Vue mounts all components and plug-ins to the same Vue instance, so this points to a unique instance of all components. Vuex can get the corresponding state directly from this, and then commit it. Action to call the corresponding mutation to modify the state. Because of this global this, Vue in TS is all kinds of unfriendly.

The commit argument can also be in the same format as the Redux action.

this.$store.commit({
  type: 'changeCount'.payload: this.$store.state.count + Awesome!
})
Copy the code

React Hooks

Starting with V7.1.0, react-Redux added support for Hooks, useSelector to get specific data from state, useDispatch to pass actions, and changes to state.

import { useSelector, useDispatch } from 'react-redux'
import type { State } from '@/store'
import { Button } from 'antd'

export default function ReduxA () {
  const count = useSelector<State, number> (state= > state.count)

  const dispatch = useDispatch()

  function add () {
    dispatch({ type: 'count'.payload: count + 1})}return <>
    <h1>Redux A</h1>
    <div>count:{count}</div>
    <Button onClick={add}>add</Button>
  </>
}
Copy the code

We can see that the type marking of useSelector in TS is slightly tedious. Each file needs to introduce the type of state and manually mark the type of the value taken out. You can encapsulate a layer of hooks by yourself, making it more pleasant to use TS.

import { useSelector } from 'react-redux'

export function useMySelector<T = any> (fn: (state: State) => T) {
  return useSelector<State, T>(fn)
}
Copy the code

After encapsulation, you can get the specific state type in parentheses, and you can infer the specific data type from the state type.

React HOC

import { Button } from 'antd'
import { connect, DispatchProp } from 'react-redux'
import type { State } from '@/store'

function ReduxB (props: { count: number } & DispatchProp) {
  function minus () {
    props.dispatch({ type: 'count'.payload: props.count - 1})}return <>
    <h1>Redux B</h1>
    <div>count:{props.count}</div>
    <Button onClick={minus}>minus</Button>
  </>
}

export default connect((state: State) = > {
  return { count: state.count }
})(ReduxB)
Copy the code

React is used in the business layer, mainly through the connect package or useSelector to pass specific state values. To modify the state, you can send the corresponding action to the Dispatch to notify the Reducer to modify the state.

Simple summary

When used in specific businesses, the difference between Redux and Vuex can be said to be the difference between React and Vue. React obtains state and modifies state by hook or HOC. Vue uses this to obtain the method of obtaining state and modifying state. In the setup of Vue 3, it can also obtain the same store object as this through hook.

Since Vuex is a state management tool that is specialized for Vue, it can be injected into the root instance of Vue in the form of a global plug-in, making the Store available in this for all components.

Redux is a simple JS state management tool. To use it in React, you need the react-Redux plug-in, which wraps a layer of Provider labels on top of it, and introduces the method of obtaining store separately in each component.

In the business layer, when notifies the Store of state changes, Vuex and Redux do so by means of strings or strings in objects. As a result, the IDE can hardly infer and jump to the corresponding modification logic, which is undoubtedly a pain point for subsequent personnel maintenance.

asynchronous

Vuex

Asynchrony in Vuex is as simple as setting up a function in the Action to commit the corresponding mutation, and then changing state asynchronously in the business through Dispatch, returning a Promise for a chain callback.

// store.js
const actions = {
  login({ commit }, form) {
    return ajax.login(form).then(res= > {
      commit('changeUser', res)
      return res
    })
  }
}
// Playground.vue
function fn() {
  this.$store.dispatch('login', form).then(res= > {
    / /...})}Copy the code

Redux

Redux does not come with asynchronous operations and requires some middleware, such as the official Redux-Thunk.

import ThunkMiddleware from 'redux-thunk'

const store = configureStore({
  reducer: slice.reducer,
  middleware: [ThunkMiddleware]
})

function asyncFn(form) {
  return function(dispatch) {
    return ajax.login(form).then(res= > {
      dispatch(changeUser(res))
      return res
    })
  }
}
Copy the code

Redux passed in to redux-Thunk in the middleware can use asynchronous operations in the business layer, and the defined asyncFn is passed in to Dispatch, which can continue the chain operation after dispatch.

const dispatch = useDispatch()
dispatch(asyncFn(form)).then(res= > {
  / /...
})
Copy the code

In JS, you can then do all sorts of nice asynchrony, but in TS this asynchrony leads to type confusion, and then does not exist on Dispatch.

You need to do some type gymnastics and repackage the useDispatch hook function.

import { useDispatch } from 'react-redux'
import { ThunkDispatch } from 'redux-thunk'
import { Action } from 'redux'

export function useMyDispatch() {
  return useDispatch<ThunkDispatch<State, void, Action>>()
}
Copy the code

This gives you perfect ts support, type constraints on asyncFn passed to Dispatch, and the ability to determine whether asynchronous chained calls are allowed by parameters passed to Dispatch. It also helps you infer the type of res in.then.

Simple summary

The Vuex asynchronous operation is provided by the official. Different from the synchronous operation COMMIT, the use of Dispatch can be more intuitive to see whether it is synchronous or asynchronous. The asynchronous operation of Redux needs to be implemented by the middleware, and both synchronous and asynchronous are called with the same Dispatch.

All Vuex requires is to perform an asynchronous operation in an asynchronous action and call COMMIT. The Redux needs to pass a method, a new method into dispatch, a little nesting doll.

Both Vuex and Redux support dispach to continue with a Promise by returning a Promise after the asynchronous operation has finished.

Redux Toolkit

Perhaps the concept and process of Redux are quite complicated for most people, but Redux has officially launched the Redux Toolkit.

The Redux Toolkit package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux:

  • “Configuring a Redux store is too complicated”
  • “I have to add a lot of packages to get Redux to do anything useful”
  • “Redux requires too much boilerplate code”

As you can see, Redux officially intends to promote the Redux Toolkit as a Redux best practice, simplifying many Redux operations and simplifying some overly complex concepts. In the React Redux documentation, all tutorials are taught using the Redux Toolkit. Even in the Redux documentation, tutorials are taught using the Redux Toolkit. CreateStore, combineReducers, and applyMiddleware are all API references for those who want to go further.

The official sample

First look at the official Redux Toolkit example, which shows one of the most important apis

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter'.initialState: {
    value: 0
  },
  reducers: {
    increment: state= > {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: state= > {
      state.value -= 1
    },
    incrementByAmount: (state, action) = > {
      state.value += action.payload
    }
  }
})
Copy the code

It is obvious that reducers in createSlice not only does not have a switch, but also directly changes the value of state. This is one of the most obvious changes in the Redux Toolkit. Instead of Redux’s original action concept, action functions are merged with the reducer’s original functions, and state can be directly modified in the Reducer. Operations to keep data immutable within the Redux Toolkit.

The establishment of the store

Let’s look at a detailed store creation

import { configureStore, createSlice } from '@reduxjs/toolkit'

const state = {
  color: 'red'.count: 1.arr: [] as string[]}const slice = createSlice({
  name: 'default'.initialState: state,
  reducers: {
    changeCount(state, action: { payload: number }) {
      state.count = action.payload
    },
    changeArr(state) {
      state.arr.push('16')}}})export const { changeCount, changeArr } = slice.actions

// configureStore can be used as a store in the top-level Provider tag
export default configureStore({
  reducer: slice.reducer,
  // Reducer can be combined with multiple slices
  // reducer: {
  // user: userSlice.reducer,
  // modal: modalSlice.reducer,
  // theme: themeSlice.reducer,
  / /...
  // },
  middleware,
  devTools: true
})
Copy the code

As can be seen from the changeCount method in the Reducer, the concept of action has not been completely removed. The Reducer still needs to receive specific payload values through the action to assign the state value. The type field is no longer used.

Actions export const {changeCount, changeArr} = slice.actions

Use of the business layer

In the business layer, we still need a concept to describe state changes. This concept is the action method exported from slicing. Actions with the same name as reducer

import { useDispatch, useSelector } from 'react-redux'
import { changeCount } from '@/store'
import type { State } from '@/store'
import { Button } from 'antd'

export default function ReduxA() {
  const count = useSelector(state= > state.count)
  const dispatch = useDispatch()

  function add() {
    dispatch(changeCount(count + 1))
    console.log(changeCount(count + 1))
    // Obj = obj
    // dispatch({ type: 'default/changeCount', payload: count + 1 })
  }

  return <>
    <div>count:{count}</div>
    <Button onClick={add}>add</Button>
  </>
}
Copy the code

You can see that the methods exported from slicing. Actions are now received through Dispatch, just as asynchronous operations like Thunk are used.


Log. console (changeCount(count + 1))) Return {type, payload}. ChangeCount (count + 1) {type: ‘default/changeCount’, payload: count + 1}

Changing the object to slicing. Actions is not an unnecessary step, but it enhances the weak jump feature in both Vuex and Redux, and greatly improves the ease of troubleshooting and tracing problems in Redux. In Vuex and Redux before, the business layer of commit and Dispatch uses strings to describe specific operations, which means the IDE cannot analyze and jump to specific operations, and later maintenance uses the original global search human jump. The fastest way to jump to the reducer is to use the slicing. Actions export method twice, and the payload type can be better restricted in TS.

asynchronous

Asynchron in Redux Toolkit is more complicated than using middleware in my experience 😂 just post an example of the official document.

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

// First, create the thunk
const fetchUserById = createAsyncThunk('users/fetchByIdStatus'.async (userId, thunkAPI) => {
  const response = await userAPI.fetchById(userId)
  return response.data
})

// Then, handle actions in your reducers:
const usersSlice = createSlice({
  name: 'users'.initialState: { entities: [].loading: 'idle' },
  reducers: {
    // standard reducer logic, with auto-generated action types per reducer
  },
  extraReducers: builder= > {
    // Add reducers for additional action types here, and handle loading state as needed
    builder.addCase(fetchUserById.fulfilled, (state, action) = > {
      // Add user to the state array
      state.entities.push(action.payload)
    })
  }
})

// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
Copy the code

ExtraReducers can be reduced to an object

extraReducers: {
    [fetchUserById.padding]: (state, action) = > {},
    [fetchUserById.fulfilled]: (state, action) = > {},
    [fetchUserById.rejected]: (state, action) = >{}}Copy the code

The final summary

At the store level, the Redux Toolkit removes the concept of action and converts the concepts of switch and case from one reducer to multiple reducers. In addition, variable data is written in the reducer.

In my opinion, these changes make the best practices of Redux very similar to Vuex. The same state, reducer and mutation are written with variable data, with only subtle differences between API names. For newcomers, they no longer need to feel dizzy about switches in Action and Reducer, which greatly reduces the understanding and entry threshold of Redux. With the Vuer, you can quickly and seamlessly switch to Redux after using the mutable data writing method, simply by memorizing the new API.

The biggest improvement of The Redux Toolkit is to replace the original action objects with methods, greatly improving the jump to definitions.

Redux and Redux Toolkit still have higher scalability than Vuex, although the Redux Toolkit package makes the use of Redux more similar to Vuex and easier to use. In the face of complex business, Redux and the Redux Toolkit still provide a lot of apis for users to customize and extend, and the community has a lot of middleware to compose itself. Unlike Vuex, giegie has only basic documented uses and a relatively simple plugin hook. Of course, for most people, a standard Vuex or a createSlice is sufficient for almost all scenarios.

There is an old saying about state management: You Might Not Need Redux. However, given that the default job hunting environment is to be skilled in using the whole family bucket, Redux Toolkit and Vuex are still somewhat able to lower the threshold and provide convenience for getting started.