「都 1202 年了怎麼還有人在用 Redux」——這大概很多人看到這篇文章的第一反應。首先先代表一下,這篇文章並不討論是否是應該使用 Redux,這是一個比較大的話題,應該單獨水一篇。並且社區已經存在許許多多的討論了,你總能從幾篇高讚的文章中找到一些優缺點的對比圖,而後結合你項目的場景最終做出決定。咱們來隨便舉幾個團隊使用 Redux 的緣由。首先是易懂,Redux 被人吐槽不少的多是寫法繁瑣,可是在繁瑣寫法的背後就沒有那麼多黑科技了,很是容易排查問題。另外,Redux 本質是對邏輯處理方式提出了標準範式,而且搭配得給到了一組實踐規範,有助於保持項目代碼書寫風格與組織方式的一致性,這點在多人合做開發的項目裏面尤其重要。其餘的優勢就不在此贅述啦。javascript
這時候就有同窗可能要問了,你講 Redux,那和 hooks 又有啥子關係呢。衆所周知,在 React 團隊推出 Hooks 這個概念後不久,Redux 也更新了對應的 API 來支持。Hooks 的本質是對邏輯的封裝以及邏輯與 UI 代碼的解耦。有了 Hooks 的加持可以讓咱們的 Redux React 項目更加簡潔、易懂、擴展性更強。並且 Hooks API 在 Redux 的最佳實踐建議中目前是 Level 2 的強烈推薦使用級別。他擁有更簡潔的表達方式,更乾淨的 React 節點數,更友好的 typescript 支持。java
具體 Redux 相關的 API 怎麼用,這裏不作介紹,能夠直接跳轉官方文檔進行了解。下面咱們會從一個應用場景來具體講一講,他們是怎麼幫助咱們更好地組織代碼的。其中的部分工程級別代碼來自於 react-boilerplate 的項目模版,它在動態加載問題上提供了很多幫助。react
在開發大型 React 應用的時候,動態懶加載代碼永遠是咱們項目架構中的必選項。代碼的拆分、動態引用等,工程化工具都已經幫咱們完成了。咱們更須要關注的是,動態引入與解除掛載等操做時額外要作什麼,以及這個工做如何儘可能少的暴露給項目開發者。前面說過了,Hooks 最強大的能力在於邏輯的封裝,這裏固然也就要藉助他的力量了。typescript
這裏咱們以 Reducer 做爲例子來說,其餘中間件,例如 Saga 等均可以類推,若是須要能夠後續再把相應的代碼一併貼出來。咱們把整個封裝分爲三層:核心實現、可組合封裝、對開發者暴露封裝。下面咱們按順序一一講解。(具體實現中我都會默認帶上包含 connected router 的實現,方便須要抄代碼的能夠直接用)redux
這裏的代碼實現的是如何爲一個 store 掛載與解除掛載拆分後的各個 Reducer 的邏輯。數組
// 本段代碼徹底來自於 react-boilerplate 項目 import { combineReducers } from 'redux'; import { connectRouter } from 'connected-react-router'; import invariant from 'invariant'; import { isEmpty, isFunction, isString } from 'lodash'; import history from '@/utils/history'; import checkStore from './checkStore'; // 作類型安全檢測的,不用關心 function createReducer(injectedReducers = {}) { return history => combineReducers({ router: connectRouter(history), ...injectedReducers, }); } export function injectReducerFactory(store, isValid) { return function injectReducer(key, reducer) { if (!isValid) checkStore(store); invariant( isString(key) && !isEmpty(key) && isFunction(reducer), '(src/utils...) injectReducer: Expected `reducer` to be a reducer function', ); if ( Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer ) return; store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign store.replaceReducer(createReducer(store.injectedReducers)(history)); }; } export default function getInjectors(store) { checkStore(store); return { injectReducer: injectReducerFactory(store, true), }; }
這段有個點比較特殊,須要講一下。你可能會發現,這裏面根本沒有解除掛載的部分。這是由於 reducer 比較特殊,他並不會產生反作用,而且由於目前提供的方法是經過整個替換的方式去掛載新的 Reducer,因此並無什麼必要去單獨作解除掛載。在處理其餘中間件的掛載時,特別是那些存在反作用的(例如 redux-saga),咱們須要對應地實現一個解除掛載的 eject 方法。安全
OK,那麼如今咱們已經能夠經過 getInjectors 方法爲整個項目提供一個 injectReducer 注入 Reducer 的能力了(同時可能包含 eject 方法)。下一步就是怎麼調度這個能力。react-router
這裏,咱們但願經過一個自定義的 hooks,能夠容許開發者爲一個組件聲明某一個 命名空間 的 reducer 與其生命週期一致地進行掛載與解除掛載。開發者只須要傳入 reducer 的命名空間與 reducer 實現,並將這個 hooks 放到相應的組件邏輯中便可。架構
import React from 'react'; import { ReactReduxContext } from 'react-redux'; // 這是咱們在上一步實現的 injector 工廠,經過他來產出一個與固定 store 綁定的 injectReducer 函數 import getInjectors from './reducerInjectors'; const useInjectReducer = ({ key, reducer }) => { // 須要從 Redux 的 context 中獲取到當前應用的全局 store 實例 const context = React.useContext(ReactReduxContext); // 爲了模擬 constructor 的運行時機 const initFlagRef = React.useRef(false); if (!initFlagRef.current) { initFlagRef.current = true; getInjectors(context.store).injectReducer(key, reducer); } // 若是須要加入 eject 的邏輯,則可使用這樣的寫法。相似於爲當前組件增長一個 willUnmount 的生命週期邏輯。 // React.useEffect(() => (() => { // const injectors = getInjectors(context.store); // injectors.ejectReducer(key); // }), []); }; export { useInjectReducer };
useInjectReducer 這個 Hooks 幫助咱們處理了什麼時候去掛載,怎麼掛載等問題,咱們最終只須要告訴他 掛載什麼 就能夠了。經過這層封裝,能夠發現咱們進一步收斂了關注點。到這一步爲止,咱們都是提供了一個項目級別的公共方法。在下一步中,咱們會提供一個統一的寫法,在具體的開發過程當中去使用,進一步作封裝收斂。函數
在進入下一步以前,咱們先簡單解釋一下上面的邏輯。邏輯經過註釋分爲了三段(第三段在 reducer 場景下沒用到),第一段咱們經過當前組件所處的 redux 上下文,拿到了 store 的引用,第二段與第三段咱們分別讓組件在 初始化 和 銷燬前 執行掛載與解除掛載的操做。經過一個 initFlagRef 爲 functional 的組件模擬構造器的生命週期(若是有更好的實現方案歡迎指教),由於若是在掛載以後再 inject 的話,會在第一次渲染時取不到對應 store 的內容。
在完成公用方法的封裝以後,咱們下一步考慮的就是如何用更簡單的方式,爲咱們的模塊掛載 store 。按照下面的方式,開發者不用關心任何東西,只需一句話就能夠完成掛載,也不用提供額外的參數。若是同時有 reducer、saga 或其餘中間件內容,也能夠一塊兒打包搞定。
import { useInjectReducer, // useInjectSaga, } from '@/utils/store'; import actions from './actions'; import constants from './constants'; import reducer from './reducer'; // import saga from './saga'; const namespace = constants.namespace; const useSubStore = () => { useInjectReducer({ key: namespace, reducer }); // useInjectSaga({ key: namespace, saga }); }; export { namespace, actions, constants, useSubStore, };
實際使用範例:
import React from 'react'; import { useSubStore, } from './store'; export default function Page() { useSubStore(); return <div />; };
具體的數據和邏輯咱們也能夠封裝成幾個 Hooks ,例如咱們須要提供一個數組數據簡單操做,咱們只關心 添加 和 數量,就能夠封裝一個 Hooks,這樣實際使用方只須要關心 添加 和 數量 這兩個要素,不用關心 redux 的具體實現方式了。
import { useMemo, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { actions, constants, namespace, } from './store'; export function useItemList() { const dispatch = useDispatch(); const list = useSelector(state => state[namespace].itemList); // 這只是範例! const count = useMemo(() => list.length, [list]); const add = useCallback((item) => dispatch(actions.addItem(item)), []); return [count, add]; }
下面咱們修改一下使用的地方:
import React from 'react'; import { useSubStore, } from './store'; import { useItemList } from './useItemList'; export default function Page() { useSubStore(); const [count, add] = useItemList(); return <div onClick={() => add({})}>{count}</div>; };
經過這樣一種拆分方式,store 的定義,store 的使用邏輯,業務側三者都只關注本身必須關注的部分,任何一方改動均可以儘可能少地引發變動。
那咱們進一步思考一下,之前咱們可能一個頁面對應一個 store。經過 Hooks 進行拆分後,咱們更方便從功能層面去拆分 store,store 的邏輯也會更爲清晰。與 store 的交互被封裝成了 Hooks 以後也能夠很快在多個展現層被使用。這在複雜 B 端工做臺場景下會展示出很大的價值。案例會有點長,之後有時間能夠再補上。
看完上面的例子,相信聰明的讀者已經知道我想表達的問題了。經過結合 Redux + Hooks,標準化了定義代碼,對邏輯、調用、定義三者必定程度上進行了解耦。經過簡化的 API,減小了邏輯的理解成本,減小了後續維護的複雜度,必定程度上還能夠達到複用。不論是相較於過去的 Redux 接入方案,仍是相較於單純使用 Hooks,都有着其獨特的優點。特別適用於邏輯相對複雜的工做臺場景。(並且我很喜歡 Saga 的設計思路,能用起來就很爽)。
OK,收。此次以一個簡單的例子,稍稍展現了一下在 Hooks 大環境下 Redux 與其產生的化學反應。主要想展現的是依賴 Hooks 的邏輯可封裝能力的一種設計思路,Redux 黑的同窗們不要過多糾結與這個選型,蘿蔔青菜各有所愛。
但願這個系列能繼續寫下去 :)
做者:ES2049 / armslave00
文章可隨意轉載,但請保留此原文連接。很是歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj@alibaba-inc.com 。