Last active
April 8, 2019 08:06
-
-
Save kmsheng/bb3a2cb65da936e4ce90f0f3ccffd9cb to your computer and use it in GitHub Desktop.
redux-react-hook / src / create.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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