Last active
February 14, 2019 05:12
-
-
Save chris-pardy/6ff60fdae7404f5745a865423989e0db to your computer and use it in GitHub Desktop.
react-redux hooks
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
import React from 'react'; | |
import { | |
Store, | |
Action, | |
Dispatch, | |
AnyAction, | |
ActionCreator, | |
ActionCreatorsMapObject, | |
bindActionCreators | |
} from 'redux'; | |
// matches the interface from the select effect from Sagas | |
// components that call this should wrap the selectors in useMemo, | |
// or rely on a memoization utility like reselect | |
export interface UseSelect<S> { | |
(): S; | |
<R>(selector: (state: S) => R): R; | |
<P,R>(selector: (state: S, params: P, ...args: any[]) => R), params: P, ...args: any[]): R; | |
} | |
// passes the argument into bindActionCreators with the dispatch | |
// if no arguments are passed returns the dispatch function. | |
// calling sites are responsible for ensuring that actionCreators | |
// don't change between reneders. | |
export interface UseDispatch<A> { | |
(): Dispatch<A>; | |
<C extends ActionCreator<A>>(actionCreator: C): C; | |
<M extends ActionCreatorsMapObject<A>>(creatorMap: M): M; | |
} | |
// A ReduxContext, each context is unique. | |
export type ReduxContext<S, A> { | |
Provider: React.ComponentType<{store: Store<S, A>}>; | |
useSelect: UseSelect<S>; | |
useDispatch: UseDispatch<A>; | |
} | |
// avoid using an in-line function to prevent unnessesary re-renders. | |
const identity: <T>(v: T) => T = v => v; | |
// lowers the overhead / issues with regular stores | |
type LightweightStore<S, A> = { | |
dispatch: Dispatch<A>; | |
// subscribe passes the state, this elliminates most calls to getState() | |
subscribe: (subscriber: (state: S) => void) => () => void; | |
getState: () => S; | |
} | |
// create a ReduxContext | |
export const createContext: <S, A extends Action = AnyAction>() => ReduxContext<S,A> = () => { | |
// create a Provider/Context pair, give a dummy store. | |
const {Provider, Context} = React.createContext<LightweightStore<S,A>>({ | |
dispatch: a => a, | |
getState: () => <S>null!, | |
subscribe: () => { | |
return () => {} | |
}, | |
}); | |
return { | |
// Provide the store to the hooks | |
Provider: ({store, children}) => { | |
const state = React.useRef(store.getState()); | |
const subscribers = React.useRef([]); | |
const lightweightStore = React.useMemo(() => ({ | |
dispatch: store.dispatch, | |
getState: () => state.current, | |
subscribe: subscription => { | |
const i = subscriber.current.push(subscription); | |
subscription(state.current); | |
return () => { delete subscriber.current[i] }; | |
} | |
}), [store, state, subscribers]) | |
React.useEffect(() => { | |
return store.subscribe(() => { | |
const s = store.getState() | |
state.current = s; | |
subscribers.current.filter(sub => !!sub).forEach(sub => sub(s)); | |
}) | |
}, [store, subscribers, state]) | |
return ( | |
<Provider value={lightweightStore}> | |
{children} | |
</Provider> | |
); | |
}, | |
// use identity as the default as it will pass through the store state. | |
useSelect: (selector = identity, params = {}, ...args) => { | |
const store = React.useContext(Context); | |
const [value, setValue] = React.useState(() => selector(store.getState(), params, ...args)); | |
// This is operating on the assumption that | |
// both subscribe/unsubscribe and getState are very cheap. | |
// if they aren't building proxies that cache them | |
// would help. | |
React.useEffect( | |
() => ( | |
store.subscribe(state => { | |
// set value already checks against the last value | |
// no need to repeat that check | |
setValue(selector(state)) | |
}) | |
), | |
[store, selector, setValue, params, ...args] | |
); | |
return value; | |
}, | |
// use identity as the default as it will pass through the dispatch function. | |
useDispatch: (actionCreators = identity) => { | |
const store = React.useContext(Context); | |
// this could be React.useCallback instead, however | |
// bindActionCreators would be run on every render. | |
return React.useMemo( | |
() => bindActionCreators(actionCreators, store.dispatch), | |
[store, actionCreators] | |
); | |
} | |
} | |
} | |
// Create a generic set of functions, if you don't care about type safety. | |
const {Provider, useSelect, useDispatch} = createContext<any>(); | |
export { | |
Provider, | |
useSelect, | |
useDispatch | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment