本文是深入浅出 ahooks 源码系列文章的第十二篇,该系列已整顿成文档-地址。感觉还不错,给个 star 反对一下哈,Thanks。
明天咱们来聊聊 ahooks 中那些能够帮忙咱们更优雅治理咱们 state(状态)的那些 hook。一些比拟非凡的,比方 cookie/localStorage/sessionStorage,useUrlState等,咱们曾经独自拿进去细讲了,感兴趣能够看看笔者的历史文章。
useSetState
治理 object 类型 state 的 Hooks,用法与 class 组件的 this.setState 基本一致。
先来理解一下可变数据和不可变数据的含意和区别如下:
- 可变数据(mutable)即一个数据被创立之后,能够随时进行批改,批改之后会影响到原值。
- 不可变数据(Immutable) 就是一旦创立,就不能再被更改的数据。对
Immutable
对象的任何批改或增加删除操作都会返回一个新的Immutable
对象。
咱们晓得,React Function Components 中的 State 是不可变数据。所以咱们常常须要写相似如下的代码:
setObj((prev) => ({ ...prev, name: 'Gopal', others: { ...prev.others, age: '27', }}));
通过 useSetState,能够省去对象扩大运算符操作这个步骤,即:
setObj((prev) => ({ name: 'Gopal', others: { age: '27', }}));
其外部实现也比较简单,如下所示:
- 调用设置值办法的时候,会依据传入的值是否为函数。如果是函数,则入参为旧状态,输入新的状态。否则间接作为新状态。这个合乎 setState 的应用办法。
- 应用对象拓展运算符,返回新的对象,保障原有数据不可变。
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) => { // 新状态 const newState = isFunction(patch) ? patch(prevState) : patch; // 也能够通过相似 Object.assign 的形式合并 // 对象拓展运算符,返回新的对象,保障原有数据不可变 return newState ? { ...prevState, ...newState } : prevState; }); }, []); return [state, setMergeState];};
能够看到,其实就是将对象拓展运算符的操作封装到外部。
还有其余更优雅的形式?咱们能够应用 use-immer
useImmer(initialState)
十分相似于useState
。该函数返回一个元组,元组的第一个值是以后状态,第二个是updater
函数,它承受一个immer producer
函数或一个值作为参数。
应用如下:
const [person, updatePerson] = useImmer({ name: "Michel", age: 33});function updateName(name) { updatePerson(draft => { draft.name = name; });}function becomeOlder() { updatePerson(draft => { draft.age++; });}
当向更新函数传递一个函数的时候,draft
参数能够自在地扭转,直到 producer
函数完结,所做的扭转将是不可变的,并成为下一个状态。这更合乎咱们的应用习惯,能够通过 draft.xx.yy
的形式更新咱们对象的值。
useBoolean 和 useToggle
这两个都是非凡状况下的值治理。
useBoolean,优雅的治理 boolean 状态的 Hook。
useToggle,用于在两个状态值间切换的 Hook。
实际上,useBoolean 又是 useToggle 的一个非凡应用场景。
先看 useToggle。
- 这里应用了 typescript 函数重载申明入参和出参类型,依据不同的入参会返回不同的后果。比方第一个入参为 boolean 布尔值,则返回一个元组,第一项为 boolean 值,第二个为更新函数。优先级从上到下顺次变低。
- 入参可能有两个值,第一个为默认值(认为是左值),第二个是取反之后的值(认为是右值),能够不传,不传的时候,则间接依据默认值取反
!defaultValue
。 - toggle 函数。切换值,也就是下面的左值和右值的转换。
- set。间接设置值。
- setLeft。设置默认值(左值)。
- setRight。如果传入了 reverseValue, 则设置为 reverseValue。 否则设置为 defautValue 的取反值。
// TS 函数重载的应用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; // 切换 state const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue)); // 批改 state const set = (value: D | R) => setState(value); // 设置为 defaultValue const setLeft = () => setState(defaultValue); // 如果传入了 reverseValue, 则设置为 reverseValue。 否则设置为 defautValue 的反值 const setRight = () => setState(reverseValueOrigin); return { toggle, set, setLeft, setRight, }; // useToggle ignore value change // }, [defaultValue, reverseValue]); }, []); return [state, actions];}
而 useBoolean 是对 useToggle 的一个应用。如下,比较简单,不细说
export default function useBoolean(defaultValue = false): [boolean, Actions] { const [state, { toggle, set }] = useToggle(defaultValue); const actions: Actions = useMemo(() => { const setTrue = () => set(true); const setFalse = () => set(false); return { toggle, set: (v) => set(!!v), setTrue, setFalse, }; }, []); return [state, actions];}
usePrevious
保留上一次状态的 Hook。
其原理,是每次状态变更的时候,比拟值有没有发生变化,变更状态:
- 保护两个状态 prevRef(保留上一次的状态)和 curRef(保留以后状态)。
- 状态变更的时候,应用 shouldUpdate 判断是否发生变化,默认通过
Object.is
判断。开发者能够自定义 shouldUpdate 函数,并决定什么时候记录上一次状态。 - 状态发生变化,更新 prevRef 的值为上一个 curRef,并更新 curRef 为以后的状态。
const defaultShouldUpdate = <T>(a?: T, b?: T) => !Object.is(a, b);function usePrevious<T>( state: T, shouldUpdate: ShouldUpdateFunc<T> = defaultShouldUpdate,): T | undefined { // 应用了 useRef 的个性,始终放弃援用不变 // 保留上一次值 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,个别用于性能优化。
window.requestAnimationFrame()
通知浏览器——你心愿执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该办法须要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
如果你的操作是比拟频繁的,就能够通过这个 hook 进行性能优化。
- 重点看 setRafState 办法,它执行的时候,会勾销上一次的 setRafState 操作。从新通过 requestAnimationFrame 去管制 setState 的执行机会。
- 另外在页面卸载的时候,会间接勾销操作,防止内存泄露。
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); }); }, []); // unMount 的时候,去除监听 useUnmount(() => { cancelAnimationFrame(ref.current); }); return [state, setRafState] as const;}
useSafeState
用法与 React.useState 齐全一样,然而在组件卸载后异步回调内的 setState 不再执行,防止因组件卸载后更新状态而导致的内存透露。
代码如下:
- 在更新的时候,通过 useUnmountedRef 判断如果组件卸载,则进行更新。
function useSafeState<S>(initialState?: S | (() => S)) { // 判断是否卸载 const unmountedRef = useUnmountedRef(); const [state, setState] = useState(initialState); const setCurrentState = useCallback((currentState) => { // 如果组件卸载,则进行更新 if (unmountedRef.current) return; setState(currentState); }, []); return [state, setCurrentState] as const;}
useUnmountedRef 这个咱们之前提过,简略回顾下,其实就是在 hook 的返回值中标记组件为已卸载。
const useUnmountedRef = () => { const unmountedRef = useRef(false); useEffect(() => { unmountedRef.current = false; // 如果曾经卸载,则会执行 return 中的逻辑 return () => { unmountedRef.current = true; }; }, []); return unmountedRef;};
useGetState
给 React.useState 减少了一个 getter 办法,以获取以后最新值。
其实现如下:
- 其实就是通过 useRef 记录最新的 state 的值,并裸露一个 getState 办法获取到最新的。
function useGetState<S>(initialState?: S) { const [state, setState] = useState(initialState); // useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内继续存在。 // 应用 useRef 解决 state const stateRef = useRef(state); stateRef.current = state; const getState = useCallback(() => stateRef.current, []); return [state, setState, getState];}
这在某一些状况下,能够防止 React 的闭包陷阱。如官网例子:
const [count, setCount, getCount] = useGetState<number>(0);useEffect(() => { const interval = setInterval(() => { console.log('interval count', getCount()); }, 3000); return () => { clearInterval(interval); };}, []);
如果这里不应用 getCount(),而是间接应用 count,是获取不到最新的值的。
总结与思考
React 的 function Component 的状态治理还是比拟灵便,咱们能够针对一些场景进行封装和优化,从而更优雅的治理咱们的 state 状态,心愿 ahooks 这些封装能对你有所帮忙。
本文已收录到集体博客中,欢送关注~