使用70行程式碼配合hooks重新實現react-redux
原由
react-hooks 是 react 官方新的編寫推薦,我們很容易在官方的 useReducer 鉤子上進行一層很簡單的封裝以達到和以往 react-redux \ redux-thunk \ redux-logger 類似的功能,並且大幅度簡化了宣告。
react-hooks 的更多資訊請閱讀 reactjs.org/hooks;
先看看原始碼
這70行程式碼就是全部, 客官可以先閱讀,或許後續的說明文件也就不需要閱讀了。
- 簡易的實現了 react-redux, redux-thunk 和 redux-logger
- 預設使用 reducer-in-action 的風格, 也可宣告傳統的 reducer 風格
import React from 'react';
function devLog(lastState, nextState, action, isDev) {
if (isDev) {
console.log(
`%c|------- redux: ${action.type} -------|`,
`background: rgb(70, 70, 70); color: rgb(240, 235, 200); width:100%;`,
);
console.log('|--last:', lastState);
console.log('|--next:' , nextState);
}
}
function reducerInAction(state, action) {
if (typeof action.reducer === 'function') {
return action.reducer(state);
}
return state;
}
export default function createStore(params) {
const { isDev, reducer, initialState, actions, middleware } = {
isDev: false,
reducer : reducerInAction,
initialState: {},
actions: {},
middleware: params.isDev ? [devLog] : undefined,
...params,
};
const AppContext = React.createContext();
const store = {
useContext: function() {
return React.useContext(AppContext);
},
actions,
dispatch: undefined,
state: initialState,
initialState,
};
let realReducer;
if (middleware) {
realReducer = function(lastState, action) {
let nextState = reducer(lastState, action);
for (let i = 0; i < middleware.length; i++) {
const newState = middleware[i](lastState, nextState, action, isDev);
if (newState) {
nextState = newState;
}
}
return nextState;
};
} else {
realReducer = reducer;
}
const Provider = props => {
const [state, dispatch] = React.useReducer(realReducer, initialState);
if (!store.dispatch) {
store.dispatch = async function(action) {
if (typeof action === 'function') {
await action(dispatch, store.state);
} else {
dispatch(action);
}
};
}
store.state = state;
return <AppContext.Provider {...props} value={state} />;
};
return { Provider, store };
}
複製程式碼
reducer-in-action 風格
reducer-in-action是一個reducer函式,這 6 行程式碼就是 reducer-in-action 的全部:
function reducerInAction(state, action) {
if (typeof action.reducer === 'function') {
return action.reducer(state);
}
return state;
}
複製程式碼
它把 reducer 給簡化了,放置到了每一個 action 中進行 reducer 的處理。我們再也不需要寫一堆 switch,再也不需要時刻關注 action 的 type 是否和 redcer 中的 type 一致。
reducer-in-action 配合 thunk 風格,可以非常簡單的編寫 redux,隨著專案的複雜,我們只需要編寫 action,會使得專案結構更清晰。
使用
安裝, 您甚至可以將上面那70行程式碼拷貝至專案中, 需要 react 版本 >= 16.7
yarn add react-hooks-redux
複製程式碼
我們用了不到 35 行程式碼就聲明瞭一個完整的 react-redux 的例子, 擁抱 hooks。
import React from 'react';
import ReactHookRedux from 'react-hooks-redux';
// 通過 ReactHookRedux 獲得 Provider 元件和一個 sotre 物件
const { Provider, store } = ReactHookRedux({
isDev: true, // 列印日誌
initialState: { name: 'dog', age: 0 },
});
function actionOfAdd() {
return {
type: 'add the count',
reducer(state) {
return { ...state, age: state.age + 1 }; // 每次需要返回一個新的 state
},
};
}
function Button() {
function handleAdd() {
store.dispatch(actionOfAdd()); //dispatch
}
return <button onClick={handleAdd}>add</button>;
}
function Page() {
const state = store.useContext();
return <div>{state.age} <Button/> </div>;
}
export default function App() {
return <Provider><Page /></Provider>;
}
複製程式碼
middleware 的編寫
絕大部分情況,你不需要編寫middleware, 不過它也極其簡單。middleware 是一個一維陣列,陣列中每個物件都是一個函式, 傳入了引數並且如果返回的物件存在, 就會替換成 nextState 並且繼續執行下一個 middleware。
我們可以使用 middleware 進行列印日誌、編寫chrome外掛或者二次處理 state 等操作。
我們看看 middleware 的原始碼:
let nextState = reducer(lastState, action);
for (let i = 0; i < middleware.length; i++) {
const newState = middleware[i](lastState, nextState, action, isDev);
if (newState) {
nextState = newState;
}
}
return nextState;
複製程式碼
效能和注意的事項
效能(和實現上)上最大的區別是,react-hooks-redux 使用 useConnect 鉤子代替 connect 高階元件進行 dispatch的派發。
在傳統的 react-redux 中,如果一個元件被 connect 高階函式進行處理,那麼當 dispatch 時,這個元件相關的 mapStateToProps 函式就會被執行,並且返回新的 props 以啟用元件更新。
而在 hooks 風格中,當一個元件被聲明瞭 useContext() 時,context 相關聯的物件被變更了,這個元件會進行更新。
理論上效能和 react-redux 是一致的,由於 hooks 相對於 class 有著更少的宣告,所以應該會更快一些。
所以,我們有節制的使用 useContext 可以減少一一些元件被 dispatch 派發更新。
如果我們需要手動控制減少更新 可以參考 useMemo 鉤子的使用方式進行配合。
以上都是理論分析,由於此庫和此文件是一個深夜的產物,並沒有去做效能上的基準測試,所以有人如果願意非常歡迎幫忙做一些基準測試。
非同步action的例子
import React from 'react';
import ReactHookRedux, { reducerInAction, devLog } from 'react-hooks-redux';
// 通過 ReactHookRedux 獲得 Provider 元件和一個 sotre 物件
const { Provider, store } = ReactHookRedux({
isDev: true, // default is false
initialState: { count: 0, asyncCount: 0 }, // default is {}
reducer: reducerInAction, // default is reducerInAction 所以可省略
middleware: [devLog], // default is [devLog] 所以可省略
actions: {}, // default is {} 所以可省略
});
// 模擬非同步操作
function timeOutAdd(a) {
return new Promise(cb => setTimeout(() => cb(a + 1), 500));
}
const actions = {
// 如果返回的是一個function,我們會把它當成類似 react-thunk 的處理方式,並且額外增加一個ownState的物件方便獲取state
asyncAdd: () => async (dispatch, ownState) => {
const asyncCount = await timeOutAdd(ownState.asyncCount);
dispatch({
type: 'asyncAdd',
// if use reducerInAction, we can add reducer Function repeat reducer
reducer(state) {
return {
...state,
asyncCount,
};
},
});
},
};
function Item() {
const state = store.useContext();
return <div>async-count: {state.asyncCount}</div>;
}
function Button() {
async function handleAdd() {
// 使用 async dispatch
await store.dispatch(actions.asyncAdd());
}
return <button onClick={handleAdd}>add</button>;
}
export default function App() {
return (
<Provider>
<Item />
<Button />
</Provider>
);
}
複製程式碼
謝謝閱讀。