230205 항해99 91일차 react-singleton-hook

요니링 컴터 공부즁·2023년 2월 20일
0

react-singleton-hook

  • react-singleton-hook is a library that exposes a method to convert any hook into a singleton hook which makes sure that only one (or 0) instance of the hook is alive at any time.
  • There are 3 main things in react-singleton-hook:
  1. SingletonHooksContainer — We need a place where all our singleton hooks instances can stay alive. SingletonHooksContainer is a component in which all the singleton hooks will be mounted.
  2. SingleItemcontainerSingletonHooksContainer will render a component called SingleItemContainer for each of the singleton hooks. Whenever the return value of the hook changes, this component will “apply” the changes to all the places where the hook is used.
  3. singletonHook — This is the function that converts any hook into a singleton hook. This function will instantiate the hook if it is not already instantiated, create a consumer wherever it is used, define the functions that apply the return value changes in these consumers and also define some cleanup logic that will be executed when the hook will be unmounted.

type of singleton hook

export type Hook<HookReturn = any, HookArgs extends any[] = any[]> = {
 /**initial return value of the hook (before the hook is actually called) */
 initValue: HookReturn;
/**the hook which we want to make singleton */
 useHookBody: Function;
/**a function that informs all the hook consumers of any change in the hook return value */
 applyStateChange: (newState: HookReturn) => void;
/**the arguments that are passed to the hook while using it */
 args: HookArgs;
};

SingletonHooksContainer

  • This component will hold all the singleton hooks instances. So this component should be mounted before any component that uses any of the singleton hooks.
  • We want to define and expose a couple of functions to add and remove hooks into this component. We will define these two functions outside the component and assign them their function bodies inside the component (thus we can export functions that can access the component state). We will also keep track of whether the component has been mounted in a global variable. This is kept outside the component so that if the same component is being mounted again, we can throw an error.
import React, { useEffect, useState } from 'react';
import { SingleItemContainer } from './SingleItemContainer';
import { Hook } from './singletonHookTypes';

let SingletonHooksContainerMounted = false;

let mountIntoContainer: <HookReturn = any, HookArgs extends any[] = any[]>(hook: Hook<HookReturn, HookArgs>) => void;

let unmountFromContainer: <HookReturn = any, HookArgs extends any[] = any[]>(hook: Hook<HookReturn, HookArgs>) => void;
  • We will first write an effect to update SingletonHooksContainerMounted and throw an error if the component is being mounted more than once. We will also set SingletonHooksContainerMounted to false in the cleanup function.
export const SingletonHooksContainer = () => {
  
 useEffect(() => {
  if (SingletonHooksContainerMounted) {
   console?.log?.(
    'SingletonHooksContainer is mounted second time. ' +
     'You should mount SingletonHooksContainer before any other component and never unmount it.'
   );
  }
  SingletonHooksContainerMounted = true;
   
  return () => {
     SingletonHooksContainerMounted = false;
  };
 }, []);
  • We will create a state hooks which is a list of all the singleton hooks that are being used at that point in time. We then assign behaviour to mountIntoContainer and unmountFromContainer where we append the hook to the list and filter the hook out from the list respectively.
const [hooks, setHooks] = useState<Hook[]>([]);

useEffect(() => {
  mountIntoContainer = (newHook: Hook) => setHooks((hooks) => [...hooks, newHook]);
  
  unmountFromContainer = (hookToUnmount: Hook) =>
   setHooks((hooks) => hooks.filter((hook) => hook.useHookBody !== hookToUnmount.useHookBody));
  
  setHooks([]);
 }, []);
  • We map through hooks and render SingleItemContainer for each hook.
return (
  <>
   {hooks.map((hook, index) => (
    <SingleItemContainer {...hook} key={index} />
   ))}
  </>
 );
  • This is how we maintain all the instances of the singleton hooks in a separate component.

SingleItemContainer

  • This component is rendered inside SingletonHooksContainer for each hook. This component will accept the hook with type Hook as prop. It will keep track of the previous state (in this blog, by “state” of the hook, we refer to the hook’s return value). There is also a safety check to make sure that the useHookBody is a function.
import { useEffect, useRef } from 'react';
import { Hook } from './singletonHookTypes';

export const SingleItemContainer = ({ initValue, useHookBody, applyStateChange, args = [] }: Hook) => {
 const lastState = useRef(initValue);
 
  if (typeof useHookBody !== 'function') {
    throw new Error(`function expected as hook body parameter. got ${typeof useHookBody}`);
 }
  • We store the return value of the hook by calling useHookBody along with the arguments args. We define an effect to run applyStateChange whenever hookReturn changes and is different from the lastState.
const hookReturn = useHookBody(...args);

useEffect(() => {
  if (lastState.current !== hookReturn) {
   lastState.current = hookReturn;
   applyStateChange(hookReturn);
  }
 }, [applyStateChange, JSON.stringify(hookReturn)]);
  • Thus, this simple component will store the hook return value and apply changes to all the consumers whenever it changes.

singletonHook

  • This function converts any hook into a singleton hook. In its arguments, it accepts initValue and useHookBody which is the initial return value and the actual hook respectively.
  • There are a few local states in this function.
    mounted: boolean flag which tells if the hook is already mounted
    initStateCalculated: a boolean flag that tells us if the initial state of the hook is calculated
    lastKnownState: stores the latest hook return value
    consumers: an array of all the hook consumers. This will basically be an array of setState functions.
export function singletonHook<HookReturn = any, HookArgs extends any[] = any[]>(
 initValue: HookReturn,
 useHookBody: () => HookReturn
) {
 let mounted = false;
 
 let initStateCalculated = false;
 
 let lastKnownState: HookReturn | undefined;
 
 let consumers: React.Dispatch<React.SetStateAction<HookReturn>>[] = [];
  • We define a function applyStateChange which will be executed whenever the return value changes (as we saw in SingleItemContainer). This function executes all the consumers (setState methods) with the new value, in a single batch. We also define a function stateInitializer which stores the initial state in lastKnownState and returns it.
const applyStateChange = (newState: HookReturn) => {
  lastKnownState = newState;
  
  batch(() => consumers.forEach((setState) => setState(newState)));
 };

const stateInitializer = () => {
  if (!initStateCalculated) {
   lastKnownState = initValue;
   initStateCalculated = true;
  }
  
  return lastKnownState as HookReturn;
 };
  • singletonHook returns a hook (which is a singleton). We first get the initial state using useState. We then define an effect which will take care of making this hook a singleton. If the hook is not mounted already, we mount it using mountIntoContainer function which we saw earlier. We also push the setState function into the consumer array. If lastKnownState is not equal to the state value, we update the state using the setState method. We then define the cleanup logic which gets executed when the hook (or rather the component that consumes the hook) is being unmounted. We remove the consumer from the consumers array. If there are no more consumers, we update the different flags to their initial values and unmount the hook using the unmountFromContainer method we saw earlier. This singleton hook returns state which is the return value of the hook which we made into a singleton.
return (args: HookArgs) => {
  const [state, setState] = useState(stateInitializer);

  useEffect(() => {
   if (!mounted) {
    mounted = true;
    addHook<HookReturn, HookArgs>({ initValue, useHookBody, applyStateChange, args });
   }
   consumers.push(setState);
    
   if (lastKnownState && lastKnownState !== state) {
    setState(lastKnownState);
   }
    
   return () => {
    consumers.splice(consumers.indexOf(setState), 1);
    
     if (consumers.length === 0) {
     mounted = false;
     initStateCalculated = false;
     lastKnownState = undefined;
     removeHook({ initValue, useHookBody, applyStateChange, args });
    }
   };
  }, []);
 return state;
};
  • Thus we have the function which converts any hook into a singleton hook.

reference: Understanding react-singleton-hook

0개의 댓글