Redux + Hooks 工程實踐
點擊上方關注 前端技術江湖,一起學習,天天進步
“都 1202 年了怎么還有人在用 Redux”——這大概不少人看到這篇文章的第一反應。首先先表明一下,這篇文章并不討論是不是應該使用 Redux,這是一個比較大的話題,應該單獨水一篇。而且社區(qū)已經存在許許多多的討論了,你總能從幾篇高贊的文章中找到一些優(yōu)缺點的對比圖,然后結合你項目的場景最終作出決定。我們來隨便舉幾個團隊使用 Redux 的原因。首先是易懂,Redux 被人吐槽很多的可能是寫法繁瑣,但是在繁瑣寫法的背后就沒有那么多黑科技了,非常容易排查問題。另外,Redux 本質是對邏輯處理方式提出了標準范式,并且搭配得給到了一組實踐規(guī)范,有助于保持項目代碼書寫風格與組織方式的一致性,這點在多人合作開發(fā)的項目里面尤為重要。其他的優(yōu)點就不在此贅述啦。
這時候就有同學可能要問了,你講 Redux,那和 hooks 又有啥子關系呢。眾所周知,在 React 團隊推出 Hooks 這個概念后不久,Redux 也更新了對應的 API 來支持。Hooks 的本質是對邏輯的封裝以及邏輯與 UI 代碼的解耦。有了 Hooks 的加持能夠讓我們的 Redux React 項目更加簡潔、易懂、擴展性更強。而且 Hooks API 在 Redux 的最佳實踐建議中目前是 Level 2 的強烈推薦使用級別。他擁有更簡潔的表達方式,更干凈的 React 節(jié)點數(shù),更友好的 typescript 支持。
具體 Redux 相關的 API 怎么用,這里不做介紹,可以直接跳轉官方文檔進行了解。下面我們會從一個應用場景來具體講一講,他們是怎么幫助我們更好地組織代碼的。其中的部分工程級別代碼來自于 react-boilerplate 的項目模版,它在動態(tài)加載問題上提供了不少幫助。
封裝案例
在開發(fā)大型 React 應用的時候,動態(tài)懶加載代碼永遠是我們項目架構中的必選項。代碼的拆分、動態(tài)引用等,工程化工具都已經幫我們完成了。我們更需要關注的是,動態(tài)引入與解除掛載等操作時額外要做什么,以及這個工作如何盡量少的暴露給項目開發(fā)者。前面說過了,Hooks 最強大的能力在于邏輯的封裝,這里當然也就要借助他的力量了。
這里我們以 Reducer 作為例子來講,其他中間件,例如 Saga 等都可以類推,如果需要可以后續(xù)再把相應的代碼一并貼出來。我們把整個封裝分為三層:核心實現(xiàn)、可組合封裝、對開發(fā)者暴露封裝。下面我們按順序一一講解。(具體實現(xiàn)中我都會默認帶上包含 connected router 的實現(xiàn),方便需要抄代碼的可以直接用)
核心實現(xiàn)
這里的代碼實現(xiàn)的是如何為一個 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),
};
}
這段有個點比較特殊,需要講一下。你可能會發(fā)現(xiàn),這里面根本沒有解除掛載的部分。這是因為 reducer 比較特殊,他并不會產生副作用,并且因為目前提供的方法是通過整個替換的方式去掛載新的 Reducer,所以并沒有什么必要去單獨做解除掛載。在處理其他中間件的掛載時,特別是那些存在副作用的(例如 redux-saga),我們需要對應地實現(xiàn)一個解除掛載的 eject 方法。
OK,那么現(xiàn)在我們已經可以通過 getInjectors 方法為整個項目提供一個 injectReducer 注入 Reducer 的能力了(同時可能包含 eject 方法)。下一步就是怎么調度這個能力。
可組合的封裝
這里,我們希望通過一個自定義的 hooks,可以允許開發(fā)者為一個組件聲明某一個 命名空間 的 reducer 與其生命周期一致地進行掛載與解除掛載。開發(fā)者只需要傳入 reducer 的命名空間與 reducer 實現(xiàn),并將這個 hooks 放到相應的組件邏輯中即可。
import React from 'react';
import { ReactReduxContext } from 'react-redux';
// 這是我們在上一步實現(xiàn)的 injector 工廠,通過他來產出一個與固定 store 綁定的 injectReducer 函數(shù)
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 幫助我們處理了何時去掛載,怎么掛載等問題,我們最終只需要告訴他 掛載什么 就可以了。通過這層封裝,可以發(fā)現(xiàn)我們進一步收斂了關注點。到這一步為止,我們都是提供了一個項目級別的公共方法。在下一步中,我們會提供一個統(tǒng)一的寫法,在具體的開發(fā)過程中去使用,進一步做封裝收斂。
在進入下一步之前,我們先簡單解釋一下上面的邏輯。邏輯通過注釋分為了三段(第三段在 reducer 場景下沒用到),第一段我們通過當前組件所處的 redux 上下文,拿到了 store 的引用,第二段與第三段我們分別讓組件在 初始化 和 銷毀前 執(zhí)行掛載與解除掛載的操作。通過一個 initFlagRef 為 functional 的組件模擬構造器的生命周期(如果有更好的實現(xiàn)方案歡迎指教),因為如果在掛載之后再 inject 的話,會在第一次渲染時取不到對應 store 的內容。
對開發(fā)者暴露封裝
在完成公用方法的封裝之后,我們下一步考慮的就是如何用更簡單的方式,為我們的模塊掛載 store 。按照下面的方式,開發(fā)者不用關心任何東西,只需一句話就可以完成掛載,也不用提供額外的參數(shù)。如果同時有 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 />;
};
具體的數(shù)據(jù)和邏輯我們也可以封裝成幾個 Hooks ,例如我們需要提供一個數(shù)組數(shù)據(jù)簡單操作,我們只關心 添加 和 數(shù)量,就可以封裝一個 Hooks,這樣實際使用方只需要關心 添加 和 數(shù)量 這兩個要素,不用關心 redux 的具體實現(xiàn)方式了。
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 的使用邏輯,業(yè)務側三者都只關注自己必須關注的部分,任何一方改動都可以盡量少地引起變更。
可復用的 Hooks
那我們進一步思考一下,以前我們可能一個頁面對應一個 store。通過 Hooks 進行拆分后,我們更方便從功能層面去拆分 store,store 的邏輯也會更為清晰。與 store 的交互被封裝成了 Hooks 之后也可以很快在多個展示層被使用。這在復雜 B 端工作臺場景下會展現(xiàn)出很大的價值。案例會有點長,以后有時間可以再補上。
回顧
看完上面的例子,相信聰明的讀者已經知道我想表達的問題了。通過結合 Redux + Hooks,標準化了定義代碼,對邏輯、調用、定義三者一定程度上進行了解耦。通過簡化的 API,減少了邏輯的理解成本,減少了后續(xù)維護的復雜度,一定程度上還可以達到復用。不管是相較于過去的 Redux 接入方案,還是相較于單純使用 Hooks,都有著其獨特的優(yōu)勢。特別適用于邏輯相對復雜的工作臺場景。(而且我很喜歡 Saga的設計思路,能用起來就很爽)。
OK,收。這次以一個簡單的例子,稍稍展示了一下在 Hooks 大環(huán)境下 Redux 與其產生的化學反應。主要想展示的是依賴 Hooks 的邏輯可封裝能力的一種設計思路,Redux 黑的同學們不要過多糾結與這個選型,蘿卜青菜各有所愛。
希望這個系列能繼續(xù)寫下去 :)
作者:ES2049 / armslave00 https://zhuanlan.zhihu.com/p/374788504 非常歡迎有激情的你加入 ES2049 Studio,簡歷請發(fā)送至 [email protected] 。
The End
歡迎自薦投稿到《前端技術江湖》,如果你覺得這篇內容對你挺有啟發(fā),記得點個 「在看」哦
點個『在看』支持下 
