当前位置:网站首页>这些 hook 更优雅的管理你的状态
这些 hook 更优雅的管理你的状态
2022-08-01 20:58:00 【GopalFeng】
本文是深入浅出 ahooks 源码系列文章的第十二篇,这个系列的目标主要有以下几点:
- 加深对 React hooks 的理解。
- 学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。
- 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。
今天我们来聊聊 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[1]
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 这些封装能对你有所帮助。
参考资料
[1]
use-immer: https://github.com/immerjs/use-immer
边栏推荐
- Based on FPGA in any number of bytes (single-byte or multibyte) serial port (UART) to send (including source engineering)
- Interpretation of the meaning of each dimension of two-dimensional, three-dimensional, and four-dimensional matrices
- 写给刚进互联网圈子的人,不管你是开发,测试,产品,运维都适用
- 【Untitled】
- 】 【 nn. The Parameter () to generate and why do you want to initialize
- OSG Notes: Set DO_NOT_COMPUTE_NEAR_FAR to manually calculate far and near planes
- Go Atomic
- Goroutine Leaks - The Forgotten Sender
- 徒步,治好了我的精神内耗
- Convolutional Neural Network (CNN) mnist Digit Recognition - Tensorflow
猜你喜欢
Application of Acrel-5010 online monitoring system for key energy consumption unit energy consumption in Hunan Sanli Group
技能大赛训练:A部分加固题目
Godaddy域名解析速度慢问题以及如何使用DNSPod解析解决
【节能学院】数据机房中智能小母线与列头柜方案的对比分析
WeChat applet cloud development | personal blog applet
wps excel 插入公式 整列
【个人作品】记之-串口日志记录工具
C语言之字符串函数二
What is the difference between a utility model patent and an invention patent?Understand in seconds!
Little data on how to learn?Jida latest small learning data review, 26 PDF page covers the 269 - page document small data learning theory, method and application are expounded
随机推荐
和我一起写一个音乐播放器,听一首最伟大的作品
[Energy Conservation Institute] Application of Intelligent Control Device in High Voltage Switchgear
The configuration manual for the secondary development of the XE training system of the missing moment document system
Batch get protein .pdb files based on Uniprot ID/PDB ID
tiup mirror init
【Kaggle】Classify Leaves
tiup mirror
OSG笔记:设置DO_NOT_COMPUTE_NEAR_FAR,手动计算远近平面
漏洞分析丨HEVD-0x6.UninitializedStackVariable[win7x86]
tiup mirror merge
【Kaggle】House Prices
徒步,治好了我的精神内耗
latex paper artifact -- server deployment overleaf
[译] 容器和 Kubernetes 中的退出码完整指南
Go Atomic
虚拟机的IP地址自动变为127.0.0.1
Redis does web page UV statistics
【Social Media Marketing】How to know if your WhatsApp is blocked?
New graduate students, great experience in reading English literature, worthy of your collection
StringTable Detailed String Pool Performance Tuning String Concatenation