In this series, I will read the source code of ahooks, which is used to familiarized myself with the writing method of custom hooks, and improve my ability to write custom hooks.

In order to distinguish it from the original comments of the code, the personal understanding section uses the beginning of ///, which is not related to the triple slash instruction, just to distinguish it.

Review past

  • Ahooks source code interpretation series
  • Ahooks Source Code Interpretation Series – 2
  • Ahooks Source Code Interpretation series – 3
  • Ahooks Source Code Interpretation series – 4
  • Ahooks Source Code Interpretation series – 5
  • Ahooks Source Code Interpretation series – 6
  • Ahooks Source Code Interpretation series – 7
  • Ahooks Source Code Interpretation series – 8
  • Ahooks Source Code Interpretation series – 9

Unconsciously, it has been updated to the 10th article. The feedback of the previous several articles is not very good, the number of reading is relatively low, and there is no comment at all. I don’t know where the problem is, is there any experienced big guy comment, thank 🙏 ~ but anyway, the pit has been opened will adhere to update, later or according to their own pace will finish the rest of the hook interpretation, thank you for reading, heart ~

useCounter

“Put a seat belt on Count.”

import { useMemo, useState } from 'react';
import useCreation from '.. /useCreation';

/ / /...

/// The target value is calculated to ensure that it is within the set min-max
function getTargetValue(val: number, options: Options = {}) {
  const { min, max } = options;
  let target = val;
  if (typeof max === 'number') {
    target = Math.min(max, target);
  }
  if (typeof min === 'number') {
    target = Math.max(min, target);
  }
  return target;
}

function useCounter(initialValue: number = 0, options: Options = {}) {
  const { min, max } = options;

  // get init value
  const init = useCreation(() = > {
    returngetTargetValue(initialValue, { min, max, }); } []);const [current, setCurrent] = useState(init);

  const actions = useMemo(() = > {
    const setValue = (value: ValueParam) = > {
      setCurrent((c) = > {
        // get target value
        let target = typeof value === 'number' ? value : value(c);
        return getTargetValue(target, {
          max,
          min,
        });
      });
    };
    const inc = (delta: number = 1) = > {
      setValue((c) = > c + delta);
    };
    const dec = (delta: number = 1) = > {
      setValue((c) = > c - delta);
    };
    const set = (value: ValueParam) = > {
      setValue(value);
    };
    const reset = () = > {
      setValue(init);
    };
    return { inc, dec, set, reset };
  }, [init, max, min]);

  return [current, actions] as const;
}

export default useCounter;

Copy the code

useControllableValue

“The world goes around without you.”

If “value” is passed to props, external control of state is given, and if not, internal state changes are not affected. A bit like the native input component, it becomes a controlled component if you pass value, and an uncontrolled component if you don’t.

import { useCallback, useState } from 'react';
import useUpdateEffect from '.. /useUpdateEffect';

exportinterface Options<T> { defaultValue? : T; defaultValuePropName? : string; valuePropName? : string; trigger? : string; }export interface Props {
  [key: string]: any;
}

interface StandardProps<T> {
  value: T; defaultValue? : T; onChange:(val: T) = > void;
}
function useControllableValue<T = any> (props: StandardProps<T>) :T, (val: T) = >void];
function useControllableValue<T = any> (
  props?: Props,
  options?: Options<T>,
) :T, (v: T, ... args: any[]) = >void];
function useControllableValue<T = any> (props: Props = {}, options: Options<T> = {}) {
  const {
    defaultValue,
    defaultValuePropName = 'defaultValue',
    valuePropName = 'value',
    trigger = 'onChange',
  } = options; /// configure the managed key values for state and setState

  const value = props[valuePropName] as T;

  const [state, setState] = useState<T>(() = > {
    if (valuePropName in props) {
      return value;
    }
    if (defaultValuePropName in props) {
      return props[defaultValuePropName];
    }
    return defaultValue;
  });

  /* init */
  useUpdateEffect(() = > {
    if (valuePropName in props) { // if "value" is passed in, the current state is updated
      setState(value);
    }
  }, [value, valuePropName]);

  const handleSetState = useCallback(
    (v: T, ... args: any[]) = > {
      if(! (valuePropNamein props)) { // if no "value" is passed externally, the state is updated internally
        setState(v);
      }
      if (props[trigger]) { /// call if "onChange" is passed in from the outsideprops[trigger](v, ... args); } }, [props, valuePropName, trigger], );return [valuePropName in props ? value : state, handleSetState] as const;
}

export default useControllableValue;

Copy the code

useCookieState

‘The Internet has memories’

The state is stored in the cookie and the page refresh is not lost.

import Cookies from 'js-cookie'; // use js-cookie to implement cookie management
import { useCallback, useState } from 'react';
import { isFunction } from '.. /utils';

// TODO ts is not named properly, to be fixed in the next big version
export type TCookieState = string | undefined | null;
export type TCookieOptions = Cookies.CookieAttributes;

export interface IOptions extendsTCookieOptions { defaultValue? : TCookieState | (() = > TCookieState);
}

// the same principle applies to hijacking state acquisition and assignment, useStorageState and other cache hooks
function useCookieState(cookieKey: string, options: IOptions = {}) {
  If there is no cookie, use the default value. The default value can be a method
  const [state, setState] = useState<TCookieState>(() = > {
    const cookieValue = Cookies.get(cookieKey);
    if (typeof cookieValue === 'string') return cookieValue;

    if (isFunction(options.defaultValue)) return options.defaultValue();
    return options.defaultValue;
  });

  UsePersistFn guarantees that the returned updateState will not change
  const updateState = useCallback(
    (newValue? : TCookieState | ((prevState: TCookieState) => TCookieState), newOptions: Cookies.CookieAttributes = {},) = > {
      const{ defaultValue, ... restOptions } = { ... options, ... newOptions }; setState( (prevState: TCookieState):TCookieState= > {
          /// The new value can be a function that behaves like useState's assignment method
          const value = isFunction(newValue) ? newValue(prevState) : newValue;
          /// Remove the target directly in cookies in both cases
          if (value === undefined || value === null) {
            Cookies.remove(cookieKey);
          } else {
            Cookies.set(cookieKey, value, restOptions);
          }
          /// Failure to set cookies does not affect state changes
          returnvalue; }); }, [cookieKey, options], );return [state, updateState] as const;
}

export default useCookieState;

Copy the code

useLocalStorageState & useSessionStorageState

“The Internet also has Memory – Storage Memory”

Am I the only one who thinks these hooks are bad?

import { createUseStorageState } from '.. /createUseStorageState';

const useLocalStorageState = createUseStorageState(
  typeof window= = ='object' ? window.localStorage : null,);export default useLocalStorageState;

Copy the code
import { createUseStorageState } from '.. /createUseStorageState';

const useSessionStorageState = createUseStorageState(
  typeof window= = ='object' ? window.sessionStorage : null,);export default useSessionStorageState;

Copy the code
import { useState, useCallback } from 'react';
import useUpdateEffect from '.. /useUpdateEffect';

/ / /...

function isFunction<T> (obj: any) :obj is T {
  return typeof obj === 'function';
}

export function createUseStorageState(nullishStorage: Storage | null) {
  function useStorageState<T> (key: string, defaultValue? : T | IFuncUpdater
       
        ,
       ) :StorageStateResult<T> {
    const storage = nullishStorage as Storage;
    const [state, setState] = useState<T | undefined> (() = > getStoredValue());
    useUpdateEffect(() = > {
      setState(getStoredValue());
    }, [key]);
    
    // The same idea as cookie is to change the API implementation, and then the retrieved data has been converted by parse
    function getStoredValue() {
      const raw = storage.getItem(key);
      if (raw) {
        try {
          return JSON.parse(raw);
        } catch (e) {}
      }
      if (isFunction<IFuncUpdater<T>>(defaultValue)) {
        return defaultValue();
      }
      return defaultValue;
    }
  
    /// the same idea as cookie
    const updateState = useCallback(
      (value? : T | IFuncUpdater
       ) = > {
        /// This is slightly different from cookie. Only undefined removes the value, while null is stored as a valid value
        if (typeof value === 'undefined') {
          storage.removeItem(key);
          setState(undefined);
        } else if (isFunction<IFuncUpdater<T>>(value)) {
          const previousState = getStoredValue();
          const currentState = value(previousState);
          storage.setItem(key, JSON.stringify(currentState));
          setState(currentState);
        } else {
          storage.setItem(key, JSON.stringify(value));
          setState(value);
        }
      },
      [key],
    );

    return [state, updateState];
  }
  /// if the cookie is not supported, the initial value will always be returned, and the value cannot be changed.
  /// And the initial value is recalculated with each component update
  /// so if the initial value is a method, and the initial value is used elsewhere as a dependency, it is easy to loop
  // write unit tests when storage does not exist
  if(! nullishStorage) {return function (_: string, defaultValue: any) {
      return [
        isFunction<IFuncUpdater<any>>(defaultValue) ? defaultValue() : defaultValue,
        () = >{},]; }as typeof useStorageState;
  }
  return useStorageState;
}

Copy the code

usePrevious

“Anything you say from now on can and will be used against you in a court of law.”

import { useRef } from 'react';

export type compareFunction<T> = (prev: T | undefined, next: T) = > boolean;
// each time the component is re-rendered, the current prev is recorded
/// There is a problem with this. Render that is not triggered by prev changes can also cause prev updates. So it has to be used with useSetState, right?
function usePrevious<T> (state: T, compare? : compareFunction
       ) :T | undefined {
  const prevRef = useRef<T>();
  const curRef = useRef<T>();

  const needUpdate = typeof compare === 'function' ? compare(curRef.current, state) : true;
  if (needUpdate) {
    prevRef.current = curRef.current;
    curRef.current = state;
  }

  return prevRef.current;
}

export default usePrevious;

Copy the code

The above content due to my level problem is inevitable error, welcome to discuss feedback.