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 核心createStore
API,并自动为我们处理大部分存储设置。事实上,我们可以有效地将其缩减为一步:
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 有一个createSlice
API 可以帮助我们简化我们的 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 creatorscreateSlice 在默认情况下自动返回现有状态
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.pending
:todos/fetchTodos/pending
fetchTodos.fulfilled
:todos/fetchTodos/fulfilled
fetchTodos.rejected
:todos/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
什么必须是纯函数?
先告诉你结果吧,如果在reducer中,在原来的state
上进行操作,并返回的话,并不会让React重新渲染。 完全不会有任何变化!
接下来看下Redux的源码:
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
复制代码复制代码
我不要进行深度比较,只是浅比较,引用值不一样(不是同一个对象),那就是不一样的。 这就是redux
的reducer
如此设计的原因了(最后一句不太赞成,浅克隆本质还是指向堆内存中同一地址,所以redux返回的新对象必须是深克隆的,由于深克隆太耗费性能,才有了react-immutable等数据持久化库的出现,为什么一般使用Object.assign()返回新的state呢?因为当state只有1层,即内部不包含数组或对象时,它就是深克隆)