ahooks 源码解读系列 - 10

692 阅读5分钟

这个系列是将 ahooks 里面的所有 hook 源码都进行解读,通过解读 ahooks 的源码来熟悉自定义 hook 的写法,提高自己写自定义 hook 的能力,希望能够对大家有所帮助。

为了和代码原始注释区分,个人理解部分使用 ///开头,此处和 三斜线指令没有关系,只是为了做区分。

往期回顾

不知不觉已经更新到第 10 篇了,前面几篇的反馈并没有很好,阅读数比较低,评论更是一个都没有。不清楚问题出在哪里,有没有经验丰富的大佬留言评价一下,感谢🙏~
不过不管怎样,坑已经开了就会坚持更新下去,后面还是会按照自己的节奏将剩下的 hook 解读完,感谢大家的阅读,比心~

useCounter

“给count系上安全带“

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

/// ...

/// hook 的核心作用体现,计算目标值,保证一定在设置的 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(() => {
    return getTargetValue(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;

useControllableValue

“没有你(value),地球照样能转”

如果 props 传入了 “value”,就由外部控制 state,如果没有也不影响内部的 state 变动。有点像原生的 input 组件,传了 value 就变成受控组件,不传就是非受控组件。

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

export interface 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; /// 配置 state 和 setState 托管的 key 值

  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) { /// 如果外部传入了 “value” 则同步更新当前的 state
      setState(value);
    }
  }, [value, valuePropName]);

  const handleSetState = useCallback(
    (v: T, ...args: any[]) => {
      if (!(valuePropName in props)) { /// 如果外部没有传入 “value” 则内部自行更新 state
        setState(v);
      }
      if (props[trigger]) { /// 如果外部传入了 “onChange” 则调用一下
        props[trigger](v, ...args);
      }
    },
    [props, valuePropName, trigger],
  );

  return [valuePropName in props ? value : state, handleSetState] as const;
}

export default useControllableValue;

useCookieState

“互联网也有记忆”

被存储在 cookie 中的 state,页面刷新也不会丢失。

import Cookies from 'js-cookie'; /// 使用 js-cookie 实现 cookie 管理
import { useCallback, useState } from 'react';
import { isFunction } from '../utils';

// TODO ts 命名不规范,待下个大版本修复
export type TCookieState = string | undefined | null;
export type TCookieOptions = Cookies.CookieAttributes;

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

/// 劫持状态的获取和赋值,useStorageState 等其他缓存 hook 都是同样的原理
function useCookieState(cookieKey: string, options: IOptions = {}) {
  /// 先取 cookie 中的值,没有则使用默认值,默认值可以是一个方法
  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 保证返回的 updateState 不会变化
  const updateState = useCallback(
    (
      newValue?: TCookieState | ((prevState: TCookieState) => TCookieState),
      newOptions: Cookies.CookieAttributes = {},
    ) => {
      const { defaultValue, ...restOptions } = { ...options, ...newOptions };
      setState(
        (prevState: TCookieState): TCookieState => {
          /// 新值可以是一个函数,和 useState 的赋值方法行为保持一致
          const value = isFunction(newValue) ? newValue(prevState) : newValue;
          /// 这两种情况下直接在cookie中移除目标
          if (value === undefined || value === null) {
            Cookies.remove(cookieKey);
          } else {
            Cookies.set(cookieKey, value, restOptions);
          }
          /// cookie 设置失败也不影响状态的变更
          return value;
        },
      );
    },
    [cookieKey, options],
  );

  return [state, updateState] as const;
}

export default useCookieState;

useLocalStorageState & useSessionStorageState

“互联网也有记忆 - 之 storage 分忆”

只有我一个人认为这两 hook 坑很深嘛?

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

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

export default useLocalStorageState;

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

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

export default useSessionStorageState;

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<T>,
  ): StorageStateResult<T> {
    const storage = nullishStorage as Storage;
    const [state, setState] = useState<T | undefined>(() => getStoredValue());
    useUpdateEffect(() => {
      setState(getStoredValue());
    }, [key]);
    
    /// 和 cookie 一个思路,就是换了一个 api 实现,然后取出来的数据已经 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;
    }
  
    /// 也是和 cookie 一个思路
    const updateState = useCallback(
      (value?: T | IFuncUpdater<T>) => {
        /// 这里和 cookie 有点细微差别,只有 undefined 会删除值,null 则会被当成一个合理的值进行存储
        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];
  }
  /// 这里和 cookie 有很大的不同,如果不支持 storage 则永远返回初始值,且无法对值进行变动,感觉这个逻辑很迷。。。
  /// 而且每次组件更新都会重新计算初始值
  /// 这样如果初始值是个方法,而且这个初始值被其他地方作为依赖的话,就很容易死循环了
  /// 查看源码发现也没有对 storage 不存在的时候写单元测试
  if (!nullishStorage) {
    return function (_: string, defaultValue: any) {
      return [
        isFunction<IFuncUpdater<any>>(defaultValue) ? defaultValue() : defaultValue,
        () => {},
      ];
    } as typeof useStorageState;
  }
  return useStorageState;
}

usePrevious

“从现在开始,你所说的话讲作为呈堂证供”

import { useRef } from 'react';

export type compareFunction<T> = (prev: T | undefined, next: T) => boolean;
/// 每次组件重新渲染,都会记下当前的 prev
/// 这样有个问题啊,不是 prev 变更触发的渲染也会导致 prev 被更新,这样真的不会有问题吗?也就是必须和 useSetState 同时使用?
function usePrevious<T>(state: T, compare?: compareFunction<T>): 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;

以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。