easy-peasy
Version:
Vegetarian friendly state for React
130 lines (113 loc) • 4.11 kB
JavaScript
import {
useContext,
useEffect,
useLayoutEffect,
useReducer,
useRef,
useState,
} from 'react';
import EasyPeasyContext from './context';
// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser. We need useLayoutEffect to ensure the store
// subscription callback always has the selector from the latest render commit
// available, otherwise a store update may happen between render and the effect,
// which may cause missed updates; we also must ensure the store subscription
// is created synchronously, otherwise a store update may occur before the
// subscription is created and an inconsistent state may be observed
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export function createStoreStateHook(Context) {
return function useStoreState(mapState) {
const store = useContext(Context);
const mapStateRef = useRef(mapState);
const stateRef = useRef();
const mountedRef = useRef(true);
const subscriptionMapStateError = useRef();
const [, forceRender] = useReducer(s => s + 1, 0);
if (
subscriptionMapStateError.current ||
mapStateRef.current !== mapState ||
stateRef.current === undefined
) {
try {
stateRef.current = mapState(store.getState());
} catch (err) {
let errorMessage = `An error occurred trying to map state in a useStoreState hook: ${err.message}.`;
if (subscriptionMapStateError.current) {
errorMessage += `\nThis error may be related to the following error:\n${subscriptionMapStateError.current.stack}\n\nOriginal stack trace:`;
}
throw new Error(errorMessage);
}
}
useIsomorphicLayoutEffect(() => {
mapStateRef.current = mapState;
subscriptionMapStateError.current = undefined;
});
useIsomorphicLayoutEffect(() => {
const checkMapState = () => {
try {
const newState = mapStateRef.current(store.getState());
if (newState === stateRef.current) {
return;
}
stateRef.current = newState;
} catch (err) {
// see https://github.com/reduxjs/react-redux/issues/1179
// There is a possibility mapState will fail due to stale state or
// props, therefore we will just track the error and force our
// component to update. It should then receive the updated state
subscriptionMapStateError.current = err;
}
if (mountedRef.current) {
forceRender({});
}
};
const unsubscribe = store.subscribe(checkMapState);
checkMapState();
return () => {
mountedRef.current = false;
unsubscribe();
};
}, []);
return stateRef.current;
};
}
export const useStoreState = createStoreStateHook(EasyPeasyContext);
export function createStoreActionsHook(Context) {
return function useStoreActions(mapActions) {
const store = useContext(Context);
return mapActions(store.getActions());
};
}
export const useStoreActions = createStoreActionsHook(EasyPeasyContext);
export function createStoreDispatchHook(Context) {
return function useStoreDispatch() {
const store = useContext(Context);
return store.dispatch;
};
}
export const useStoreDispatch = createStoreDispatchHook(EasyPeasyContext);
export function useStore() {
return useContext(EasyPeasyContext);
}
export function createStoreRehydratedHook(Context) {
return function useStoreRehydrated() {
const store = useContext(Context);
const [rehydrated, setRehydrated] = useState(false);
useEffect(() => {
store.persist.resolveRehydration().then(() => setRehydrated(true));
}, []);
return rehydrated;
};
}
export const useStoreRehydrated = createStoreRehydratedHook(EasyPeasyContext);
export function createTypedHooks() {
return {
useStoreActions,
useStoreDispatch,
useStoreState,
useStoreRehydrated,
useStore,
};
}