上一篇文章主要介紹了 redux 文檔里所用到的基本優(yōu)化方案全跨,但是很多都是手工實(shí)現(xiàn)的渺杉,不夠自動(dòng)化是越。這篇文章主要講的是怎么用 redux-toolkit 組織 redux 代碼诵原。
先來(lái)回顧一下绍赛,我們所用到除 JS 之外的有:
- react-redux
- Provider 組件
- useSelector
- useDispatch'
- redux
- createStore
- combineReducers
- applyMiddleware
- redux-thunk
最終得到的代碼大概如下(因?yàn)槠邢蘼鸢觯椭伙@示其中一部分蚯妇,詳細(xì)代碼可以看這里)
todos/store.ts
// 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
// 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
// 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
// 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
// todos/actionTypes.ts
export const SET_TODOS = 'setTodos'
export type SET_TODOS = typeof SET_TODOS
以前的做法
- 手動(dòng)配置常用中間件和 Chrome 的 dev tool
- 手動(dòng)將 slice 分類敷燎,并暴露 reducer
- 手動(dòng) Normalization: 將 todos 數(shù)據(jù)結(jié)構(gòu)變成
{ids: [], entities: {}}
結(jié)構(gòu) - 使用 redux-thunk 來(lái)做異步暂筝,手動(dòng)返回函數(shù)
- 手動(dòng)使用表驅(qū)動(dòng)來(lái)替換 reducer 的 switch-case 模式
- 手動(dòng)將 selector 進(jìn)行封裝成函數(shù)
- 手動(dòng)引入 immer,并使用 mutable 寫法
以前的寫法理解起來(lái)真的不難硬贯,因?yàn)檫@種做法是非常純粹的焕襟,基本就是 JavaScript 。不過(guò)饭豹,帶來(lái)的問(wèn)題就是每次都這么寫鸵赖,累不累?
因此這里隆重介紹 redux 一直在推薦的 redux-toolkit拄衰,這是官方提供的一攬子工具它褪,這些工具并不能帶來(lái)很多功能,只是將上面的手動(dòng)檔都變成自動(dòng)檔了翘悉。
安裝:
$ yarn add @reduxjs/toolkit
configureStore
最重要的 API 就是 configureStore 了:
// store.ts
const reducer = combineReducers({
todos: todosSlice.reducer,
filter: filterSlice.reducer,
loading: loadingSlice.reducer
})
const store = configureStore({
reducer,
devTools: true
})
可以和之前的 createStore 對(duì)比一下,configureStore 帶來(lái)的好處是直接內(nèi)置了 redux-thunk 和 redux-devtools-extension,這個(gè) devtools 只要將 devTools: true
就可以直接使用。兩個(gè)字:簡(jiǎn)潔诫尽。
createSlice
上面的代碼我們看到是用 combineReducers
來(lái)組裝大 reducer 的她按,前文也說(shuō)過(guò) todos, filter, loading 其實(shí)都是各自的 slice,redux-toolkit 提供了 createSlice
來(lái)更方便創(chuàng)建 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
}
}
...
}
})
這里其實(shí)會(huì)發(fā)現(xiàn) reducers 字段里面就是我們所用的表驅(qū)動(dòng)呀屎鳍。name 就相當(dāng)于 namespace 了。
異步
之前我們用 redux-thunk 都是 action creator 返回函數(shù)的方式來(lái)寫代碼巍杈,redux-toolkit 提供一個(gè) createAsyncThunk
直接可以創(chuàng)建 thunk(其實(shí)就是返回函數(shù)的 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
}
)
可以發(fā)現(xiàn)使用 createSlice 的另一個(gè)好處就是可以直接獲取 action箍土,不再需要每次都引入常量沟堡,不得不說(shuō),使用字符串來(lái) dispatch 真的太 low 了。
這其實(shí)還沒(méi)完绢彤,我們?cè)賮?lái)看 todos/slice.ts 又變成什么樣子:
// 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
}
}
})
這里我們發(fā)現(xiàn)有勾,key 變成了 fetchTodos.fulfilled.toString()
了雇逞,這就不需要每次都要?jiǎng)?chuàng)建一堆常量。直接使用字符串來(lái) dispatch 是非常容易出錯(cuò)的挤忙,而且對(duì) TS 非常不友好。
注意:createSlice 里的 reducer 里可以直接寫 mutable 語(yǔ)法淀零,這里其實(shí)是內(nèi)置了 immer。
我們?cè)賮?lái)看組件是怎么 dispatch 的:
// TodosApp.tsx
import {fetchTodos} from './store/todos/actionCreators'
const TodoApp: FC = () => {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchTodos())
}, [dispatch])
...
}
其實(shí)還是和以前一樣膛壹,直接 dispatch(actionCreator())
函數(shù)完事唠亚。
builder
其實(shí)到這里我們對(duì) [fetchTodos.fulfilled.toString()]
的寫法還是不滿意持痰,為啥要搞個(gè) toString()
出來(lái)工窍?真丑淹仑。這里主要因?yàn)椴?toString()
會(huì)報(bào) TS 類型錯(cuò)誤怀吻,官方的推薦寫法是這樣的:
// 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 來(lái)添加 extraReducer 的 case紫皇,這種做法僅僅是為了 TS 服務(wù)的,所以你喜歡之前的 toString 寫法也是沒(méi)問(wèn)題的键兜。
Normalization
之前我們使用的 Normalization 是需要我們自己去造 {ids: [], entities: {}}
的格式的凤类,無(wú)論增,刪蝶押,改踱蠢,查,最終還是要變成這樣的格式棋电,這樣的手工代碼寫得不好看茎截,而且容易把自己累死,所以 redux-toolkit 提供了一個(gè) createEntitiyAdapter 的函數(shù)來(lái)封裝這個(gè) Normalization 的思路赶盔。
// todos/slice.ts
const todosAdapter = createEntityAdapter<TTodo>({
selectId: todo => todo.id,
sortComparer: (aTodo, bTodo) => aTodo.id.localeCompare(bTodo.id), // 對(duì) ids 數(shù)組排序
})
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'
})
}
})
創(chuàng)建出來(lái)的 todosAdapter
就厲害了企锌,它除了上面的 setAll
還有 updateOne
, upsertOne
, removeOne
等等的方法,這些 API 用起來(lái)就和用 Sequlize 這個(gè)庫(kù)來(lái)操作數(shù)據(jù)庫(kù)沒(méi)什么區(qū)別于未,不足的地方是 payload 一定要按照它規(guī)定的格式撕攒,如 updateOne 的 payload 類型就得這樣的
export declare type Update<T> = {
id: EntityId;
changes: Partial<T>;
};
這時(shí) TS 的強(qiáng)大威力就體現(xiàn)出來(lái)了,只要你去看里面的 typing.d.ts烘浦,使用這些 API 就跟切菜一樣簡(jiǎn)單抖坪,還要這個(gè)??皮 redux 文檔有個(gè)??兒用。
createSelector
我們之前雖然封裝好了 selector闷叉,但是只要?jiǎng)e的地方更新使得組件被更新后擦俐,useSelector 就會(huì)被執(zhí)行,而 todos.filter(...) 都會(huì)返回一個(gè)新的數(shù)組握侧,如果有組件依賴 filteredTodos蚯瞧,則那個(gè)小組件也會(huì)被更新。
說(shuō)白了品擎,todos.filter(...) 這個(gè) selector 其實(shí)就是依賴了 todos 和 filter 嘛埋合,那能不能實(shí)現(xiàn) useCallback 那樣,只要 todos 和 filter 不變萄传,那就不需要 todos.filter(..) 了甚颂,用回以前的數(shù)組,這個(gè)過(guò)程就是 Memorization秀菱。
市面上也有這種庫(kù)來(lái)做 Memorization西设,叫 Reselect。不過(guò) redux-toolkit 提供了一個(gè) createSelector
答朋,那還用個(gè)屁的 Reselect贷揽。
// todos/selectors.ts
export const selectFilteredTodos = createSelector<TStore, TTodo[], TFilter, TTodo[]>(
selectTodos,
selectFilter,
(todos: TTodo[], filter: TFilter) => {
if (filter === 'all') {
return todos
}
return todos.filter(todo => todo.state === filter)
}
)
上面的 createSelector 第一個(gè)參數(shù)是獲取 selectTodos 的 selector,selectFilter 返回 filter梦碗,然后第三個(gè)參數(shù)是函數(shù)禽绪,頭兩個(gè)參數(shù)就是所依賴的 todos 和 filter蓖救。這就完成了 memorization 了。
createReducer + createAction
其實(shí) redux-toolkit 里面有挺多好的東西的印屁,上面所說(shuō)的 API 大概覆蓋了 80% 了循捺,剩下的還有 createReducer 和 createAction 沒(méi)有說(shuō)。沒(méi)有說(shuō)的原因是 createReducer + createAction 約等于 createSlice雄人。
這里一定要注意:createAction 和 createReducer 是并列的从橘,createSlice 類似于前兩個(gè)的結(jié)合,createSlice 更強(qiáng)大一些础钠。網(wǎng)上有些聲音是討論該用 createAction + createReducer 還是直接上 createSlice 的恰力。如果分不清哪個(gè)好,那就用 createSlice
旗吁。
總結(jié)
到這里會(huì)發(fā)現(xiàn)真正我們用到的東西就是 redux + react-redux + redux-toolkit 就可以寫一個(gè)最佳實(shí)踐出來(lái)了踩萎。
市面上還有很多諸如 redux-action, redux-promise, reduce-reducers等等的 redux 衍生品(redux 都快變一個(gè) IP 了)。這些東西要不就是更好規(guī)范 redux 代碼很钓,要不就是在dispatch(action) -> UI 更新 這個(gè)流程再多加流程香府,它們的最終目的都是為了更自動(dòng)化地管理狀態(tài)/數(shù)據(jù),相信理解了這個(gè)思路再看那些 redux 衍生品就更容易上手了码倦。