React hooks have become quite popular since they were released. Developers have used the composable nature of react hooks to abstract logic into custom hooks. These custom hooks enhance a functional component by providing behavior and local state.
React Global Hooks expands on this idea by introducing global versions of these same hooks. These are the foundational building blocks for writing custom hooks that are shared between components but effect component independent interactions. Components subscribe to behavior and state encapsulated within these global hooks.
// hooks/use-fibonocci.jsimport{createSharedState,createCommonHook,useCommonCallback,useCommonEffect,}from'@uber/react-global-hooks';const[useGetFib,useSetFib]=createSharedState({prev: 0,curr: 1});export{useGetFib};exportconstuseFibonocciOnMove=createCommonHook(()=>{constsetFibonocci=useSetFib();consthandleMouseMove=useCommonCallback(()=>{setFibonocci(({prev, curr})=>({prev: curr,curr: prev+curr}));},[]);// setFibonocci is referentially stable and not needed in dependency arrayuseCommonEffect(()=>{document.addEventListener('mousemove',handleMouseMove);return()=>{document.removeEventListener('mousemove',handleMouseMove);};},[handleMouseMove]);});// components/fibonocci.jsimport{useGetFib,useFibonocciOnMove}from'../hooks/use-fibonocci';exportconstFib=()=>{useFibonocciOnMove();returnnull;};// selector for current fib valueconstselectCurrent=({curr})=>curr;// Debounce rerenders with 500ms delayconstdebounce=(fn)=>_.debounce(fn,500);exportconstShowFibDebounced=()=>{constfib=useGetFib(selectCurrent,null,debounce);returnfib;};Shared hooks can be shared between multiple components and custom hooks. They provide referentially stable results across all call positions.
Common hooks partition behavior on call position. Each call position provides independent behavior and results.
A hook's call position is the expression where that hook is invoked. A hook invoked in multiple places is said to have multiple call positions.
For example, say Hook A is only invoked by Hook B, and Hook B is invoked by multiple components. Hook A is still said to have only one call position, (inside Hook B). Hook A's call position provides consistent behavior and referentially stable results for that call position across all call stacks.
Example 1 Consistent Behavior
const useHookA = useCommonEffect;
const useHookB = createCommonHook(() => {
useHookA(() => {
console.log('runs only on first component mount');
return () => console.log('runs only on last component unmount');
}, []);
});
Example 2 Referential Stability
const useHookA = useCommonRef;
const useHookB = createCommonHook(() => {
const ref = useHookA();
return ref
});
const CheckRef = () => {
const ref1 = useHookB();
const ref2 = useHookB();
console.log(ref1 === ref2); // true
return null;
};
import {createStoreMap, Provider as GlobalHooksProvider} from '@uber/react-global-hooks';
const storeMap = createStoreMap();
ReactDOM.render(
<GlobalHooksProvider value={storeMap}>
<App />
</GlobalHooksProvider>,
document.getElementById('root')
);
Concurrent Mode
import {createStoreMap, Provider as GlobalHooksProvider} from '@uber/react-global-hooks';
const storeMap = createStoreMap();
ReactDOM.createRoot(
document.getElementById('root')
).render(
<GlobalHooksProvider value={storeMap}>
<App />
</GlobalHooksProvider>
);
React's useState and useReducer are good solutions for state isolated to a component. This library expands on this idea by providing shareable versions of useState and useReducer so that atomic and molecular state can be shared across many components.
Returns useSelector and useSetState hooks.
useSelector and useSetState are useful for sharing global data atomics.
useSetState returns setState.
setState's API is similar to React's setState.
typeCreateSharedState=(InitialState|LazyInitialState,
?DebugName,)=>[UseSelector,UseDispatch];typeLazyInitialState=(Dispatch)=>InitialState;typeUseSelector=(?Selector, ?EqualityFn, ?TimeVaryingFn)=>SelectedState;typeSelector=(NextState)=>SelectedState;typeEqualityFn=(CurrentState,NextState)=>boolean;typeTimeVaryingFn=(Function)=>Function;typeUseDispatch=()=>Dispatch;typeDispatch=(State|LazyState)=>void;typeLazyState=(CurrentState)=>NextState;typeDebugName=string;const[useSelector,useSetState]=createSharedState(initialCount);conststate=useSelector();constsetState=useSetState();Components that use this selector will only rerender when state.count changes
You can make the selector referentially stable to improve performance. The selector is otherwise run on every render.
const selectCount = useCallback(state => state.count, []);
const count = useSelector(selectCount);
Pass a equality function to override the default. The default equality function is Object.is.
const vehicleSelector = useCallback(state => state.vehicle, []);
const vehicleEquality = useCallback((curr, next) => curr.vin === next.vin), []);
const vehicle = useSelector(vehicleSelector, vehicleEquality);
Specify a time-varying function such as debounce or throttle to limit the number of rerenders.
Important Note: selector and equalityFn must be referentially stable for timeVaryingFn to work. Use useCallback or define outside the component to ensure stability.
const vehicleSelector = useCallback(state => state.vehicle, []);
const vehicleEquality = useCallback((curr, next) => curr.vin === next.vin), []);
const timeVaryingFn = useCallback(fn => _.debounce(fn, 500), []); // lodash debounce
const vehicle = useSelector(vehicleSelector, vehicleEquality, timeVaryingFn);
Set a simple state
setState(5);
or set state based on previous state
setState(count => count + 1);
Bail out of a render by returning the original state
setState(count => {
if (someCondition) {
return count;
}
return count + 1;
});
Lazy initial state
const [useSelector, useSetState] = createSharedState(
() => someExpensiveComputation();
);
Async lazy initial state
const fetchPromise = fetch('example.api').then(data => data.json());
const [useGetState, useSetState, useSubscribe] = createSharedState(setState => {
fetchPromise.then(setState);
return {}; // use this value until example.api responds
});
Returns useSelector and useDispatch hooks.
typeCreateSharedReducer=(Reducer,InitialState|LazyInitialState,
?DebugName,)=>[UseSelector,UseDispatch];typeReducer=(CurrentState,Action)=>NextState;typeAction=Object;typeLazyInitialState=(Dispatch)=>InitialState;typeUseSelector=(?Selector, ?EqualityFn, ?TimeVaryingFn)=>SelectedState;typeSelector=(NextState)=>SelectedState;typeEqualityFn=(CurrentState,NextState)=>boolean;typeTimeVaryingFn=(Function)=>Function;typeUseDispatch=()=>Dispatch;typeDispatch=(Action)=>void;typeLazyState=(CurrentState)=>NextState;typeDebugName=string;constinitialState={count: 0};functionreducer(state,action){switch(action.type){case'increment':
return{count: state.count+1};case'decrement':
return{count: state.count-1};default:
thrownewError();}}const[useSelector,useDispatch]=createSharedReducer(reducer,initialState);Components that use this selector will only rerender when state.count changes
const countSelector = useCallback(state => state.count, []);
const count = useSelector(selectCount);
Pass a equality function to override the default. The default equality function is Object.is.
You can make the selector referentially stable to improve performance. The selector is otherwise run on every render.
const vehicleSelector = useCallback(state => state.vehicle, []);
const vehicleEquality = useCallback((curr, next) => curr.vin === next.vin), []);
const vehicle = useSelector(vehicleSelector, vehicleEquality);
Specify a time-varying function such as debounce or throttle to limit the number of rerenders.
Important Note: selector and equalityFn must be referentially stable for timeVaryingFn to work. Use useCallback or define outside the component to ensure stability.
const vehicleSelector = useCallback(state => state.vehicle, []);
const vehicleEquality = useCallback((curr, next) => curr.vin === next.vin), []);
const timeVaryingFn = useCallback(fn => _.debounce(fn, 500), []); // lodash debounce
const vehicle = useSelector(vehicleSelector, vehicleEquality, timeVaryingFn);
Dispatch an action
const dispatch = useDispatch();
dispatch({type: 'increment'});
Lazy initial state
const [useSelector, useDispatch] = createSharedReducer(reducer,
() => someExpensiveComputation()
);
Async lazy initial state
const fetchPromise = fetch('example.api').then(data => data.json());
const [useSelector, useDispatch] = createSharedReducer(reducer, dispatch => {
fetchPromise.then(value => {
dispatch({type: 'INITIALIZE', value});
});
return {}; // use this value until example.api responds
});
Returns useSharedRef that provides a referentially stable ref that may be used by multiple hooks.
useSharedRef is useful for creating refs that are watched by other common hooks.
typecreateSharedRef=(?any, ?DebugName)=>useSharedRef;typeuseSharedRef=()=>Ref;typeRef={current: any};typeDebugName=string;constuseSharedRef=createSharedRef();If we intend to write truely shareable hooks, we need hooks that are not based on individual component lifecycle events. This library provides Common hooks that compose into shareable custom hooks.
This higher order hook is required to use the useCommon-* hooks in this library.
createCommonHook internally tracks each call position and memoizes a separate common hook for each position. This is only possible inside a custom hook wrapped by createCommonHook.
typecreateCommonHook=(Hook, ?DebugName)=>SharedHook;typeHook=Function;typeSharedHook=Hook;typeDebugName=string;import{createCommonHook,useCommonEffect,useCommonMemo,useCommonRef}from`@uber/react-global-hooks`;constuseCustomHook=createCommonHook(()=>{constref=useCommonRef();useCommonEffect(()=>{},[ref]);useCommonEffect(()=>{},[ref]);returnuseCommonMemo(()=>{},[]);});exportdefaultuseCustomHook;It is also safe to use react hooks within a createCommonHook. The function argument respects React's call position across renders.
import{useEffect}from'react';import{createCommonHook,useCommonMemo,}from`@uber/react-global-hooks`;constuseCustomHook=()=>{useEffect(()=>{},[]);returnuseCommonMemo(()=>{},[]);};exportdefaultcreateCommonHook(useCustomHook);Provides a referentially stable callback across all call stacks of the enclosing hook.
This API is identical to React's useCallback.
typeuseCommonCallback=(InputFn,WatchedArgs)=>StableFn;typeInputFn=Function;typeWatchedArgs=Array<any>;typeStableFn=InputFn;import{createCommonHook,useCommonCallback}from`@uber/react-global-hooks`;constuseCustomHook=createCommonHook((fn)=>{conststableFn=useCommonCallback(fn,[]);});exportdefaultuseCustomHook;Executes a function on the first component mount or whenever props change asynchronously post render. The returned cleanup function is executed on last component unmount or whenever props change. This API is identical to React's useEffect.
useCommonEffect is useful for registering event listeners, fetching data, and other side-effects that should applied only once.
typeuseCommonEffect=(InputFn,WatchedArgs)=>void;typeInputFn=()=>Cleanup;typeCleanup=()=>void;typeWatchedArgs=Array<any>;import{createCommonHook,useCommonEffect}from`@uber/react-global-hooks`;constuseCustomHook=createCommonHook((fn)=>{useCommonEffect(fn,[]);});exportdefaultuseCustomHook;Executes a function on the first component mount or whenever props change synchronously after all DOM mutations This API is identical to React's useLayoutEffect.
useCommonLayoutEffect is useful for DOM layout dependent effects that should be applied only once.
typeuseCommonEffect=(InputFn,WatchedArgs)=>void;typeInputFn=()=>Cleanup;typeCleanup=()=>void;typeWatchedArgs=Array<any>;import{createCommonHook,useCommonLayoutEffect}from`@uber/react-global-hooks`;constuseCustomHook=createCommonHook((fn)=>{useCommonLayoutEffect(fn,[]);});exportdefaultuseCustomHook;Provides a referentially stable memo across all call stacks of the enclosing hook. This API is identical to React's useMemo.
Fn will be called on first component mount or whenever any values change. useCommonMemo runs synchronously during render.
typeuseCommonMemo=(InputFn,WatchedArgs)=>MemoizedValue;typeInputFn=()=>Value;typeValue=any;typeMemoizedValue=Value;typeWatchedArgs=Array<any>;import{createCommonHook,useCommonMemo}from`@uber/react-global-hooks`;constuseCustomHook=createCommonHook((fn)=>{conststableMemo=useCommonMemo(fn,[]);});exportdefaultuseCustomHook;Provides a referentially stable ref across all call stacks of the enclosing hook. This API is identical to React's useRef.
useCommonRef is useful for creating refs that are watched by other common hooks.
typeuseCommonRef=(Value)=>Ref;typeValue=any;typeRef={current: Value};import{createCommonHook,useCommonRef}from`@uber/react-global-hooks`;constuseCustomHook=createCommonHook(()=>{conststableRef=useCommonRef();});exportdefaultuseCustomHook;Provides a common state and setState. This API is identical to React's useState.
useCommonState is useful for storing atomic state that is local to the enclosing hook. Prefer useSharedState and useSharedReducer for organizing application state. These APIs provide extended capabilities for limiting the number of rerenders.
typeuseCommonState=(State|LazyState)=>[State,SetState];typeLazyState=(SetState)=>NextState;typeSetState=(State)=>NextState;import{createCommonHook,useCommonState}from`@uber/react-global-hooks`;constuseCustomHook=createCommonHook(()=>{const[state,setState]=useCommonState();});exportdefaultuseCustomHook;Need a hook that doesn't exist? You can register your own with hookFactory to piggyback off createCommonHook's call position tracking.
To use hookFactory the callback must take the shape of () => Function.
typeHookFactory=(CreateHook)=>CommonHook;typeCreateHook=()=>Hook;typeCommonHook=Hook;typeHook=Function;import{hookFactory}from'@uber/react-global-hooks';constuseDebounced=hookFactory(functioncreateDebouncedHook(){lettimeout;returnfunctionuseDebounced(fn,value){clearTimeout(timeout);timeout=setTimeout(fn,value);};});constuseHookA=createCommonHook((a,b,c)=>{useDebounced(a);useDebounced(b);useDebounced(c);});constuseHookB=createCommonHook((d)=>{useDebounced(d);});MIT