jungpaeng/sagen

View on GitHub
src/hooks/useSagenState.ts

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
import React from 'react';
import { CreateStore } from 'sagen-core';
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';

function defaultEqualityFn(prev: any, next: any) {
  return prev === next;
}

export type SagenState<Selected = never, State = any> = [Selected] extends [never]
  ? State
  : Selected;

export function useSagenState<Selected = never, State = any>(
  store: CreateStore<State>,
  selector?: (value: State) => Selected,
  equalityFn = defaultEqualityFn,
): SagenState<Selected, State> {
  const [, forceUpdate] = React.useReducer((curr: number) => curr + 1, 0) as [never, () => void];
  const selectedState = React.useCallback(
    (state: State) => (selector ? selector(state) : state) as SagenState<Selected, State>,
    [selector],
  );

  const storeState = store.getState();

  const storeStateRef = React.useRef(storeState);
  const selectorRef = React.useRef(selector);
  const equalityFnRef = React.useRef(equalityFn);
  const erroredRef = React.useRef(false);

  const currentSliceRef = React.useRef<SagenState<Selected, State>>();
  if (currentSliceRef.current === undefined) {
    currentSliceRef.current = selectedState(storeState);
  }

  let newStateSlice: SagenState<Selected, State> | undefined;
  let hasNewStateSlice = false;

  if (
    storeStateRef.current !== storeState ||
    selectorRef.current !== selector ||
    equalityFnRef.current !== equalityFn ||
    erroredRef.current
  ) {
    // Using local variables to avoid mutations in the render phase.
    newStateSlice = selectedState(storeState);
    hasNewStateSlice = !equalityFn(
      currentSliceRef.current as SagenState<Selected, State>,
      newStateSlice,
    );
  }

  // Syncing changes in useEffect.
  useIsomorphicLayoutEffect(() => {
    if (hasNewStateSlice) {
      currentSliceRef.current = newStateSlice as SagenState<Selected, State>;
    }

    storeStateRef.current = storeState;
    selectorRef.current = selector;
    equalityFnRef.current = equalityFn;
    erroredRef.current = false;
  });

  const stateBeforeSubscriptionRef = React.useRef(storeState);
  useIsomorphicLayoutEffect(() => {
    const listener = () => {
      try {
        const nextState = store.getState();
        const nextStateSlice = selectorRef.current?.(nextState);

        if (
          !equalityFnRef.current(
            currentSliceRef.current as SagenState<Selected, State>,
            nextStateSlice,
          )
        ) {
          storeStateRef.current = nextState;
          currentSliceRef.current = nextStateSlice as SagenState<Selected, State>;
          forceUpdate();
        }
      } catch (error) {
        erroredRef.current = true;
        forceUpdate();
      }
    };

    const unSubscribe = store.onSubscribe(listener);

    if (store.getState() !== stateBeforeSubscriptionRef.current) {
      listener(); // state has changed before subscription
    }

    return unSubscribe;
  }, [selectedState, equalityFn, store]);

  return hasNewStateSlice
    ? (newStateSlice as SagenState<Selected, State>)
    : currentSliceRef.current!;
}