在我们平时使用React的时候,对于React中的Ref的属性,相信大家使用的频率是很低的。说实话,真正了解React Ref属性的人少之又少,我都不确定自己是否真正的了解了所有的内容,毕竟它不是一个经常能够被人使用的属性,而且在过去一段时间,它本身的API在不断修改。那么在本教程中,我将尽可能的向大家介绍React中的Ref
这些内容相信已经被大家写烂了,毕竟好处多大家才会使用,这里简单总结三点
过去,在class component中,React Ref经常与DOM保持紧密关联,但是自从出现了React Hook以后,Ref的使用也不再变得只是与Dom相关的Api,而是可以表示对任何内容的引用(DOM节点,JavaScript值等)
那么接下来,我们先看看不带DOM的Ref,然后我们再结合DOM了解其如何使用
首先看以下栗子🌰:
function Counter() {
const [count, setCount] = useState(0);
function onClick() {
const newCount = count + 1;
setCount(newCount);
}
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}
这是一个简单的递增函数组件,接下来我们将引用了React.useRef
这个API,这是React为函数式组件使用Ref时提供的最新API。简单来说,useRef Hook向我们返回一个可变对象,该对象在React组件的生命周期内保持不变。具体来说就是返回的对象具有当前属性,该属性可以保存我们任何可以修改的值
function Counter() {
const hasClickedButton = useRef(false);
const [count, setCount] = useState(0);
function onClick() {
const newCount = count + 1;
setCount(newCount);
hasClickedButton.current = true;
console.log("hasClickedButton",hasClickedButton)
}
console.log('Has clicked button? ' + hasClickedButton.current);
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}
控制台打印结果:
可以看出,实际上useRef Hook返回值是一个对象,其具有两个特点:
其实单看上面代码并不能明确单独Ref的改变是否会引起页面的重新渲染,看看下面这个栗子🌰:
function Counter() {
const hasClickedButton = useRef(false);
const [count, setCount] = useState(0);
function onClick() {
// const newCount = count + 1;
// setCount(newCount);
hasClickedButton.current = true;
}
console.log('Has clicked button? ' + hasClickedButton.current);
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}
可以看出,单独的ref重新赋值,并不会使组件重新渲染,无论何时需要,我们都可以将ref的当前属性重新分配给新值,他的存在仅仅相当于一个状态,那么就有一个疑问了,我们引用他的作用是什么呢?
function ComponentWithRefInstanceVariable() {
const [count, setCount] = useState(0);
function onClick() {
setCount(count + 1);
}
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
}
});
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
<p>{isFirstRender.current ? 'First render.' : 'Re-render.'}</p>
</div>
);
}
这段代码向我们展示了将ref的current属性设置为false是不会触发重新渲染的。利用这一特性,我们可以创建一个useEffect挂钩,该挂钩仅在每次组件更新时都运行其逻辑,而不在初始渲染时运行。这肯定是每个React开发人员在某个时候都需要的功能,但是React的useEffect Hook没有提供此功能
function ComponentWithRef() {
const [count, setCount] = useState(0);
function onClick() {
setCount(count + 1);
}
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
} else {
console.log("re-render");
}
});
return (
<div>
<p>{count}</p>
<button type="button" onClick={onClick}>
Increase
</button>
</div>
);
}
可以知道,每当需要跟踪React组件中的状态而该状态不应该触发组件的重新渲染时,都可以使用React的useRef Hooks为其创建一个实例变量。
接下来让我们回归到最原始的Ref使用:Dom。通常,每当必须与HTML元素进行交互时,我们都会选择使用React的ref。React本质上是声明性的,但是有时您需要从HTML元素读取值,与HTML元素的API交互,甚至必须将值写入HTML元素。对于这些罕见的情况,您必须使用React的refs以强制性而非声明性的方式与DOM进行交互。
下面这个🌰将简单的展示Ref与Dom的交互:
function App() {
return (
<ComponentWithDomApi
label="Label"
isFocus
/>
);
}
function Test({ label, isFocus }) {
const refs = useRef();
useEffect(() => {
if (isFocus) {
console.log(ref)
refs.current.focus();)
}
}, [isFocus]);
return (
<label>
{label}: <input type="text" ref={refs} />
{/* ref对象提供给HTML元素作为ref HTML属性。React为我们自动将此HTML元素的DOM节点分配给ref对象。 */}
</label>
);
}
通过ref获取dom的width:
function Test() {
const [text, setText] = useState('Some text ...');
function handleOnChange(event) {
setText(event.target.value);
}
const ref =useRef();
useEffect(() => {
const { width } = ref.current.getBoundingClientRect();
document.title = `Width:${width}`;
console.log("ref",ref.current.getBoundingClientRect().width)
}, [text]);
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}
使用Ref管理Button的状态是一个不错的选择,但是注意,是按钮的状态,而不是组件的状态。
让我们考虑一个真实的场景。表单已经完成,提交按钮需要从默认的禁用状态启用提交状态。仅为了执行此操作而重新渲染我的整个表单将会执行以下步骤:
但如果使用Ref控制按钮以切换其disabled属性,就简单的多了:
buttonRef.current.setAttribute("disabled", true);
// or
buttonRef.current.removeAttribute("disabled");
通过上述两个🌰可以看到Ref与Dom妙不可言的关系,但实际上我们还可以通过使用回调函数来实现我们想要的一系列操作。
function Test() {
const [text, setText] = useState('Some text ...');
function handleOnChange(event) {
setText(event.target.value);
}
const ref = (node) => {
if (!node) return;
const { width } = node.getBoundingClientRect();
document.title = `Width:${width}`;
console.log("node",node) // => <span>Some text ...<span>
};
return (
<div>
<input type="text" value={text} onChange={handleOnChange} />
<div>
<span ref={ref}>{text}</span>
</div>
</div>
);
}
回调ref就是一个可用于JSX中HTML元素的ref属性的函数。该函数可以访问DOM节点,并且只要在HTML元素的ref属性上使用该函数,就会触发该函数。本质上,它的作用与以前的副作用相同,但是这次回调ref本身通知我们它已附加到HTML元素
我们还可以通过使用Ref对Dom进行样式的读写,但是不建议使用,故不做解释
function Test() {
const [timer, setTimer] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setTimer(pre => pre + 1)
}, 1000)
}, [])
return (
<div>
HookTimer - {timer}
<br />
<button
onClick={() => {
clearInterval(intervalRef.current)
}}
>Clear Hook Timer</button>
</div>
)
const useFn = (fn, time) => useEffect(
() => {
const tick = setInterval(fn);
return () => clearInterval(tick);
},
[fn, time]
);
useFn(() => setCounter(counter => counter + 1));
会发现和我们预期的“每秒计数加1”不同,这个定时器执行频率会变得非常诡异。因为你传入的fn每一次都在变化,每一次都导致useEffect销毁前一个定时器,打开一个新的定时器,所以简而言之,如果1秒之内没有重新渲染,定时器会被执行,而如果有新的渲染,定时器会重头再来,这让频率变得不稳定。
function Test() {
const [counter, setCounter] = useState(0);
const useFn = (fn, time) => {
const callback = useRef(fn);
callback.current = fn;
useEffect(() => {
const tick = setTimeout(callback.current);
return () => clearTimeout(tick);
}, [time]);
};
useFn(() => setCounter((counter) => counter + 1), 1000);
return <div>计时器:{counter}</div>;
}
function FocusInput() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current && inputRef.current.focus()
}, [])
return (
<div>
<input ref={inputRef} type="text" />
</div>
)
}
至此,向大家介绍了整个react ref的使用方式以及对应的场景,如果大家还有什么问题,欢迎提出!