一直以来使用useRef的场景比较常见和基础,大多是为了操作已经mount的dom节点,例如设置焦点之类的,如官方例子所示:
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
最近在项目中遇到了使用 useRef 另一种用法的场景,顿时觉得真香,下面我们来分析下该场景~
useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
我们需要在react function component中实现模糊搜索,用户输入过程中触发input组件的onChange事件时获取数据,动态更新下拉列表中的数据项。但是若每次触发onchange事件都去拉取数据,会导致请求太过频繁,前端体验并不好,浪费网络资源的同时还会对后台的服务造成一定的压力,通常这时我们就要使用函数节流 throttle 了。
我们先用class component的写法来还原下:
import React from "react";
import _ from "lodash";
export default class SearchInput extends React.Component {
state = {
inputValue: "",
options: []
};
componentDidUpdate(prevProps, prevState) {
if (prevState.inputValue !== this.state.inputValue) {
this.updateOptions();
}
}
updateOptions = _.throttle(() => {
const { options, inputValue } = this.state;
const list = options.concat([inputValue]);
this.setState({ options: list });
}, 500);
handleChange = e => {
const { value } = e.target;
this.setState({ inputValue: value });
};
render() {
const { options } = this.state;
return (
<div>
<input onChange={this.handleChange} />
<br />
{options.map((item, i) => {
return <p key={i}>{item}</p>;
})}
</div>
);
}
}
节流效果如图:
没有毛病,那下面我们试试在function component中写:
import React, { useState, useEffect } from "react";
import _ from "lodash";
export default function SearchInput() {
const [inputValue, setInputValue] = useState("");
const [options, setOptions] = useState([]);
useEffect(() => {
updateOptions();
}, [inputValue]);
const updateOptions = _.throttle(() => {
const list = options.concat([inputValue]);
setOptions(list);
}, 500);
const handleChange = e => {
setInputValue(e.target.value);
};
return (
<div>
<input onChange={handleChange} />
<br />
{options.map((item, i) => {
return <p key={i}>{item}</p>;
})}
</div>
);
}
查看下节流效果:
问题来了,每次输入都触发了options的更新,根本没有节流的效果嘛...
分析后发现,由于react function component的特性,每次渲染都会生成一个新的 updateOptions 方法的副本, 而lodash中的throttled方法默认leading 为 true,即首次触发updateOptions方法时会执行options的更新,这样以来就导致了每次inputValue更新都会更新options,节流就失效啦~
既然是因为每次渲染重新生成updateOptions方法的副本导致的,那是不是用useCallback就可以了呢?
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
代码改造如下:
const updateOptions = useCallback(
_.throttle(() => {
console.log('options', options);
const list = options.concat([inputValue]);
setOptions(list);
}, 1000),
[]
);
执行结果如下:
看控制台的打印结果,函数节流确实生效了,可是为啥每次从state中获取到的options都是空数组呢?
当然又是因为函数组件的特性了,使用了useCallback之后,updateOptions方法永远是第一次渲染时的版本,其中获取的state也是第一次渲染的副本,没有随着后续组件的重新渲染而更新。
整理下我们的预期,我们希望在一个不变的方法里面,获取到可变的值。
听起来有点熟悉,是不是和useRef的官方介绍有点雷同?
本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。
如果我们把依赖可变state的方法保存在ref.current中,即使ref在组件的整个生命周期内永远不变,但是其current属性却是每一次渲染后更新的值,看起来好像是可行的!
尝试一下,改造部分如下:
const updateRef = useRef(null);
updateRef.current = () => {
const list = options.concat([inputValue]);
setOptions(list);
};
const updateOptions = useCallback(
_.throttle(() => {
updateRef.current();
}, 1000),
[]
);
执行结果:
终于成功了,撒花🎉🎉
虽然功能实现了,但是代码看起来很乱很分散,不加注释的话也不好理解,并且下一次使用函数节流时又得乱写一通,这里能不能抽成一个通用的hooks呢?
// 通用的自定义hooks
export default function useThrottle(func, wait, options) {
const funcRef = useRef(null);
funcRef.current = func;
return useCallback(
_.throttle(
() => {
funcRef.current();
},
wait,
options
),
[]
);
}
// 调用方法
const updateOptions = useThrottle(() => {
const list = options.concat([inputValue]);
setOptions(list);
}, 1000);
搞定!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。