在讲这几个 自定义 Hook 之前,我们可以先来熟悉一下 React 自带的一个 Hook API —— useRef
。
useRef
const myRef = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。
一个作用是用来访问 DOM ,与 React.createRef()
相似。你可以将 ref 对象,这样使用 <div ref={myRef} />
,然后 ref.current
就会一直指向这个 DOM div节点了。
另一个作用其实在 Hook 中应该是用得更加频繁的。useRef()
返回的 ref 对象 实际上可以很方便保存任何值。useRef()
会在每次渲染时返回同一个 ref 对象。
这就类似于一个 class 的实例变量,看一个例子。
function Timer() {
const intervalRef = useRef();
// mount的时候启动一个定时器
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
}, []);
}
因为setInterval
返回的值放在了 intervalRef
,所以无论 Timer
组件重新渲染多少次,interval.current
都不会改变,所以我们可以清除这个循环定时器。
function handleCancelClick() {
clearInterval(intervalRef.current);
}
所以从概念上讲,可以认为 ref 就像是一个 class 的实例变量。
useRefCallback
useRefCallback
通过使用 useRef
的特性,可以返回一个引用地址永远不变的函数。
这里就会有人问了,这不是和 useCallback
一样么?
实际上还是不一样的,useCallback
的第二个参数可以填写依赖值,如果回调所依赖的值变化了,回调就会更新。在实际开发过程中,其实内部的 state
是经常会发生变化的,这就会导致 useCallback
的效果不是很好,对于比较复杂的子组件,重新渲染会对性能造成一定影响。
使用useRefCallback
可以避免这种情况,来看看以下例子🌰(codesandbox在线例子)
export default () => {
const [count, setCount] = useState(0);
// 输入框onChange时,count发生变化 showCount1更新
const showCount1 = useCallback(() => {
alert(count);
}, [count]);
// 引用不会发生变化
const showCount2 = useRefCallback(() => {
alert(count);
});
return (
<>
<button onClick={() => setCount((c) => c + 1)}>Add Count</button>
<p>点击👆按钮可以看到子组件的渲染次数</p>
<div style={{ marginTop: 32 }}>
<h4>使用useRefCallback</h4>
<ExpensiveTree showCount={showCount2} />
</div>
<div style={{ marginTop: 32 }}>
<h4>使用useCallback</h4>
<ExpensiveTree showCount={showCount1} />
</div>
</>
);
};
// 这里使用React.memo
const ExpensiveTree = React.memo<{ [key: string]: any }>(({ showCount }) => {
const renderCountRef = useRef(0);
renderCountRef.current += 1;
return (
<div>
<p>渲染次数: {renderCountRef.current}</p>
<button onClick={showCount}>showParentCount</button>
</div>
);
});
可以看到,当点击按钮时,使用 useCallback
记忆回调的子组件,会经常发生渲染。
所以,使用 useRefCallback
来记忆回调函数,可以达到一定的性能优化效果,useRefCallback
的实现也比较简单,源码如下:
function useRefCallback<T extends (...args: any[]) => any>(fn: T) {
// 这个ref.current的引用用于更新fn
const fnRef = useRef<T>(fn);
fnRef.current = fn;
const persistFn = useRef<T>();
if (!persistFn.current) {
persistFn.current = function (...args) {
return fnRef.current(args);
} as T;
}
return persistFn.current;
}
useExecuteFn
在中后台场景中,一个按钮的点击,许多时候都需要发起一个请求,虽然是一个简单的场景,但确实一个体现细节的地方,来看看“各大工程师”的实现。
偷懒工程师:我直接就一个 onClick
完事,then
完就是 setState
。
结果就是过几天后台过来开喷了:你 ** 一个同样的请求没事发这么多次干嘛??
这里大家都应该清楚,在碰到网络请求慢的时候,大多数用户都会选择再点击一次,接着就会再次发起请求了。
锁工程师:onClick
的时候,我先给它 this.locked = true
一波。
这种处理方式就还好一点,但是依旧会出现用户多次点击的问题,用户体验不是很好。
Loading工程师:onClick
的时候,显示 loading
,有一个用户反馈,可以让用户感知到是在处理某些事情。
这是一种比较不错的处理方式了,loading
的时候也是禁用点击。但是也有许多人偷懒不做这样的处理,因为要显示 loading
态,就意味要多维护一个 state
,接着你还要去在相应的位置做 状态更改,显得比较麻烦。
所以我就在思考,有没有一种简易的方法来实现这种方式。
loading
态无需手动维护,请求开始前开始loading,请求结束后停止 loading。
于是就有 useExecuteFn
这个自定义 Hook,看看它是怎么使用的。
const [handleSubmit, submitExecuting] = useExecuteFn(async () => {
await submitTask();
});
// antd Button
// handleSubmit 执行时 submitExecuting 为true 结束后为 false
return <Button onClick={handleSubmit} loading={submitExecuting}>
这样就达到了自动化 loading
的目的了。
其源码也很简单:
export function useExecuteFn<P extends any[] = any[], V extends any = any>(
fn: (...args: P) => Promise<V>
) {
// 维护一个 executing State
const [executing, setExecuting] = useState(false);
const executeFn = useRefCallback(async (...args: P) => {
try {
setExecuting(true);
const ret = await fn(...args);
setExecuting(false);
return ret;
} catch (e) {
setExecuting(false);
throw e;
}
});
return [executeFn, executing] as const;
}
类似的,如果不需要 loading
这种,只需要一个 lock 的话,也可以 利用 useRef
实现一个 useLockFn
这样的 hook,大概实现方式也与 useExecuteFn
差不多。
usePrevious
这个 Hook 主要用于获取上一轮的 props 或 state。
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return <h1>Now: {count}, before: {prevCount}</h1>;
}
主要是通过 useRef
来实现:
function usePrevious<T>(value: T) {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
useToggle
这一个 Hook 主要是做布尔值切换的,比较常用的场景是 弹窗的打开关闭,不需要显示设置 true 或 false。
function Index() {
const [visible, toggleVisible] = useToggle(); // 这里也可以传默认值
// 也可手动设置 toggleVisible(false);
return <Modal visible={visible} onOk={toggleVisible} onCancel={toggleVisible}>xxx</Modal>;
}
源码如下:
export function useToggle(initialState = false) {
const [state, setState] = useState(initialState);
/**
* 可直接传入一个state 否则置反
*/
const toggle = useCallback((s?: any) => {
if (typeof s === "boolean") {
setState(s);
} else {
setState(prevState => !prevState);
}
}, []);
return [state, toggle] as const;
}
这里其实也可以支持 toggle 两个值,不过 TS 的类型重载搞起来略蛋疼,有空再研究研究。
大概就这些了…