React Refs:完整的故事
可变数据存储
带 Refs 的可视计时器
DOM 元素引用
组件引用
类组件引用
单向流
将数据添加到参考
React Refs 中useEffect
回调引用
useState
参考文献
结论
编程术语可能相当令人困惑。我第一次听说“React Refs”是在获取 DOM 节点引用的上下文中。然而,随着 hooks 的引入,useRef
它扩展了“refs”的定义。
今天,我们将介绍 ref 的两个定义:
我们还将探索这两个定义各自的附加功能,例如组件 refs、向 ref 添加更多属性,甚至探索与使用相关的常见代码陷阱useRef
。
由于本篇内容主要依赖于
useRef
钩子,因此我们将在所有示例中使用函数式组件。不过,我们也有一些 API,例如React.createRef
类实例变量,可以用来重新创建React.useRef
类的功能。
可变数据存储
虽然useState
是最常见的数据存储钩子,但它并非唯一的钩子。React 的useRef
钩子功能与 不同useState
,但它们都用于在渲染过程中持久化数据。
const ref = React.useRef();
ref.current = "Hello!";
在此示例中,初始渲染后将ref.current
包含"Hello!"
。 返回的值useRef
是一个包含单个键的对象:current
。
如果您运行以下代码:
const ref = React.useRef();
console.log(ref)
你会发现{current: undefined}
控制台打印了一条信息。这是所有 React Refs 的形状。如果你查看 Hooks 的 TypeScript 定义,你会看到类似这样的内容:
// React.d.ts
interface MutableRefObject {
current: any;
}
function useRef(): MutableRefObject;
为什么要useRef
依赖于将数据存储在属性中current
?这是因为这样你就可以利用 JavaScript 的“按引用传递”功能来避免渲染。
现在,您可能会认为useRef
钩子的实现如下:
// This is NOT how it's implemented
function useRef(initial) {
const [value, setValue] = useState(initial);
const [ref, setRef] = useState({ current: initial });
useEffect(() => {
setRef({
get current() {
return value;
},
set current(next) {
setValue(next);
}
});
}, [value]);
return ref;
}
然而事实并非如此。引用 Dan Abramov 的话:
...
useRef
工作原理更像这样:function useRef(initialValue) { const [ref, ignored] = useState({ current: initialValue }) return ref }
由于这种实现,当你改变current
值时,它不会导致重新渲染。
由于数据存储无需渲染,它对于存储需要引用但无需在屏幕上渲染的数据特别有用。计时器就是一个这样的例子:
const dataRef = React.useRef();
const clearTimer = () => {
clearInterval(dataRef.current);
};
React.useEffect(() => {
dataRef.current = setInterval(() => {
console.log("I am here still");
}, 500);
return () => clearTimer();
}, [dataRef]);
带 Refs 的可视计时器
虽然计时器有不渲染值的用途,但如果我们让计时器渲染状态值,会发生什么?
让我们继续之前的例子,但是在里面setInterval
,我们更新useState
包含数字的,以将一添加到其状态。
const dataRef = React.useRef();
const [timerVal, setTimerVal] = React.useState(0);
const clearTimer = () => {
clearInterval(dataRef.current);
}
React.useEffect(() => {
dataRef.current = setInterval(() => {
setTimerVal(timerVal + 1);
}, 500)
return () => clearInterval(dataRef.current);
}, [dataRef])
return (
<p>{timerVal}</p>
);
现在,我们期望看到计时器在继续渲染时从1
到(以及之后)更新。但是,如果我们在应用运行时查看它,我们会看到一些我们可能意想不到的行为:2
这是因为传递给 的闭包已过期。这是使用 React Hooks 时常见的问题。虽然的 APIsetInterval
中隐藏了一个简单的解决方案,但让我们使用 突变 和 来解决这个问题。useState
useRef
因为useRef
依赖于通过引用传递并改变该引用,如果我们简单地引入第二个useRef
并在每次渲染时对其进行变异以匹配值useState
,我们就可以解决陈旧闭包的限制。
const dataRef = React.useRef();
const [timerVal, setTimerVal] = React.useState(0);
const timerBackup = React.useRef();
timerBackup.current = timerVal;
const clearTimer = () => {
clearInterval(dataRef.current);
};
React.useEffect(() => {
dataRef.current = setInterval(() => {
setTimerVal(timerBackup.current + 1);
}, 500);
return () => clearInterval(dataRef.current);
}, [dataRef]);
- 我不会在生产中用这种方式解决它。
useState
接受一个回调,您可以将其用作替代(更推荐)路线:const dataRef = React.useRef(); const [timerVal, setTimerVal] = React.useState(0); const clearTimer = () => { clearInterval(dataRef.current); }; React.useEffect(() => { dataRef.current = setInterval(() => { setTimerVal(tVal => tVal + 1); }, 500); return () => clearInterval(dataRef.current); }, [dataRef]);
我们只是用 a
useRef
来概括 refs 的一个重要属性:突变。
DOM 元素引用
在本文开头,我提到ref
s 不仅仅是一种可变数据存储方法,还是一种从 React 内部引用 DOM 节点的方法。跟踪 DOM 节点最简单的方法是useRef
使用任意元素的ref
属性将其存储在钩子中:
const elRef = React.useRef();
React.useEffect(() => {
console.log(elRef);
}, [elRef]);
return (
<div ref={elRef}/>
)
请记住,该
ref
属性由 React 在任何 HTML 元素上添加和处理。本例中使用了div
,但这也适用于span
s 和header
s 以及更多,哦天哪。
在此示例中,如果我们查看console.log
中的,我们会在 属性中useEffect
找到一个HTMLDivElement
实例current
。打开以下 StackBlitz 并查看控制台值以确认:
因为elRef.current
现在是HTMLDivElement
,这意味着我们现在可以访问整个Element.prototype
JavaScript API。因此,它elRef
可以用于设置底层 HTML 节点的样式:
const elRef = React.useRef();
React.useEffect(() => {
elRef.current.style.background = 'lightblue';
}, [elRef]);
return (
<div ref={elRef}/>
)
替代语法
值得注意的是,该ref
属性也接受一个函数。虽然我们以后会更多地讨论这一点,但请注意,此代码示例所做的与 完全相同ref={elRef}
:
const elRef = React.useRef();
React.useEffect(() => {
elRef.current.style.background = 'lightblue';
}, [elRef]);
return (
<div ref={ref => elRef.current = ref}/>
)
组件引用
HTML 元素是 s 的绝佳用例ref
。然而,在很多情况下,你需要为子组件渲染过程中的元素提供 ref。我们如何将 ref 从父组件传递给子组件?
通过将属性从父组件传递给子组件,你可以将 ref 传递给子组件。例如:
const Container = ({children, divRef}) => {
return <div ref={divRef}/>
}
const App = () => {
const elRef = React.useRef();
React.useEffect(() => {
if (!elRef.current) return;
elRef.current.style.background = 'lightblue';
}, [elRef])
return (
<Container divRef={elRef}/>
);
你可能想知道为什么我没有将该属性命名ref
为divRef
。这是因为 React 的一个限制。如果我们尝试将该属性的名称改为ref
,就会出现一些意想不到的后果。
// This code does not function as intended
const Container = ({children, ref}) => {
return <div ref={ref}/>
}
const App = () => {
const elRef = React.useRef();
React.useEffect(() => {
if (!elRef.current) return;
// If the early return was not present, this line would throw an error:
// "Cannot read property 'style' of undefined"
elRef.current.style.background = 'lightblue';
}, [elRef])
return (
<Container ref={elRef}/>
);
你会注意到, 的Container
div
样式没有背景lightblue
。这是因为elRef.current
从未设置为包含HTMLElement
引用。因此,对于简单的引用转发,你不能使用ref
属性名称。
如何让ref
属性名称按照预期与功能组件一起工作?
您可以使用ref
属性名称通过 API 转发 ref forwardRef
。定义函数式组件时,与其像其他方式一样简单地将其定义为箭头函数,不如将组件赋值给 ,forwardRef
并将箭头函数作为其第一个属性。这样,您就可以ref
从内部箭头函数的第二个属性进行访问。
const Container = React.forwardRef((props, ref) => {
return <div ref={ref}>{props.children}</div>
})
const App = () => {
const elRef = React.useRef();
React.useEffect(() => {
console.log(elRef);
elRef.current.style.background = 'lightblue';
}, [elRef])
return (
<Container ref={elRef}/>
);
现在我们正在使用forwardRef
,我们可以使用父组件上的属性名称再次ref
访问。elRef
类组件引用
虽然我提到我们将在本文的大部分内容中使用函数式组件和钩子,但我认为介绍类组件如何处理ref
属性也很重要。以以下类组件为例:
class Container extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
ref
如果我们尝试传递一个属性,您认为会发生什么?
const App = () => {
const compRef = React.useRef();
React.useEffect(() => {
console.log(compRef.current);
});
return (
<Container ref={container}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</Container>
);
}
如果您愿意,您也可以将其写
App
为类组件:class App extends React.Component { compRef = React.createRef(); componentDidMount() { console.log(this.compRef.current); } render() { return ( <Container ref={this.compRef}> <h1>Hello StackBlitz!</h1> <p>Start editing to see some magic happen :)</p> </Container> ); } }
如果你看一下该console.log
语句,你会注意到它打印了如下内容:
Container {props: {…}, context: {…}, refs: {…}, updater: {…}…}
context: Object
props: Object
refs: Object
state: null
updater: Object
_reactInternalInstance: Object
_reactInternals: FiberNode
__proto__: Container
你会注意到它打印出了一个实例的值Container
。实际上,如果我们运行以下代码,我们可以确认该ref.current
值是该类的一个实例Container
:
console.log(container.current instanceof Container); // true
但是,这个类是什么?那些 props 是从哪里来的?好吧,如果你熟悉类继承,你就会知道它就是被扩展的属性React.Component
。如果我们看一下这个React.Component
类的 TypeScript 定义,我们会发现它里面有一些非常熟悉的属性:
// This is an incomplete and inaccurate type definition shown for educational purposes - DO NOT USE IN PROD
class Component {
render(): ReactNode;
context: any;
readonly props: Object;
refs: any;
state: Readonly<any>;
}
不仅refs
、state
、props
和context
与我们在 中看到的一致console.log
,而且属于类的方法(如render
)也存在:
console.log(this.container.current.render);
ƒ render()
自定义属性和方法
你不仅可以从类引用访问 React 组件的内置函数(例如render
和props
),还可以访问附加到该类的数据。由于container.current
是类的实例Container
,因此当你添加自定义属性和方法时,它们也可以从引用中可见!
因此,如果你将类定义更改为如下所示:
class Container extends React.Component {
welcomeMsg = "Hello"
sayHello() {
console.log("I am saying: ", this.welcomeMsg)
}
render() {
return <div>{this.props.children}</div>;
}
}
然后您可以引用welcomeMsg
属性和sayHello
方法:
function App() {
const container = React.useRef();
React.useEffect(() => {
console.log(container.current.welcomeMsg); // Hello
container.current.sayHello(); // I am saying: Hello
});
return (
<Container ref={container}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</Container>
);
}
单向流
虽然“通用定向流”的概念比我最初想在这篇文章中讨论的范围要广,但我认为理解为什么不应该使用上面概述的模式很重要。ref 如此有用的原因之一,也是它作为一个概念如此危险的原因之一:它破坏了单向数据流。
通常,在 React 应用程序中,您希望数据一次只传输一个方向。
让我们看一个遵循这种单向性的代码示例:
import React from "react";
class SimpleForm extends React.Component {
render() {
return (
<div>
<label>
<div>Username</div>
<input
onChange={e => this.props.onChange(e.target.value)}
value={this.props.value}
/>
</label>
<button onClick={this.props.onDone}>Submit</button>
</div>
);
}
}
export default function App() {
const [inputTxt, setInputTxt] = React.useState("");
const [displayTxt, setDisplayTxt] = React.useState("");
const onDone = () => {
setDisplayTxt(inputTxt);
};
return (
<div>
<SimpleForm
onDone={onDone}
onChange={v => setInputTxt(v)}
value={inputTxt}
/>
<p>{displayTxt}</p>
</div>
);
}
在此示例中,由于onChange
属性和value
属性都被传递到SimpleForm
组件中,因此您可以将所有相关数据保存在一个位置。您会注意到,组件内部没有任何实际逻辑SimpleForm
。因此,该组件被称为“哑”组件。它用于样式和可组合性,但不用于逻辑本身。
一个合适的 React 组件应该是这样的。这种将状态从组件本身提升出来,并留下一个“哑”组件的模式源自 React 团队的指导。这种模式被称为“状态提升”。
现在我们对要遵循的模式有了更好的理解,让我们来看看错误的做事方式。
打破建议的模式
与“提升状态”相反,我们将状态降回到SimpleForm
组件中。然后,为了从 访问该数据App
,我们可以使用ref
属性从父级访问该数据。
import React from "react";
class SimpleForm extends React.Component {
// State is now a part of the SimpleForm component
state = {
input: ""
};
onChange(e) {
this.setState({
input: e.target.value
});
}
render() {
return (
<div>
<label>
<div>Username</div>
<input onChange={this.onChange.bind(this)} value={this.state.input} />
</label>
<button onClick={this.props.onDone}>Submit</button>
</div>
);
}
}
export default function App() {
const simpleRef = React.useRef();
const [displayTxt, setDisplayTxt] = React.useState("");
const onDone = () => {
// Reach into the Ref to access the state of the component instance
setDisplayTxt(simpleRef.current.state.input);
};
return (
<div>
<SimpleForm
onDone={onDone}
ref={simpleRef}
/>
<p>{displayTxt}</p>
</div>
);
}
然而,问题在于,当你开始扩展时,你会发现管理这种双状态行为更加困难。甚至遵循应用程序逻辑也更加困难。让我们先来看看这两个组件的生命周期是如何直观呈现的。
首先我们先来看一下组件simpleRef
,其中状态在SimpleForm
组件中被“降低”了:
在这个例子中,应用程序状态的流程如下:
App
(以及它的子项,SimpleForm
)渲染- 用户对存储在
SimpleForm
- 用户触发
onDone
操作,从而触发一个函数App
- 该
App
onDone
方法检查来自SimpleForm
- 一旦数据返回到,它就会更改自己的数据,从而触发和 的重新
App
渲染App
SimpleForm
从上图和数据流的概述可以看出,您将数据分散在两个不同的位置。因此,修改此代码的思维模型可能会变得混乱且脱节。当onDone
需要更改 中的状态时,此代码示例会变得更加复杂SimpleForm
。
现在,让我们将其与强制单向性所需的心理模型进行对比。
App
(以及它的子项,SimpleForm
)渲染- 用户在 中进行更改,状态通过回调
SimpleForm
提升至App
- 用户触发
onDone
操作,从而触发一个函数App
- 该
App
onDone
方法已经包含了它自己的组件中所需的所有数据,因此它只需重新渲染,App
而SimpleForm
无需任何额外的逻辑开销
如您所见,虽然这些方法之间的步骤数相似(并且在不太简单的示例中可能并非如此),但单向流程更加精简且更容易遵循。
这就是为什么 React 核心团队(以及整个社区)强烈建议您使用单向性,并且在不需要时正确地避免脱离该模式。
将数据添加到参考
如果你以前从未听说过useImperativeHandle
Hook,这就是原因所在。它允许你向转发/传递到组件的数据添加方法和属性ref
。这样,你就可以直接在父组件中访问子组件的数据,而不必强制提升状态,因为这可能会破坏单向性。
让我们看一下可以使用来扩展的组件useImperativeHandle
:
import React from "react";
import "./style.css";
const Container = React.forwardRef(({children}, ref) => {
return <div ref={ref} tabIndex="1">
{children}
</div>
})
export default function App() {
const elRef = React.useRef();
React.useEffect(() => {
elRef.current.focus();
}, [elRef])
return (
<Container ref={elRef}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</Container>
);
}
正如您在嵌入式演示中所见,它将重点关注Container
div
应用程序渲染时的 。此示例未使用钩子,而是依赖于的useImperativeHandle
时机来useEffect
定义。ref
current
假设我们想以Container
div
编程方式跟踪每次 获得焦点的时间。该怎么做呢?有很多方法可以实现该功能,但有一种不需要修改App
(或其他Container
消费者)的方法是使用useImperativeHandle
。
不仅useImperativeHandle
允许向 ref 添加属性,还可以通过返回同名函数来提供本机 API 的替代实现。
import React from "react";
import "./style.css";
const Container = React.forwardRef(({children}, ref) => {
const divRef = React.useRef();
React.useImperativeHandle(ref, () => ({
focus: () => {
divRef.current.focus();
console.log("I have now focused");
}
}))
return <div ref={divRef} tabIndex="1">
{children}
</div>
})
export default function App() {
const elRef = React.useRef();
React.useEffect(() => {
elRef.current.focus();
}, [elRef])
return (
<Container ref={elRef}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</Container>
);
}
如果您查看控制台,您会发现运行
console.log
时已经运行focus()
!
正如您可以的那样,useImperativeHandle
可以与之结合使用,forwardRef
以最大限度地提高组件 API 的自然外观。
但是,请注意,如果您希望使用自己的 API 来补充原生 API,则只有第二个参数返回的属性和方法才会被设置为 ref。这意味着如果您现在运行:
React.useEffect(() => {
elRef.current.style.background = 'lightblue';
}, [elRef])
在 中App
,您将遇到一个错误,因为style
不再定义elRef.current
。
话虽如此,你并不局限于原生 API 的名称。你认为这个代码示例在其他App
组件中可能会做什么?
React.useEffect(() => {
elRef.current.konami();
}, [elRef])
当你的焦点位于
Container
元素上时,尝试使用箭头键输入“Konami 代码”。完成后会有什么反应?
React Refs 中useEffect
我必须坦白:我一直在骗你。并非恶意,但我在之前的示例中反复使用了不该在生产环境中使用的代码。这是因为,如果不费吹灰之力,教授这些东西可能会很棘手。
有问题的代码是什么?
React.useEffect(() => {
elRef.current.anything.here.is.bad();
}, [elRef])
什么?
没错!你不应该把它放在elRef.current
任何东西里面useEffect
(除非你真的 真的 知道自己在做什么)。
为什么?
在我们全面回答这个问题之前,让我们先看看它是如何useEffect
工作的。
假设我们有一个如下所示的简单组件:
const App = () => {
const [num, setNum] = React.useState(0);
React.useEffect(() => {
console.log("Num has ran");
}, [num])
return (
// ...
)
}
你可能以为,当num
更新时,依赖项数组会“监听” 的变化num
,并且当数据更新时,它会触发副作用。这种想法实际上是“useEffect 会主动监听数据更新,并在数据更改时运行副作用”。这种思维模式是不准确的,并且在实际使用中可能会很危险ref
。甚至我直到开始写这篇文章时才意识到这是错误的!
在非 ref(useState
/props)依赖数组跟踪下,这种推理通常不会在代码库中引入错误,但是当ref
添加 s 时,由于误解,它会引发一系列麻烦。
useEffect
实际工作方式更加被动。在渲染期间,useEffect
会检查依赖项数组中的值。如果任何值的内存地址发生变化(这意味着对象突变会被忽略),它就会运行副作用。这可能看起来与之前概述的理解类似,但这是“推”与“拉”的区别。useEffect
它不会监听任何内容,也不会触发渲染,而是渲染触发useEffect
对值的监听和比较。这意味着如果没有渲染,useEffect
即使数组中的内存地址发生了变化,也无法运行副作用。
为什么在使用 s 时会出现这种情况ref
?嗯,有两件事需要记住:
- Refs 依赖于对象变异而不是重新分配
-
当 a
ref
发生变异时,它不会触发重新渲染 -
useEffect
仅在重新渲染时检查数组 -
Ref 的当前属性集不会触发重新渲染(记住它实际上
useRef
是如何实现的)
了解了这一点,让我们再看一次令人反感的例子:
export default function App() {
const elRef = React.useRef();
React.useEffect(() => {
elRef.current.style.background = "lightblue";
}, [elRef]);
return (
<div ref={elRef}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
}
这段代码的行为正如我们最初预期的那样,这并不是因为我们做对了,而是由于 ReactuseEffect
钩子的时间特性。
因为useEffect
发生在第一次渲染之后elRef
,所以在新值被赋值之前就已经赋值了elRef.current.style
。但是,如果我们以某种方式打破了这种时间预期,就会看到不同的行为。
如果在初始渲染之后div
进行渲染,您认为会发生什么?
export default function App() {
const elRef = React.useRef();
const [shouldRender, setRender] = React.useState(false);
React.useEffect(() => {
if (!elRef.current) return;
elRef.current.style.background = 'lightblue';
}, [elRef.current])
React.useEffect(() => {
setTimeout(() => {
setRender(true);
}, 100);
}, []);
return !shouldRender ? null : (
<div ref={elRef}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
}
糟糕!背景不再存在了'lightblue'
!因为我们延迟了 的渲染div
,所以初始渲染时不会赋值。之后,一旦elRef
渲染完成,它会修改.current
的属性elRef
来赋值 ref。由于修改不会触发重新渲染(并且useEffect
只会在渲染期间运行),所以useEffect
它没有机会“比较”值的差异,从而导致副作用。
困惑了吗?没关系!我一开始也是。我做了个游乐场,帮我们这些动觉学习者!
const [minus, setMinus] = React.useState(0);
const ref = React.useRef(0);
const addState = () => {
setMinus(minus + 1);
};
const addRef = () => {
ref.current = ref.current + 1;
};
React.useEffect(() => {
console.log(`ref.current:`, ref.current);
}, [ref.current]);
React.useEffect(() => {
console.log(`minus:`, minus);
}, [minus]);
打开控制台并记下
console.log
更改相应值时运行的内容!
如何使用这个例子?好问题!
首先,点击useState
标题下方的按钮。你会注意到,每次点击按钮都会立即触发重新渲染,UI 中显示的值也会立即更新。因此,它使得useEffect
(num
依赖依赖)能够将先前的值与当前值进行比较(如果它们不匹配),并运行console.log
副作用。
现在,一旦触发了useState
“添加”按钮,就对 按钮执行相同的操作useRef
。您可以根据需要多次点击它,但(单独)它永远不会触发重新渲染。由于useRef
突变不会重新渲染 DOM,因此两者都无法useEffect
比较值,因此都useEffect
不会运行。但是, 中的值.current
正在更新 - 它们只是没有显示在 UI 中(因为组件没有重新渲染)。一旦触发重新渲染(useState
再次按下“添加”按钮),它将更新 UI 以匹配 的内部内存值.current
。
简而言之- 尝试按useState
两次“加”键。屏幕上的值为 2。然后,尝试按useRef
三次“加”键。屏幕上的值为 0。useState
再次按下 按钮,瞧 - 两个值都变成了 3!
核心团队的评论
由于ref
在 a 中跟踪 a 会产生意想不到的影响useEffect
,因此核心团队明确建议避免这样做。
正如我之前提到的,如果你把 [ref.current] 放在依赖项中,你很可能会犯一个错误。refs 指的是那些其更改不需要触发重新渲染的值。
如果您想在 ref 改变时重新运行效果,您可能需要回调 ref。
当你尝试添加
ref.current
依赖项时,通常需要一个回调引用
我认为你需要回调 ref 来实现这一点。你无法让组件神奇地对 ref 的变化做出反应,因为 ref 可以深入底层,并且拥有与所属组件无关的生命周期。
这些都是很好的观点...但是 Dan 所说的“回调引用”是什么意思呢?
回调引用
在本文开头,我们提到了另一种分配 ref 的方法。而不是:
<div ref={elRef}>
这是有效的(并且稍微冗长一些):
<div ref={node => elRef.current = node}>
这是因为ref
可以接受回调函数。这些函数会使用元素节点本身进行调用。这意味着,如果你愿意,你可以内联.style
我们在本文中多次使用的赋值语句:
<div ref={node => node.style.background = "lightblue"}>
但是,你可能会想,如果它接受一个函数,我们可以传递一个之前在组件中声明的回调函数。没错!
const elRefCB = React.useCallback(node => {
if (node !== null) {
node.style.background = "lightblue";
}
}, []);
return !shouldRender ? null : (
<div ref={elRefCB}>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
但是嘿!等一下!即使
shouldRender
时间不匹配仍然存在,背景仍然会被应用!为什么useEffect
时间不匹配不会导致我们之前遇到的 bug?
嗯,那是因为我们在这个例子中完全消除了 的使用useEffect
!因为回调函数只运行一次ref
,所以我们可以确定.current
会存在,因此,我们可以在回调中分配属性值等等!
但我还需要把它传递
ref
给代码库的其他部分!我不能传递函数本身;那只是一个函数,而不是引用!
确实如此。不过,你可以将这两种行为结合起来,创建一个回调,并将其数据存储在 中useRef
(以便稍后使用该引用)。
const elRef = React.useRef();
console.log("I am rendering");
const elRefCB = React.useCallback(node => {
if (node !== null) {
node.style.background = "lightblue";
elRef.current = node;
}
}, []);
React.useEffect(() => {
console.log(elRef.current);
}, [elRef, shouldRender]);
useState
参考文献
有时,仅使用useRef
和回调引用是不够的。在极少数情况下,每当 中获取新值时,都需要重新渲染.current.
。问题在于 的固有特性会.current
阻止重新渲染。我们如何解决这个问题?.current
将 替换为 ,即可完全避免useRef
这种情况useState
。
您可以使用回调引用来分配给useState
钩子,从而相对简单地完成此操作。
const [elRef, setElRef] = React.useState();
console.log('I am rendering');
const elRefCB = React.useCallback(node => {
if (node !== null) {
setElRef(node);
}
}, []);
React.useEffect(() => {
console.log(elRef);
}, [elRef])
现在ref
更新会导致重新渲染,您现在可以安全地使用ref
inuseEffect
的依赖数组。
const [elNode, setElNode] = React.useState();
const elRefCB = React.useCallback(node => {
if (node !== null) {
setElNode(node);
}
}, []);
React.useEffect(() => {
if (!elNode) return;
elNode.style.background = 'lightblue';
}, [elNode])
然而,这会以性能为代价。由于触发了重新渲染,因此它本身会比不触发重新渲染时慢。不过,这样做也有其合理之处。你只需要注意你的决策以及代码对它们的使用。
结论
与大多数工程工作一样,了解 API 的局限性、优势和变通方案可以提高性能,减少生产环境中的错误,并使代码组织更加便捷。既然您已经了解了 ref 的全部内容,您将如何运用这些知识呢?我们期待您的反馈!请在下方留言,或加入我们的 Discord 社区!
文章来源:https://dev.to/this-is-learning/react-refs-the-complete-story-16km