这个系列是将 ahooks 里面的所有 hook 源码都进行解读,通过解读 ahooks 的源码来熟悉自定义 hook 的写法,提高自己写自定义 hook 的能力,希望能够对大家有所帮助。
为了和代码原始注释区分,个人理解部分使用 ///开头,此处和 三斜线指令没有关系,只是为了做区分。
往期回顾
- ahooks 源码解读系列
- ahooks 源码解读系列 - 2
- ahooks 源码解读系列 - 3
- ahooks 源码解读系列 - 4
- ahooks 源码解读系列 - 5
- ahooks 源码解读系列 - 6
- ahooks 源码解读系列 - 7
- ahooks 源码解读系列 - 8
- ahooks 源码解读系列 - 9
不知不觉已经更新到第 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;
以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。