ahooks - State

711 阅读8分钟

useSetState:管理 object 类型 state

主要原因是因为 useState 不会自动合并更新对象,大部分情况下需要我们自己手动合并,因此提供了 useSetState hooks 来解决这个问题

const useSetState = <S extends Record<string, any>>(
  initialState: S | (() => S),
): [S, SetState<S>] => {
  const [state, setState] = useState<S>(initialState);

  const setMergeState = useCallback((patch) => {
    setState((prevState) => {
      // 判断是否为 function,若是则调用 function 获取状态
      const newState = isFunction(patch) ? patch(prevState) : patch;
      // 合并
      return newState ? { ...prevState, ...newState } : prevState;
    });
  }, []);

  return [state, setMergeState];
};

useBoolean:管理 boolean 状态

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

  // 函数不会变,使用 useMemo
  const actions: Actions = useMemo(() => {
    const setTrue = () => set(true); // 设置 true
    const setFalse = () => set(false); // 设置 false
    return {
      toggle, // 切换
      set: (v) => set(!!v), // 设置值
      setTrue,
      setFalse,
    };
  }, []);

  return [state, actions];
}

useToggle:接收两个状态值,设置两个状态值来回切换

useToggle() 不传参数,则效果和 useBoolean 是一致的。

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

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

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

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

function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
  const [state, setState] = useState<D | R>(defaultValue);

  const actions = useMemo(() => {
    const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;
		// 切换,跟上一个值判断是不是一样,然后切另一个
    const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
    const set = (value: D | R) => setState(value); // 设置值
    const setLeft = () => setState(defaultValue); // 设置左值
    const setRight = () => setState(reverseValueOrigin); // 设置右值

    return {
      toggle,
      set,
      setLeft,
      setRight,
    };
    // useToggle ignore value change
    // }, [defaultValue, reverseValue]);
  }, []);

  return [state, actions];
}

useUrlState:通过 url query 来管理 state

  • navigateMode 路由方式支持 push 或者 replace
  • url 参数解析主要使用 query-string 这个库:www.npmjs.com/package/que…
  • 原来就有的 url 参数不会被清除
export interface Options {
  navigateMode?: 'push' | 'replace';
}

// 这些是 query-string 使用的参数
const parseConfig = {
  skipNull: false,
  skipEmptyString: false,
  parseNumbers: false,
  parseBooleans: false,
};

type UrlState = Record<string, any>;

const useUrlState = <S extends UrlState = UrlState>(
  initialState?: S | (() => S),
  options?: Options,
) => {
  type State = Partial<{ [key in keyof S]: any }>;
  const { navigateMode = 'push' } = options || {};

  const location = rc.useLocation();

  // react-router v5
  const history = rc.useHistory?.();
  // react-router v6
  const navigate = rc.useNavigate?.();

  const update = useUpdate();

  // hooks 传入进来的 initialState
  const initialStateRef = useRef(
    typeof initialState === 'function' ? (initialState as () => S)() : initialState || {},
  );

  // 获取当前 url 上的参数
  const queryFromUrl = useMemo(() => {
    return parse(location.search, parseConfig);
  }, [location.search]);

  // 传入进来的 initialState 与 当前 url 上的 query 参数合并
  const targetQuery: State = useMemo(
    () => ({
      ...initialStateRef.current,
      ...queryFromUrl,
    }),
    [queryFromUrl],
  );

  const setState = (s: React.SetStateAction<State>) => {
    const newQuery = typeof s === 'function' ? s(targetQuery) : s;

    // 1. 如果 setState 后,search 没变化,就需要 update 来触发一次更新。比如 demo1 直接点击 clear,就需要 update 来触发更新。
    // 2. update 和 history 的更新会合并,不会造成多次更新
    update();
    
    // 主要兼容了两个版本 react-router 的 api,一个是 history,一个是 navigate
    if (history) {
      history[navigateMode]({
        hash: location.hash,
        search: stringify({ ...queryFromUrl, ...newQuery }, parseConfig) || '?',
      });
    }
    if (navigate) {
      navigate(
        {
          hash: location.hash,
          search: stringify({ ...queryFromUrl, ...newQuery }, parseConfig) || '?',
        },
        {
          replace: navigateMode === 'replace',
        },
      );
    }
  };

  return [targetQuery, useMemoizedFn(setState)] as const;
};

useCookieState:存储 cookie

  • cookies 主要使用 js-cookie:github.com/js-cookie/j…
  • useCookieState 的时候可以传入 js-cookie 的 option,在 updateState 的时候也可以传入,会对两者进行合并
export type State = string | undefined;

// 基于 cookies 的 options 再扩展多一个属性 defaultValue
export interface Options extends Cookies.CookieAttributes {
  defaultValue?: State | (() => State);
}

// 传入 cookieKey,还有 js-cookie 的 options
function useCookieState(cookieKey: string, options: Options = {}) {
  const [state, setState] = useState<State>(() => {
    const cookieValue = Cookies.get(cookieKey);

    if (typeof cookieValue === 'string') return cookieValue;

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

    return options.defaultValue;
  });

  const updateState = useMemoizedFn(
    (
      newValue: State | ((prevState: State) => State),
      newOptions: Cookies.CookieAttributes = {},
    ) => {
      // options 是一开始初始化时候的 options,newOptions 是 update 的时候动态传入进来的
      // 把 defaultValue 剔除掉,然后用 ...restOptions 去接收剩余所有参数
      const { defaultValue, ...restOptions } = { ...options, ...newOptions };
      setState((prevState) => {
        const value = isFunction(newValue) ? newValue(prevState) : newValue;
        // 如果值是 undefined 的话,则是删除掉这个 cookie
        if (value === undefined) {
          Cookies.remove(cookieKey);
        } else {
          Cookies.set(cookieKey, value, restOptions);
        }
        return value;
      });
    },
  );

  return [state, updateState] as const;
}

useLocalStorageState:将状态传入到 localStorage 里面

这里的 useLocalStorage 使用 createUseStorageState 包装了一层,兼容服务端渲染

const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));

options 可以设置 serializer 和 deserializer 自定义序列化与反序列化方法,在 hooks 内有这么一层函数封装:

// 如果定义了 serializer 方法,则优先调用 serializer 方法,否则默认是 JSON.stringify
const serializer = (value: T) => {
  if (options?.serializer) {
    return options?.serializer(value);
  }
  return JSON.stringify(value);
};

// 如果定义了 deserializer 方法,则优先调用 deserializer 方法,否则默认是 JSON.parse
const deserializer = (value: string) => {
  if (options?.deserializer) {
    return options?.deserializer(value);
  }
  return JSON.parse(value);
};

获取 state 方法:

function getStoredValue() {
  // 首先从 storage 获取值
  try {
    const raw = storage?.getItem(key);
    if (raw) {
      return deserializer(raw);
    }
  } catch (e) {
    console.error(e);
  }
  
  // 如果获取不到,则获取默认值
  if (isFunction<IFuncUpdater<T>>(options?.defaultValue)) {
    return options?.defaultValue();
  }
  return options?.defaultValue;
}

更新 state 方法(这里的 else if 与 else 部分,应该还能做代码优化,合并成一个,判断下是不是 function,然后统一取一个值操作即可):

const updateState = (value?: T | IFuncUpdater<T>) => {
  // 如果设置的值 value 是 undefined 的话,就清空这个值
  if (typeof value === 'undefined') {
    setState(undefined);
    storage?.removeItem(key);
  } else if (isFunction<IFuncUpdater<T>>(value)) {
    // 这里是函数的情况
    const currentState = value(state);
    try {
      setState(currentState);
      storage?.setItem(key, serializer(currentState));
    } catch (e) {
      console.error(e);
    }
  } else {
    // 这里是值的情况
    try {
      setState(value);
      storage?.setItem(key, serializer(value));
    } catch (e) {
      console.error(e);
    }
  }
};

state 的保存与写入:

// 第一次初始化时,获取 localStorage 对应 key 的值,并写入 state
const [state, setState] = useState<T | undefined>(() => getStoredValue());

// 忽略第一次加载,在当 key 发生变化时,重新获取获取 localStorage 对应 key 的值,并写入 state
useUpdateEffect(() => {
  setState(getStoredValue());
}, [key]);

useSessionStorageState:将状态传入 sessionStorage 里面

与 useLocalStorageState 一致

useDebounce:处理防抖【值】

基于 useDebounceFn 封装的 useDebounce,建议先阅读 Effect 篇的 useDebounceFn

针对传入 value 值的防抖,调用 useDebounceFn 返回防抖函数,value 改变时,触发防抖函数即可

import { useEffect, useState } from 'react';
import useDebounceFn from '../useDebounceFn';
import type { DebounceOptions } from './debounceOptions';

function useDebounce<T>(value: T, options?: DebounceOptions) {
  const [debounced, setDebounced] = useState(value);

  const { run } = useDebounceFn(() => {
    setDebounced(value);
  }, options);

  useEffect(() => {
    run();
  }, [value]);

  return debounced;
}

useThrottle:处理节流【值】

基于 useThrottleFn 封装的 useThrottle,建议先阅读 Effect 篇的 useThrottleFn

针对传入 value 值的防抖,调用 useThrottleFn 返回节流函数,value 改变时,触发节流函数即可

import { useEffect, useState } from 'react';
import useThrottleFn from '../useThrottleFn';
import type { ThrottleOptions } from './throttleOptions';

function useThrottle<T>(value: T, options?: ThrottleOptions) {
  const [throttled, setThrottled] = useState(value);

  const { run } = useThrottleFn(() => {
    setThrottled(value);
  }, options);

  useEffect(() => {
    run();
  }, [value]);

  return throttled;
}

useMap:管理 Map 状态

传入 initialValue 作为默认值,返回几个扩展方法:

  • map:当前 map 值
  • reset:重置为默认值

这两个可以一起看,封装了一个 getInitValue 方法,获取当前的 hooks 传进来的 initialValue,reset 的时候重新调用 getInitValue 获取值再 setMap 即可

const getInitValue = () => {
  return initialValue === undefined ? new Map() : new Map(initialValue);
};

const [map, setMap] = useState<Map<K, T>>(() => getInitValue());

const reset = () => setMap(getInitValue());
  • set:添加元素,每次都是拿到上次的 map,重新 new 一个,再 set 新的 key-value
const set = (key: K, entry: T) => {
  setMap((prev) => {
    const temp = new Map(prev);
    temp.set(key, entry);
    return temp;
  });
};
  • get:获取元素,直接调用 map 的 get 方法即可
const get = (key: K) => map.get(key);
  • setAll:生成新的 Map
const setAll = (newMap: Iterable<readonly [K, T]>) => {
  setMap(new Map(newMap));
};
  • remove:移除某个元素
const remove = (key: K) => {
  setMap((prev) => {
    const temp = new Map(prev);
    temp.delete(key);
    return temp;
  });
};

这里可以看到 useMap 的 initailValue 参数和 setAll 的 newMap 参数,类型都是 Iterable<readonly [K, T]>,说明可以传入 new Array([x, x]) 或者 new Map([x, x]) 进行初始化或者重新设置参数,关于 Iterable 的说明:www.typescriptlang.org/docs/handbo…

useSet:管理 Set 状态

传入 initialValue 作为默认值,返回几个扩展方法:

  • set:当前 set 值
  • reset:重置默认值

这两个可以一起看,封装了一个 getInitValue 方法,获取当前的 hooks 传进来的 initialValue,reset 的时候重新调用 getInitValue 获取值再 setSet 即可

const getInitValue = () => {
  return initialValue === undefined ? new Set<K>() : new Set(initialValue);
};
const [set, setSet] = useState<Set<K>>(() => getInitValue());
const reset = () => setSet(getInitValue());
  • add:添加元素,先判断 has 是否已经有该值,有的话则直接 return,没有则调用 set 的 add 即可
const add = (key: K) => {
  if (set.has(key)) {
    return;
  }
  setSet((prevSet) => {
    const temp = new Set(prevSet);
    temp.add(key);
    return temp;
  });
};
  • remove:移除元素,先判断 has 是否存在该值,若没有则直接 return,有的话则调用 delete 删除即可
const remove = (key: K) => {
  if (!set.has(key)) {
    return;
  }
  setSet((prevSet) => {
    const temp = new Set(prevSet);
    temp.delete(key);
    return temp;
  });
};

usePrevious:保存上一个状态

  • 使用两个值分别保存当前值 curRef 和上一个值 prevRef
  • 调用 shouldUpdate 查看是否需要更新 curRef 与 prevRef,如果不传入,默认是两值不一样时就触发更新
export type ShouldUpdateFunc<T> = (prev: T | undefined, next: T) => boolean;

const defaultShouldUpdate = <T>(a?: T, b?: T) => a !== b;

function usePrevious<T>(
  state: T,
  shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,
): T | undefined {
  const prevRef = useRef<T>();
  const curRef = useRef<T>();

  if (shouldUpdate(curRef.current, state)) {
    prevRef.current = curRef.current;
    curRef.current = state;
  }

  return prevRef.current;
}

useRafState:只在 requestAnimationFrame callback 时更新 state,用于性能优化

developer.mozilla.org/zh-CN/docs/…

  • requestAnimationFrame 是将所有的动画都放到一个浏览器重绘周期里去做,该方法接收一个回调函数,会在下一次浏览器重绘之前去执行

  • ref 记录事件监听函数,组件 unmount 的时候 cancelAnimationFrame 取消事件注册
  • 每次调用 setRafState 时,都把上次注册的 requestAnimationFrame 监听事件取消,再重新注册 requestAnimationFrame 回调,在回调函数内调用 setState
function useRafState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
function useRafState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

function useRafState<S>(initialState?: S | (() => S)) {
  const ref = useRef(0);
  const [state, setState] = useState(initialState);

  const setRafState = useCallback((value: S | ((prevState: S) => S)) => {
    cancelAnimationFrame(ref.current);

    ref.current = requestAnimationFrame(() => {
      setState(value);
    });
  }, []);

  useUnmount(() => {
    cancelAnimationFrame(ref.current);
  });

  return [state, setRafState] as const;
}

useSafeState:组件卸载后,异步回调内的 setState 不再执行,避免因组件卸载后更新状态而导致的内存泄漏问题

  • 使用 unmountedRef 判断当前组件是否已经销毁,如果是,则直接 return,不是则调用 setState 设置值
import { useCallback, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import useUnmountedRef from '../useUnmountedRef';

function useSafeState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

function useSafeState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

function useSafeState<S>(initialState?: S | (() => S)) {
  const unmountedRef = useUnmountedRef();
  const [state, setState] = useState(initialState);
  const setCurrentState = useCallback((currentState) => {
    /** if component is unmounted, stop update */
    if (unmountedRef.current) return;
    setState(currentState);
  }, []);

  return [state, setCurrentState] as const;
}
  • 例子中这里要注意的是,组件的确是销毁了,但是里面的异步操作的确也会执行到,只是说 setValue 是 set 不到的,比如这里还是会 console.log 出来的
const Child = () => {
  const [value, setValue] = useSafeState<string>();

  useEffect(() => {
    setTimeout(() => {
      console.log(435432543252);
      setValue('data loaded from server');
    }, 5000);
  }, []);

  const text = value || 'Loading...';

  return <div>{text}</div>;
};

useGetState:扩展 React.useState 方法,增加一个 getter,获取最新的值

使用 useRef 记录最新的值

type GetState<S> = () => S;

function useGetState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>, GetState<S>] {
  const [state, setState] = useState<S>(initialState);
  const stateRef = useRef<S>(state);
  stateRef.current = state;

  const getState = useCallback<GetState<S>>(() => stateRef.current, []);

  return [state, setState, getState];
}

可以查看例子,【快速】点击按钮 6 次:

  • 按钮上的 count 值,从 1 - 6 逐一变化
  • useEffect 内定义了计数器,每 3s 输出一个结果,可以看到,getCount 一直都是输出 6,1 - 5 的数值没有输出出来