上一篇文章主要介绍了 redux 文档里所用到的基本优化方案,但是很多都是手工实现的,不够自动化。这篇文章主要讲的是怎么用 redux-toolkit 组织 redux 代码。
先来回顾一下,我们所用到除 JS 之外的有:
- react-redux
- Provider 组件
- useSelector
- useDispatch'
- redux
- createStore
- combineReducers
- applyMiddleware
- redux-thunk
最终得到的代码大概如下(因为篇幅有限,就只显示其中一部分,详细代码可以看这里)
todos/store.ts
代码语言:javascript复制// todos/store.ts
import ...
const reducer = combineReducers({
todos: todosReducer,
filter: filterReducer,
loading: loadingReducer
})
const enhancer = process.env.NODE_ENV === 'development' ? composeWithDevTools(
applyMiddleware(ReduxThunk)
) :applyMiddleware(ReduxThunk)
const store = createStore(reducer, enhancer)
export default store
todos/reducer.ts
代码语言:javascript复制// todos/reducer.ts
import ...
type THandlerMapper = {[key: string]: (todoState: TTodoStore, action: TTodoAction) => TTodoStore}
const initTodos: TTodoStore = {
ids: [],
entities: {}
}
const todosReducer = (todoState: TTodoStore = initTodos, action: any) => {
const handlerMapper: THandlerMapper = {
[SET_TODOS]: (todoState, action) => {
const {payload: todos} = action as TSetTodosAction
const entities = produce<TTodoEntities>({}, draft => {
todos.forEach(t => {
draft[t.id] = t
})
})
return {
ids: todos.map(t => t.id),
entities
}
},
...
}
const handler = handlerMapper[action.type]
return handler ? handler(todoState, action) : todoState
}
export default todosReducer
todos/selectors.ts
代码语言:javascript复制// todos/selectors
export const selectFilteredTodos = (state: TStore): TTodo[] => {
const todos = Object.values(state.todos.entities)
if (state.filter === 'all') {
return todos
}
return todos.filter(todo => todo.state === state.filter)
}
export const selectTodoNeeded = (state: TStore): number => {
return Object.values(state.todos.entities).filter(todo => todo.state === 'todo').length
}
todos/actionCreators.ts
代码语言:javascript复制// todos/actionCreators.ts
export const fetchTodos = () => async (dispatch: Dispatch) => {
dispatch(setLoading({status: true, tip: '加载中...'}))
const response: TTodo = await fetch('/fetchTodos', () => dbTodos)
dispatch({ type: SET_TODOS, payload: response })
dispatch(setLoading({status: false, tip: ''}))
}
todos/actionTypes.ts
代码语言:javascript复制// todos/actionTypes.ts
export const SET_TODOS = 'setTodos'
export type SET_TODOS = typeof SET_TODOS
以前的做法
- 手动配置常用中间件和 Chrome 的 dev tool
- 手动将 slice 分类,并暴露 reducer
- 手动 Normalization: 将 todos 数据结构变成
{ids: [], entities: {}}
结构 - 使用 redux-thunk 来做异步,手动返回函数
- 手动使用表驱动来替换 reducer 的 switch-case 模式
- 手动将 selector 进行封装成函数
- 手动引入 immer,并使用 mutable 写法
以前的写法理解起来真的不难,因为这种做法是非常纯粹的,基本就是 JavaScript 。不过,带来的问题就是每次都这么写,累不累?
因此这里隆重介绍 redux 一直在推荐的 redux-toolkit,这是官方提供的一揽子工具,这些工具并不能带来很多功能,只是将上面的手动档都变成自动档了。
安装:
代码语言:javascript复制$ yarn add @reduxjs/toolkit
configureStore
最重要的 API 就是 configureStore 了:
代码语言:javascript复制// store.ts
const reducer = combineReducers({
todos: todosSlice.reducer,
filter: filterSlice.reducer,
loading: loadingSlice.reducer
})
const store = configureStore({
reducer,
devTools: true
})
可以和之前的 createStore 对比一下,configureStore 带来的好处是直接内置了 redux-thunk 和 redux-devtools-extension,这个 devtools 只要将 devTools: true
就可以直接使用。两个字:简洁。
createSlice
上面的代码我们看到是用 combineReducers
来组装大 reducer 的,前文也说过 todos, filter, loading 其实都是各自的 slice,redux-toolkit 提供了 createSlice
来更方便创建 reducer:
// todos/slice.ts
const todosSlice = createSlice({
name: 'todos',
initialState: initTodos,
reducers: {
[SET_TODOS]: (todoState, action) => {
const {payload: todos} = action
const entities = produce<TTodoEntities>({}, draft => {
todos.forEach(t => {
draft[t.id] = t
})
})
return {
ids: todos.map(t => t.id),
entities
}
}
...
}
})
这里其实会发现 reducers 字段里面就是我们所用的表驱动呀。name 就相当于 namespace 了。
异步
之前我们用 redux-thunk 都是 action creator 返回函数的方式来写代码,redux-toolkit 提供一个 createAsyncThunk
直接可以创建 thunk(其实就是返回函数的 action creator,MD,不知道起这么多名字干啥),直接看代码
// todos/actionCreators.ts
import loadingSlice from '../loading/slice'
const {setLoading} = loadingSlice.actions
export const fetchTodos = createAsyncThunk<TTodo[]>(
'todos/' FETCH_TODOS,
async (_, {dispatch}) => {
dispatch(setLoading({status: true, tip: '加载中...'}))
const response: TTodo[] = await fetch('/fetchTodos', () => dbTodos)
dispatch(setLoading({status: false, tip: ''}))
return response
}
)
可以发现使用 createSlice 的另一个好处就是可以直接获取 action,不再需要每次都引入常量,不得不说,使用字符串来 dispatch 真的太 low 了。
这其实还没完,我们再来看 todos/slice.ts 又变成什么样子:
代码语言:javascript复制// todos/slice.ts
const todosSlice = createSlice({
name: 'todos',
initialState: initTodos,
reducers: {},
extraReducers: {
[fetchTodos.fulfilled.toString()]: (state, action) => {
const {payload: todos} = action as TSetTodosAction
const entities = produce<TTodoEntities>({}, draft => {
todos.forEach(t => {
draft[t.id] = t
})
})
state.ids = todos.map(t => t.id)
state.entities = entities
}
}
})
这里我们发现,key 变成了 fetchTodos.fulfilled.toString()
了,这就不需要每次都要创建一堆常量。直接使用字符串来 dispatch 是非常容易出错的,而且对 TS 非常不友好。
注意:createSlice 里的 reducer 里可以直接写 mutable 语法,这里其实是内置了 immer。
我们再来看组件是怎么 dispatch 的:
代码语言:javascript复制// TodosApp.tsx
import {fetchTodos} from './store/todos/actionCreators'
const TodoApp: FC = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchTodos())
}, [dispatch])
...
}
其实还是和以前一样,直接 dispatch(actionCreator())
函数完事。
builder
其实到这里我们对 [fetchTodos.fulfilled.toString()]
的写法还是不满意,为啥要搞个 toString()
出来?真丑。这里主要因为不 toString()
会报 TS 类型错误,官方的推荐写法是这样的:
// todos/slice.ts
const todosSlice = createSlice({
name: 'todos',
initialState: initTodos,
reducers: {},
extraReducers: builder => {
builder.addCase(fetchTodos.fulfilled, (state, action) => {
const {payload: todos} = action as TSetTodosAction
const entities = produce<TTodoEntities>({}, draft => {
todos.forEach(t => {
draft[t.id] = t
})
})
state.ids = todos.map(t => t.id)
state.entities = entities
})
builder.addCase...
})
使用 builder.addCase 来添加 extraReducer 的 case,这种做法仅仅是为了 TS 服务的,所以你喜欢之前的 toString 写法也是没问题的。
Normalization
之前我们使用的 Normalization 是需要我们自己去造 {ids: [], entities: {}}
的格式的,无论增,删,改,查,最终还是要变成这样的格式,这样的手工代码写得不好看,而且容易把自己累死,所以 redux-toolkit 提供了一个 createEntitiyAdapter 的函数来封装这个 Normalization 的思路。
// todos/slice.ts
const todosAdapter = createEntityAdapter<TTodo>({
selectId: todo => todo.id,
sortComparer: (aTodo, bTodo) => aTodo.id.localeCompare(bTodo.id), // 对 ids 数组排序
})
const todosSlice = createSlice({
name: 'todos',
initialState: todosAdapter.getInitialState(),
reducers: {},
extraReducers: builder => {
builder.addCase(fetchTodos.fulfilled, (state, action: TSetTodosAction) => {
todosAdapter.setAll(state, action.payload);
})
...
builder.addCase(toggleTodo.fulfilled, (state, action: TToggleTodoAction) => {
const {payload: id} = action as TToggleTodoAction
const todo = state.entities[id]
todo!.state = todo!.state === 'todo' ? 'done' : 'todo'
})
}
})
创建出来的 todosAdapter
就厉害了,它除了上面的 setAll
还有 updateOne
, upsertOne
, removeOne
等等的方法,这些 API 用起来就和用 Sequlize 这个库来操作数据库没什么区别,不足的地方是 payload 一定要按照它规定的格式,如 updateOne 的 payload 类型就得这样的
export declare type Update<T> = {
id: EntityId;
changes: Partial<T>;
};
这时 TS 的强大威力就体现出来了,只要你去看里面的 typing.d.ts,使用这些 API 就跟切菜一样简单,还要这个