Skip to content

Instantly share code, notes, and snippets.

@kmsheng
Last active April 8, 2019 08:06
Show Gist options
  • Save kmsheng/bb3a2cb65da936e4ce90f0f3ccffd9cb to your computer and use it in GitHub Desktop.
Save kmsheng/bb3a2cb65da936e4ce90f0f3ccffd9cb to your computer and use it in GitHub Desktop.
redux-react-hook / src / create.ts
// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
import {
createContext,
useContext,
useEffect,
useMemo,
useReducer,
useRef,
} from 'react';
import {Action, Dispatch, Store} from 'redux';
import shallowEqual from './shallowEqual';
// 這裡自訂 MissingProviderError error,底下檢查到沒有 store 時都會噴這個 error
class MissingProviderError extends Error {
constructor() {
super(
'redux-react-hook requires your Redux store to be passed through ' +
'context via the <StoreContext.Provider>',
);
}
}
// memoizeSingleArg 方法使用了 closure 技巧把上一次的執行參數與結果紀錄在 prevArg 與 value
// return 一個方法如果參數跟上一次執行的參數相同時則返還 cache 起來的結果。
function memoizeSingleArg<AT, RT>(fn: (arg: AT) => RT): (arg: AT) => RT {
let value: RT;
let prevArg: AT;
return (arg: AT) => {
if (prevArg !== arg) {
prevArg = arg;
value = fn(arg);
}
return value;
};
}
export function create<
TState,
TAction extends Action,
TStore extends Store<TState, TAction>
>(): {
StoreContext: React.Context<TStore | null>;
useMappedState: <TResult>(mapState: (state: TState) => TResult) => TResult;
useDispatch: () => Dispatch<TAction>;
} {
// 這個 StoreContext 是用來給外部餵 redux store 的
const StoreContext = createContext<TStore | null>(null);
/**
* 餵進來的 mapState 方法應透過 useCallback 產生,這樣可以避免每次 render 都重新訂閱。
* 如果你沒有在 mapState 裡用到其他屬性,餵一個空陣列 [] 到 useCallback 第二個參數,這樣可以避免每次 render 都重新建立 mapState。
*
* const todo = useMappedState(useCallback(
* state => state.todos.get(id),
* [id],
* ));
*/
function useMappedState<TResult>(
mapState: (state: TState) => TResult,
): TResult {
const store = useContext(StoreContext);
// 沒有 store 就丟自訂 error
if (!store) {
throw new MissingProviderError();
}
// 我們不儲存被改過的 state,但是每次 render 時會使用當前 state 呼叫 mapState 方法
// 這個做法可以保證 useMappedState 每次 return 出去被改過的 state 都是最新的
// 因為 mapState 可以是一個龐大運算的 pure 方法,所以我們可以把方法的運算結果 cache 起來
const memoizedMapState = useMemo(() => memoizeSingleArg(mapState), [
mapState,
]);
const state = store.getState();
// 這裡會使用外面給的 mapState 方法,參數重複時則會給 cache 結果
const derivedState = memoizedMapState(state);
// 這裡的 forceUpdate 方法是從 useReducer 第二個參數拿出來
// 底層會呼叫 dispatchAction 觸發更新
const [, forceUpdate] = useReducer(x => x + 1, 0);
// 把 derivedState 物件與 memoizedMapState 方法透過 useRef 存到各自 hook 物件的 memoizedState 屬性
// 如果之後在 useEffect 跟新的 state 比較發現不一樣時,就會觸發 forceUpdate
const lastStateRef = useRef(derivedState);
const memoizedMapStateRef = useRef(memoizedMapState);
// 這裡必須要在每次畫面更新時,重新更新 lastStateRef 與 memoizedMapStateRef 的屬性
// 否則 useRef 在 update phase 是不會更新 current 的值的
// 詳情請看 https://github.com/kmsheng/react/blob/medium-20190408/packages/react-reconciler/src/ReactFiberHooks.js#L824-L827
useEffect(() => {
lastStateRef.current = derivedState;
memoizedMapStateRef.current = memoizedMapState;
});
useEffect(() => {
let didUnsubscribe = false;
// 執行 mapState 方法,如果結果有變,更新畫面
const checkForUpdates = () => {
if (didUnsubscribe) {
// 這裡必需用 didUnsubscribe 這個 flag 擋掉應該被移除的 listeners
// 因為 Redux 不保證在 dispatch 事件跑到呼叫 listeners 可以移除當下在跑的 listeners
// ensureCanMutateNextListeners
// https://github.com/kmsheng/redux/blob/medium-20190408/src/createStore.js#L73-L77
// https://github.com/kmsheng/redux/blob/medium-20190408/src/createStore.js#L152
return;
}
// 透過 store.getState() 拿到最新的 state 並丟給 mapState 處理
const newDerivedState = memoizedMapStateRef.current(store.getState());
// 如果 newDerivedState 與舊的不一樣就觸發 forceUpdate
if (!shallowEqual(newDerivedState, lastStateRef.current)) {
// In TS definitions userReducer's dispatch requires an argument
(forceUpdate as () => void)();
}
};
// 這邊必須先呼叫一次 checkForUpdates
// 因為從 useMappedState 被呼叫開始到 store.subscribe 被呼叫之前有可能 state 已經不一樣了
checkForUpdates();
// 這裡呼叫 redux 的 store.subscribe 監聽 dispatch 行為
const unsubscribe = store.subscribe(checkForUpdates);
// React 元件卸載時會 call 底下方法 unsubscribe store
return () => {
didUnsubscribe = true;
unsubscribe();
};
}, [store]);
return derivedState;
}
function useDispatch(): Dispatch<TAction> {
const store = useContext(StoreContext);
// 檢查 store,如果沒有 store 就丟 error
if (!store) {
throw new MissingProviderError();
}
return store.dispatch;
}
return {
StoreContext,
useDispatch,
useMappedState,
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment