Skip to content

dmaskasky/react-global-hooks

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-global-hooks

Motivation

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.


Definitions

Shared vs Common

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.

Call Position

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;
};

Shared Hooks

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.

createSharedState

Returns useSelector hook and setState function.

useSelector and setState are useful for sharing mutable global data atomics.

setState's API is similar to React's setState.

type CreateSharedState = (
  initialState: InitialState | LazyInitialState,
  name?: DebugName,
) => [UseSelector, SetState, Subscribe];
type LazyInitialState = (setState: SetState, subscribe: Subscribe) => InitialState;
type UseSelector = (selector?: Selector, equalityFn?: EqualityFn, timeVaryingFn?: TimeVaryingFn) => SelectedState;
type Selector = (nextState: State) => SelectedState;
type EqualityFn = (currentState: State, nextState: State) => boolean;
type TimeVaryingFn = (Function) => Function;
type SetState = (state: State | LazyState) => void;
type Observer = (state: State) => void;
type Subscribe = (observer: Observer) => Unsubscribe;
type Unsubscribe = () => void;
type LazyState = (currentState: State) => NextState;
type DebugName = string;
const [useSelector, setState, subscribe] = createSharedState<State>(initialState);
const state = useSelector();

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, setState] = createSharedState(
  () => someExpensiveComputation();
);

Async lazy initial state

const fetchPromise = fetch('example.api').then(data => data.json());
const [useSelector, setState] = createSharedState((setState, subscribe) => {
  fetchPromise.then(setState);
  // keep local storage in sync with state updates
  subscribe(state => {
    localStorage.setItem(STORAGE_KEY, state);
  })
  return {}; // use this value until example.api responds
});

createSharedReducer

Returns useSelector hook and dispatch function.

type CreateSharedReducer = (
  reducer: Reducer,
  initialState: InitialState | LazyInitialState,
  name?: DebugName,
) => [UseSelector, Dispatch];
type Reducer = (currentState: State, action: Action) => NextState;
type Action = Object & { type: any }
type LazyInitialState = (dispatch: Dispatch) => InitialState;
type UseSelector = (selector?: Selector, equalityFn?: EqualityFn, timeVaryingFn?: TimeVaryingFn) => SelectedState;
type Selector = (nextState: State) => SelectedState;
type EqualityFn = (currentState: State, nextState: State) => boolean;
type TimeVaryingFn = (fn: Function) => Function;
type Dispatch = (action: Action) => void;
type LazyState = (currentState: State) => NextState;
type DebugName = string;
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

const [useSelector, dispatch] = 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 [useSelector, dispatch] = createSharedReducer(reducer, initialState);
dispatch({type: 'increment'});

Lazy initial state

const [useSelector, dispatch] = createSharedReducer(reducer,
  () => someExpensiveComputation()
);

Async lazy initial state

const fetchPromise = fetch('example.api').then(data => data.json());
const [useSelector, dispatch] = createSharedReducer(reducer, dispatch => {
  fetchPromise.then(value => {
    dispatch({type: 'INITIALIZE', value});
  });
  return {}; // use this value until example.api responds
});

Common Hooks

Build more logic into hooks with Common Hooks

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.

createCommonHook

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.

type createCommonHook = (hook: Hook, name?: DebugName) => SharedHook;
type Hook = Function;
type SharedHook = Hook;
type DebugName = string;
import {
  createCommonHook,
  useCommonEffect,
  useCommonMemo,
  useCommonRef
} from `react-global-hooks`;

const useCustomHook = createCommonHook(() => {
  const ref = useCommonRef();
  useCommonEffect(() => {}, [ref]);
  useCommonEffect(() => {}, [ref]);
  return useCommonMemo(() => {}, []);
});
export useCustomHook;

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 `react-global-hooks`;

const useCustomHook = () => {
  useEffect(() => {}, []);
  return useCommonMemo(() => {}, []);
};
export createCommonHook(useCustomHook);

useCommonCallback

Provides a referentially stable callback across all call stacks of the enclosing hook.

This API is identical to React's useCallback.

type useCommonCallback = (inputFn: InputFn, watchedArgs: WatchedArgs) => StableFn;
type InputFn = Function;
type WatchedArgs = Array<any>;
type StableFn = InputFn;
import { createCommonHook, useCommonCallback } from `react-global-hooks`;

const useCustomHook = createCommonHook((fn) => {
  const stableFn = useCommonCallback(fn, []);
});
export useCustomHook;

useCommonEffect

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.

type useCommonEffect = (inputFn: InputFn, watchedArgs: WatchedArgs) => void;
type InputFn = () => Cleanup;
type Cleanup = () => void;
type WatchedArgs = Array<any>;
import { createCommonHook, useCommonEffect } from `react-global-hooks`;

const useCustomHook = createCommonHook((fn) => {
  useCommonEffect(fn, []);
});
export useCustomHook;

useCommonLayoutEffect

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.

type useCommonEffect = (inputFn: InputFn, watchedArgs: WatchedArgs) => void;
type InputFn = () => Cleanup;
type Cleanup = () => void;
type WatchedArgs = Array<any>;
import { createCommonHook, useCommonLayoutEffect } from `react-global-hooks`;

const useCustomHook = createCommonHook((fn) => {
  useCommonLayoutEffect(fn, []);
});
export useCustomHook;

useCommonMemo

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.

type useCommonMemo = (inputFn: InputFn, watchedArgs: WatchedArgs) => MemoizedValue;
type InputFn = () => Value;
type Value = any;
type MemoizedValue = Value;
type WatchedArgs = Array<any>;
import { createCommonHook, useCommonMemo } from `react-global-hooks`;

const useCustomHook = createCommonHook((fn) => {
  const stableMemo = useCommonMemo(fn, []);
});
export useCustomHook;

useCommonRef

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.

type useCommonRef = (value: Value) => Ref;
type Value = any;
type Ref = { current: Value };
import { createCommonHook, useCommonRef } from `react-global-hooks`;

const useCustomHook = createCommonHook(() => {
  const stableRef = useCommonRef();
});
export useCustomHook;

useCommonState

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.

type useCommonState = (State | LazyState) => [State, SetState];
type LazyState = (SetState) => NextState;
type SetState = (State) => NextState;
import { createCommonHook, useCommonState } from `react-global-hooks`;

const useCustomHook = createCommonHook(() => {
  const [state, setState] = useCommonState();
});
export useCustomHook;

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 75.4%
  • TypeScript 24.6%