© Bohua Xu

react_toolkit

Jul 16, 2021 · 17min

npm install @reduxjs/toolkit
npm install @reduxjs/toolkit

(redux不应该被过度的使用)配合useContext和useReducer可以实现简单的管理

(要么是mobx这种和Vue一样的proxy代理,要么是redux这种和React一样的更改引用。就是这两派吧,也就是前端两大派系了。useReducer是复杂状态管理,useContext是绑定上下文。Redux也是做了这两件事,复杂状态管理和全局分发。useReducer+useContext其实就是Redux,虽然不一定好用,但是是可以替代Redux的,当然把useReducer换成useState也无妨。)

使用起来也很方便,配合useDispath,useSelector,还可以和ts一起使用。

!内置immer库,(redux之前的设计是浅比较与reducer设计的一样,因为深比较特别耗性能,toolkit使用immer可以不用在写纯函数的形式,非常的方便)

{Immer 使用一种称为 a 的特殊 JS 工具Proxy来包装您提供的数据,并允许您编写代码来“更改”该包装的数据。但是,Immer 会跟踪您尝试进行的所有更改,然后使用该更改列表返回一个安全不可变更新的值,就像您手动编写了所有不可变更新逻辑一样。}

之前的写法:

import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'


const rootReducer = combineReducers({
  // Define a top-level state field named `todos`, handled by `todosReducer`
  todos: todosReducer,
  filters: filtersReducer
})
const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))

const store = createStore(rootReducer, composedEnhancer)
export default store

import { combineReducers } from 'redux'
import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'


const rootReducer = combineReducers({
  // Define a top-level state field named `todos`, handled by `todosReducer`
  todos: todosReducer,
  filters: filtersReducer
})
const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware))

const store = createStore(rootReducer, composedEnhancer)
export default store

非常的繁琐,在类式react中使用connect,hook中配合配合useDispath,useSelector。(hook也可以使用connect(不建议))

mobx和redux和zustand.js 都可以使用,具体看公司需要,mobx和vuex的原理类似(现在的状态管理库 复杂状态管理 + 全局分发,useReducer + useContext干了这两件事情。hook到页面更新,性能,都是还是和框架本身有关,和状态管理库无关。状态管理库之所以那么多,变的都是复杂状态管理的方式,因为前端有重网络请求的业务,我可以在中间套一个异步中间件,或者重用户操作的业务。重网络请求的业务,甚至你用useSWR useQuery + graphQL ,状态管理交给后端都是可以的)

configureStore

configureStore包装 Redux 核心createStoreAPI,并自动为我们处理大部分存储设置。事实上,我们可以有效地将其缩减为一步:

import { configureStore } from '@reduxjs/toolkit'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const store = configureStore({
  reducer: {
    // Define a top-level state field named `todos`, handled by `todosReducer`
    todos: todosReducer,
    filters: filtersReducer
  }
})

export default store
import { configureStore } from '@reduxjs/toolkit'

import todosReducer from './features/todos/todosSlice'
import filtersReducer from './features/filters/filtersSlice'

const store = configureStore({
  reducer: {
    // Define a top-level state field named `todos`, handled by `todosReducer`
    todos: todosReducer,
    filters: filtersReducer
  }
})

export default store

一个configureStore为我们完成了所有工作:

可以删去之前需要导入的thunk,toolkit帮我们集成了这些

npm uninstall redux redux-thunk reselect
npm uninstall redux redux-thunk reselect

Writing Slices

Using createSlice

Redux Toolkit 有一个createSliceAPI 可以帮助我们简化我们的 Redux reducer 逻辑和操作createSlice为我们做了几件重要的事情:

  • 我们可以将 case reducer 编写为对象内部的函数,而不必编写switch/case语句
  • reducer 将能够编写更短的不可变更新逻辑
  • 所有的动作创建者都将根据我们提供的 reducer 函数自动生成

使用createSlice

createSlice接受一个具有三个主要选项字段的对象:

  • name: 一个字符串,将用作生成的操作类型的前缀
  • initialState:reducer的初始状态
  • reducers: 一个对象,其中键是字符串,值是处理特定操作的“大小写缩减器”函数

创建切片示例

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  entities: [],
  status: null
}

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {             //!!!!加s---reducers
    todoAdded(state, action) {
      // ✅ This "mutating" code is okay inside of createSlice!
      state.entities.push(action.payload)
    },
    todoToggled(state, action) {
      const todo = state.entities.find(todo => todo.id === action.payload)
      todo.completed = !todo.completed
    },
    todosLoading(state, action) {
      return {
        ...state,
        status: 'loading'
      }
    }
  }
})

export const { todoAdded, todoToggled, todosLoading } = todosSlice.actions

export default todosSlice.reducer           //!!!一定要.reducer
import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  entities: [],
  status: null
}

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {             //!!!!加s---reducers
    todoAdded(state, action) {
      // ✅ This "mutating" code is okay inside of createSlice!
      state.entities.push(action.payload)
    },
    todoToggled(state, action) {
      const todo = state.entities.find(todo => todo.id === action.payload)
      todo.completed = !todo.completed
    },
    todosLoading(state, action) {
      return {
        ...state,
        status: 'loading'
      }
    }
  }
})

export const { todoAdded, todoToggled, todosLoading } = todosSlice.actions

export default todosSlice.reducer           //!!!一定要.reducer
  • 我们在对象内部编写 case reducer 函数reducers,并赋予它们可读的名称

  • createSlice将自动生成对应于我们提供的每个 case reducer 函数的action creators

  • createSlice 在默认情况下自动返回现有状态

  • createSlice允许我们安全地“变异”我们的状态!

  • 但是,如果我们愿意,我们也可以像以前一样制作不可变的副本

    Using createAsyncThunk

fetchTodos让我们通过生成一个 thunk 来替换我们的 thunk createAsyncThunk

createAsyncThunk接受两个参数:

  • 将用作生成的操作类型的前缀的字符串
  • 一个“有效负载创建者”回调函数,应该返回一个Promise. 这通常使用async/await语法编写,因为async函数会自动返回一个 Promise。
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// omit imports and state

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
  const response = await client.get('/fakeApi/todos')
  return response.todos
})

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    // omit reducer cases
  },
  extraReducers: builder => {
    builder
      .addCase(fetchTodos.pending, (state, action) => {
        state.status = 'loading'
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        const newEntities = {}
        action.payload.forEach(todo => {
          newEntities[todo.id] = todo
        })
        state.entities = newEntities
        state.status = 'idle'
      })
  }
})

// omit exports
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// omit imports and state

export const fetchTodos = createAsyncThunk('todos/fetchTodos', async () => {
  const response = await client.get('/fakeApi/todos')
  return response.todos
})

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    // omit reducer cases
  },
  extraReducers: builder => {
    builder
      .addCase(fetchTodos.pending, (state, action) => {
        state.status = 'loading'
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        const newEntities = {}
        action.payload.forEach(todo => {
          newEntities[todo.id] = todo
        })
        state.entities = newEntities
        state.status = 'idle'
      })
  }
})

// omit exports

我们作为字符串前缀传递'todos/fetchTodos',以及一个调用我们的 API 并返回包含获取数据的承诺的“有效负载创建者”函数。在内部,createAsyncThunk将生成三个动作创建者和动作类型,以及一个在调用时自动调度这些动作的 thunk 函数。在这种情况下,动作创建者及其类型是:

  • fetchTodos.pendingtodos/fetchTodos/pending
  • fetchTodos.fulfilledtodos/fetchTodos/fulfilled
  • fetchTodos.rejectedtodos/fetchTodos/rejected

createEntityAdapter

让我们用createEntityAdapter.

调用createEntityAdapter为我们提供了一个“适配器”对象,其中包含几个预制的 reducer 函数,包括:

  • addOne/ addMany:向状态添加新项目
  • upsertOne/ upsertMany: 添加新项目或更新现有项目
  • updateOne/ updateMany:通过提供部分值更新现有项目
  • removeOne/ removeMany: 根据 ID 移除项目
  • setAll:替换所有现有项目

我们可以将这些函数用作 case reducer,或者作为createSlice.

该适配器还包含:

  • getInitialState:返回一个看起来像的对象{ ids: [], entities: {} },用于存储项目的规范化状态以及所有项目 ID 的数组
  • getSelectors:生成一组标准的选择器函数

让我们看看如何在我们的 todos 切片中使用它们:

See documentation for details

img

什么必须是纯函数?

先告诉你结果吧,如果在reducer中,在原来的state上进行操作,并返回的话,并不会让React重新渲染。 完全不会有任何变化!

接下来看下Redux的源码:

img

Redux接收一个给定的state(对象),然后通过循环将state的每一部分传递给每个对应的reducer。如果有发生任何改变,reducer将返回一个新的对象。如果不发生任何变化,reducer将返回旧的state。

Redux只通过比较新旧两个对象的存储位置来比较新旧两个对象是否相同。如果你在reducer内部直接修改旧的state对象的属性值,那么新的state和旧的state将都指向同一个对象。因此Redux认为没有任何改变,返回的state将为旧的state。

好了,也就是说,从源码的角度来讲,redux要求开发者必须让新的state是全新的对象。那么为什么非要这么麻烦开发者呢?

请看下面的例子:尝试比较a和b是否相同

var a = {
    name: 'jack',
    friend: ['sam','xiaoming','cunsi'],
    years: 12,
    ...//省略n项目
}


var b = {
name: 'jack',
friend: ['sam','xiaoming','cunsi'],
years: 13,
...//省略n项目
}
复制代码复制代码var b = {
name: 'jack',
friend: ['sam','xiaoming','cunsi'],
years: 13,
...//省略n项目
}
复制代码
var a = {
    name: 'jack',
    friend: ['sam','xiaoming','cunsi'],
    years: 12,
    ...//省略n项目
}


var b = {
name: 'jack',
friend: ['sam','xiaoming','cunsi'],
years: 13,
...//省略n项目
}
复制代码复制代码var b = {
name: 'jack',
friend: ['sam','xiaoming','cunsi'],
years: 13,
...//省略n项目
}
复制代码

思路是怎样的?我们需要遍历对象,如果对象的属性是数组,还需要进行递归遍历,去看内容是否一致、是否发生了变化。 这带来的性能损耗是非常巨大的。 有没有更好的办法?

有!

//接上面的例子
a === b  //false
复制代码复制代码
//接上面的例子
a === b  //false
复制代码复制代码

我不要进行深度比较,只是浅比较,引用值不一样(不是同一个对象),那就是不一样的。 这就是reduxreducer如此设计的原因了(最后一句不太赞成,浅克隆本质还是指向堆内存中同一地址,所以redux返回的新对象必须是深克隆的,由于深克隆太耗费性能,才有了react-immutable等数据持久化库的出现,为什么一般使用Object.assign()返回新的state呢?因为当state只有1层,即内部不包含数组或对象时,它就是深克隆)

CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu