redux 文檔到底說(shuō)了什么(下)

完整代碼請(qǐng)看這里

上一篇文章主要介紹了 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 衍生品就更容易上手了码倦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末企孩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子袁稽,更是在濱河造成了極大的恐慌勿璃,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件运提,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡闻葵,警方通過(guò)查閱死者的電腦和手機(jī)民泵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)槽畔,“玉大人栈妆,你說(shuō)我怎么就攤上這事∠峋” “怎么了鳞尔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)早直。 經(jīng)常有香客問(wèn)我寥假,道長(zhǎng),這世上最難降的妖魔是什么霞扬? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任糕韧,我火速辦了婚禮枫振,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘萤彩。我一直安慰自己粪滤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布雀扶。 她就那樣靜靜地躺著杖小,像睡著了一般。 火紅的嫁衣襯著肌膚如雪愚墓。 梳的紋絲不亂的頭發(fā)上予权,一...
    開(kāi)封第一講書(shū)人閱讀 51,258評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音转绷,去河邊找鬼伟件。 笑死,一個(gè)胖子當(dāng)著我的面吹牛议经,可吹牛的內(nèi)容都是我干的斧账。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼煞肾,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼咧织!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起籍救,我...
    開(kāi)封第一講書(shū)人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤习绢,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蝙昙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體闪萄,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年奇颠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了败去。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡烈拒,死狀恐怖圆裕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情荆几,我是刑警寧澤吓妆,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站吨铸,受9級(jí)特大地震影響行拢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜诞吱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一剂陡、第九天 我趴在偏房一處隱蔽的房頂上張望狈涮。 院中可真熱鬧,春花似錦鸭栖、人聲如沸歌馍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)松却。三九已至,卻和暖如春溅话,著一層夾襖步出監(jiān)牢的瞬間晓锻,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工飞几, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留砚哆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓屑墨,卻偏偏與公主長(zhǎng)得像躁锁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子卵史,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354