ahooks 源码解读系列 - 9

616 阅读4分钟

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

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

往期回顾

每日一篇,提神醒脑~ 今天进入 State 部分 hooks

State

useUrlState

“将内部状态写在脸上”

同步指定状态到页面路径的搜索参数中

/// 使用 query-string 来解析路径
import { parse, stringify } from 'query-string';
import { useMemo, useRef, useState } from 'react';
/// 要求使用 `react-router` 5.0 以上版本
import { useHistory, useLocation } from 'react-router';

export interface Options {
  navigateMode?: 'push' | 'replace'; /// 状态变更时使用 push 模式还是 replace 修改路径
}

const parseConfig = {
  skipNull: true,
  skipEmptyString: true,
  parseNumbers: false,
  parseBooleans: false,
};
interface UrlState {
  [key: string]: any;
}

export default <S extends UrlState = UrlState>(initialState?: S | (() => S), options?: Options) => {
  type state = Partial<{ [key in keyof S]: any }>;
  const { navigateMode = 'push' } = options || {};
  const location = useLocation();
  const history = useHistory();

  const [, update] = useState(false);

  const initialStateRef = useRef(
    /// 支持传入对象或者一个得到对象的方法来作为初始值
    typeof initialState === 'function' ? (initialState as () => S)() : initialState || {},
  );

  /// 解析当前路径的 search 部分
  const queryFromUrl = useMemo(() => {
    return parse(location.search, parseConfig);
  }, [location.search]);
  
  /// 根据当前路径和初始值得到当前的初始搜索条件,也就是我们需要的当前状态
  const targetQuery: state = useMemo(
    () => ({
      ...initialStateRef.current,
      ...queryFromUrl,
    }),
    [queryFromUrl],
  );

  const setState = (s: React.SetStateAction<state>) => {
    /// 同样支持传入对象或者方法
    const newQuery = typeof s === 'function' ? (s as Function)(targetQuery) : s;
    
    /// 和 useUpdate hook 功能一致
    // 1. 如果 setState 后,search 没变化,就需要 update 来触发一次更新。比如 demo1 直接点击 clear,就需要 update 来触发更新。
    // 2. update 和 history 的更新会合并,不会造成多次更新
    update((v) => !v);
    /// 变更页面路径。由于状态是由当前路径计算得到的,所以相当于变更了状态值
    history[navigateMode]({
      hash: location.hash,
      search: stringify({ ...queryFromUrl, ...newQuery }, parseConfig) || '?',
    });
  };

  return [targetQuery, setState] as const;
};

useSetState

“别整这些没用的,我就会一把梭”

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

const useSetState = <T extends object>(
  initialState: T = {} as T,
): [T, (patch: Partial<T> | ((prevState: T) => Partial<T>)) => void] => {
  const [state, setState] = useState<T>(initialState);
  /// 使用 merge 而不是替换设置新值,和 class 组件的 `this.setState` 基本一致
  const setMergeState = useCallback((patch) => {
    setState((prevState) => ({ ...prevState, ...(isFunction(patch) ? patch(prevState) : patch) }));
  }, []);

  return [state, setMergeState];
};

export default useSetState;

useToggle

“万能开关”

import { useState, useMemo } from 'react';

type IState = string | number | boolean | undefined;

export interface Actions<T = IState> {
  setLeft: () => void;
  setRight: () => void;
  toggle: (value?: T) => void;
}

function useToggle<T = boolean | undefined>(): [boolean, Actions<T>];

function useToggle<T = IState>(defaultValue: T): [T, Actions<T>];

function useToggle<T = IState, U = IState>(
  defaultValue: T,
  reverseValue: U,
): [T | U, Actions<T | U>];

function useToggle<D extends IState = IState, R extends IState = IState>(
  defaultValue: D = false as D, /// 默认值为 false
  reverseValue?: R,
) {
  const [state, setState] = useState<D | R>(defaultValue);

  const actions = useMemo(() => {
    /// 如果没有设置取反值则直接对正值取反
    const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;

    // 切换返回值
    const toggle = (value?: D | R) => {
      // 强制返回状态值,适用于点击操作
      if (value !== undefined) {
        setState(value);
        return;
      }
      /// 使用 === 来和正值比较,如果相同则设置状态为反值,否则设置为正值
      /// 所以整个逻辑都是没有对反值做过比较的,也就是说反值随便是什么乱七八糟的值都无关紧要,只会对正值进行 === 判断
      setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
    };
    // 设置默认值
    const setLeft = () => setState(defaultValue);
    // 设置取反值
    const setRight = () => setState(reverseValueOrigin);
    return {
      toggle,
      setLeft,
      setRight,
    };
  }, [defaultValue, reverseValue]);

  return [state, actions];
}

export default useToggle;

useBoolean

一个并没有什么用的管理布尔值的 hook

import { useMemo } from 'react';
import useToggle from '../useToggle';

export interface Actions {
  setTrue: () => void;
  setFalse: () => void;
  toggle: (value?: boolean | undefined) => void;
}

export default function useBoolean(defaultValue = false): [boolean, Actions] {
  const [state, { toggle }] = useToggle(defaultValue);

  const actions: Actions = useMemo(() => {
    /// 使用 useToggle 实现布尔值切换,但是 useToggle 默认就是切换布尔值,直接用 useToggle 不香嘛?
    const setTrue = () => toggle(true);
    const setFalse = () => toggle(false);
    return { toggle, setTrue, setFalse };
  }, [toggle]);

  return [state, actions];
}

useMap & useSet

“给我盯紧那小子”

可以被监听到变化版本的 Map 和 Set

import { useMemo, useState, useCallback } from 'react';

function useMap<K, T>(initialValue?: Iterable<readonly [K, T]>) {
  const initialMap = useMemo<Map<K, T>>(
    () => (initialValue === undefined ? new Map() : new Map(initialValue)) as Map<K, T>,
    [],
  );
  const [map, setMap] = useState(initialMap);

  const stableActions = useMemo(
    () => ({
      set: (key: K, entry: T) => {
        setMap((prev) => {
          const temp = new Map(prev);
          temp.set(key, entry);
          return temp;
        });
      },
      setAll: (newMap: Iterable<readonly [K, T]>) => {
        setMap(new Map(newMap));
      },
      remove: (key: K) => {
        setMap((prev) => {
          const temp = new Map(prev);
          temp.delete(key);
          return temp;
        });
      },
      reset: () => setMap(initialMap),
    }),
    [setMap, initialMap],
  );

  const utils = {
    get: useCallback((key) => map.get(key), [map]),
    ...stableActions,
  };

  return [map, utils] as const;
}

export default useMap;

/* eslint-disable max-len */
import { useState, useMemo, useCallback } from 'react';

function useSet<K>(initialValue?: Iterable<K>) {
  const initialSet = useMemo<Set<K>>(
    () => (initialValue === undefined ? new Set() : new Set(initialValue)) as Set<K>,
    [],
  );
  const [set, setSet] = useState(initialSet);

  const stableActions = useMemo(
    () => ({
      add: (key: K) => {
        setSet((prevSet) => {
          const temp = new Set(prevSet);
          temp.add(key);
          return temp;
        });
      },
      remove: (key: K) => {
        setSet((prevSet) => {
          const temp = new Set(prevSet);
          temp.delete(key);
          return temp;
        });
      },
      reset: () => setSet(initialSet),
    }),
    [setSet, initialSet],
  );

  const utils = {
    has: useCallback((key: K) => set.has(key), [set]),
    ...stableActions,
  };

  return [set, utils] as const;
}

export default useSet;

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