Skip to content

Instantly share code, notes, and snippets.

@chris-pardy
Last active February 14, 2019 05:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chris-pardy/6ff60fdae7404f5745a865423989e0db to your computer and use it in GitHub Desktop.
Save chris-pardy/6ff60fdae7404f5745a865423989e0db to your computer and use it in GitHub Desktop.
react-redux hooks
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