1.Refs
本小结参考博文
官方介绍
在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了通过使用Refs来解决。
1.1 Refs创建
下面给出ref的三种创建形式:
- 字符串形式的ref(过时)
- 回调函数形式的ref
- 函数创建
下面对三种情况分别进行介绍:
1.1.1 字符串形式的ref(过时)
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
sayName=()=>{}
render() {
return <input />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.refs);
// 拿到子组件内定义的方法
// this.echoRef.focus();
}
render() {
return (
// 这里直接把回调传递下去了
<>
<Children ref='echoRef' />
<input ref='timeRef' />
</>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
1.1.2 回调函数形式的ref
class App extends Component {
state = { text: '' };
inputRef = null;
componentDidMount() {
console.log('componentDidMount:', this.inputRef);
}
componentDidUpdate() {
console.log('componentDidUpdate:', this.inputRef);
}
onFocus() {
this.inputRef.focus();
}
render() {
return (
<div className='app'>
<input
// 通过回调函数的形式传递 ref
ref={ e => {
this.inputRef = e;
alert(e);
} }
text={ this.state.text }
onChange={ e => {
this.setState({text: e.target.value});
}}
/>
<button onClick={() => this.onFocus()}>聚焦</button>
</div>
)
}
}
const rootElement = document.getElementById("app")
ReactDOM.render(<App/>, rootElement);
回调函数会以内联函数的形式
定义的,在更新的时候他会执行两次
,第一次传入参数null,第二次传入参数DOM元素。·下面
这种回调函数以class的绑定函数
的形式定义的,可以避免
上面那种两次执行情况
,但是在大多数情况下是无关紧要的,所以使用上面哪种方式都行
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.inputRef);
}
setRef = (e) => {
this.inputRef = e;
}
// 此方法暴露给父组件
inputFocus = () => {
this.inputRef.focus();
}
render() {
return <input ref={this.setRef} />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
// 拿到子组件内定义的方法
this.echoRef.inputFocus();
}
// 定义绑定ref函数
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 这里不再直接绑定ref,而是上述函数
<Children ref={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
1.1.3 createRef
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount(){
console.log(this.echoRef);
}
render() {
// 这里的ref就是必须这么写了,通过ref属性将this.echoRef与子组件关联
return <div ref={this.echoRef}>你好啊,echo。</div>
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。值得注意的是一个ref,用在了多个元素上它永远以最后关联的DOM为准
。
import React, { Component, Fragment } from 'react';
import ReactDOM from 'react-dom';
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
// 这里调用了孙子组件的方法
this.echoRef.current.timeStepRef.current.handleClick();
}
render() {
return (
<Children ref={this.echoRef} userName="echo" />
)
}
}
class Children extends Component {
constructor(props) {
super(props);
this.timeStepRef = React.createRef();
}
state = {
userName: '听风是风'
}
// 这个方法给父组件调用
handleClick = () => { console.log('我在调用子组件的方法。') }
render() {
return (
<Grandson ref={this.timeStepRef} />
)
}
}
class Grandson extends Component {
constructor(props) {
super(props)
}
// 这个方法给祖父组件使用
handleClick = () => { console.log('我是给上上层组件使用的方法') }
render() {
return (
<div>你好,我是孙子组件。</div>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。如上图的对象解构,我们在父级组件通过this.echoRef.current.timeStepRef.current.handleClick();调用了孙子组件的方法,所以不管组件嵌套多少层,只有有定义ref你就一定能向下访问到你想要的属性,当然,这种做法非常不好!你不能在函数组件上使用 createRef属性,因为他们没有实例
。比如下面这个例子:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
function Children () {
return <div>我是子组件</div>;
}
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
}
render() {
return (
//函数组件无法被直接使用
<Children ref={this.echoRef} userName="echo" />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
但是呢,我们是可以在函数组件内,为其它DOM或者组件绑定ref
,比如这个例子:
import React, { createRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props, ref) {
const inputRef =createRef();
const handleClick = ()=>{
inputRef.current.focus();
}
return (
//react渲染需要一个父组件,当没有父组件时,就可以使用<>这个空标签 也可以使用<Fragment>
<>
<input ref={inputRef} />
<button onClick={handleClick}>点我让输入框聚焦</button>
</>
);
}
ReactDOM.render(
<Children />,
document.getElementById('root')
);
1.2 Refs转发(forwardRef )
- 可以给函数组件绑定ref,
- ref转发
Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。如果要在函数组件中使用 ref,你可以使用 forwardRef(可与 useImperativeHandle 结合使用)实现。
import React, { Component, forwardRef, useRef, useImperativeHandle, createRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props, ref) {
// useRef是一个hook,你只用知道它可以创建ref
const inputRef = useRef();
// 你可以通过这种方式创建
// const inputRef = createRef()
const sayName = () => {
console.log(1);
}
useImperativeHandle(ref, () => ({
focus: () => {
// 这里操作的是input自带的focus方法
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
}
// 由于函数组件不能用ref,这里使用`forwardRef`包裹了一层
Children = forwardRef(Children);
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.log(this.echoRef);
this.echoRef.current.focus();
}
render() {
return (
<Children ref={this.echoRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
-
首先
forwardRef
接受一个函数,此函数支持2个参数props与ref
,props很好理解,就是上层传递下来的属性,而ref自然也是上层传递下来的ref。比较巧的是我们的Children本身就是个函数,因此我们直接使用forwardRef进行了包裹,可以理解为给组件升了个级。 -
而在Children内部const inputRef = useRef()这一句,也是使用hook提供的API创建一个ref,他与createRef的原理以及含义上有一定差异,不过这里我们就理解为创建了一个ref。
-
紧接着inputRef与函数组件内部的input相关联,也就是说现在函数组件内是可以直接使用input内置的属性方法。前面说了,函数组件自己内部还是可以使用ref的,只是我们不能直接用ref关联一个函数组件,但是前面我们通过forwardRef给函数组件升了级。
-
紧接着,我们通过useImperativeHandle将函数组件内部能访问到的input上的属性方法,再次暴露给了父组件,useImperativeHandle中的ref其实就是上层传递的,这里就是通过此方法,将上层ref与函数组件内部产生了关联。我们自己定义了一个focus方法,而这个方法内部执行的却是input的focus方法,组件内部可以通过inputRef.current.focus访问到input的方法。
-
于是我们在父组件中通过this.echoRef.current.focus()访问到了函数组件暴露给它的方法,而这个方法本质执行的其实是input自带的focus方法。
-
不知道你理解没有,这里我们通过forwardRef帮父组件做了一次转发,父组件其实想访问的就是input的方法,但是函数组件在中间隔了一层,父组件就没法直接拿到,而我们通过useImperativeHandle帮父组件代劳了一次,成功达到了目的。
其实forwardRef除了能让函数组件使用ref外,还有另一种强大的作用就是转发ref
。
- 比如A>B>C的组件结构,你在A中创建了一个ref,你希望将这个ref作为props传递给B,然后在B中接受这个ref再去去关联C,以达到在A中可以访问到C的属性。我们可以假想有一个hoc的场景,父组件希望访问B组件,但是B组件被hoc包裹了一层,也就是一个高阶组件。此时你的ref假设绑定在了hoc生成的B,那么ref将访问hoc组件而非B组件。那么怎么让父组件可以访问到这个B组件呢?我们可以借用forwardRef:
import React, { Component, forwardRef } from 'react';
import ReactDOM from 'react-dom';
function hocComponent(Component) {
// 单纯包装了传入的组件,生成了一个新的组件,只是在生成中我们还用了forwardRef在外面包了一层
return forwardRef((props, ref) => {
return <Component {...props} ref={ref} />
})
}
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.log(this.echoRef);
this.echoRef.current.handleClick();
}
render() {
// 传入Children给高阶组件,得到了一个新组件
const Child = hocComponent(Children);
return (
<Child ref={this.echoRef} />
)
}
}
class Children extends Component {
constructor(props) {
super(props);
}
handleClick = () => {
console.log('给父级调用的方法')
}
render() {
return <>我是子组件啊</>;
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
这个例子就解释了hoc的情况,这个例子相对上面参杂了hook的例子来说应该好理解一点,这里就不多解释。至少到这里,我们解释了forwardRef的两种作用,一是也可以给函数组件绑定ref,第二点就是ref转发,比如hoc包裹,我们绕过绕过高阶组件,拿到高阶组件内部真正的组件属性。
另外!!!A>B>C,假设B是函数组件,我们希望A的ref绑定C从而访问C,其实还有一种做法,就是不要直接ref绑定,而是把ref作为props传递下去后再绑定,这样不管B是不是函数组件,都能成功绑定到C,再来个例子:
import React, { Component, forwardRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props) {
return (
// 子组件接受了这个ref,然后再通过ref进行绑定
<input ref={props.inputRef} />
);
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
}
handleClick = () => {
// 成功访问了子组件下的子组件
this.echoRef.current.focus();
}
render() {
return (
<>
<Children
inputRef={this.echoRef}//我们希望把这个ref作为props传递下去
/>
<button onClick={this.handleClick}>点我聚焦</button>
</>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
因为给函数组件上添加ref会警告,那么我们就不用ref,而是把创建的ref作为属性传下去,在子组件中接受后,再绑定给你要访问的DOM或者组件,这样不仅解决了函数组件绑定ref的问题,还搞定了ref转发的问题。
1.3 注意事项
另外,我们在通过ref获取子组件属性时,比如获取一个函数,请注意函数的写法,比如这个例子中,我们能拿到sayName,但拿不到sayAge,这是因为后者本质上是绑定在原型上,无法通过这种方式直接访问,但是你可以通过原型找到它。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
}
sayName = () => { }
sayAge() { }
render() {
// 父级传递来的props,最后给了input用了
return <input />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
}
// 定义绑定ref函数
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 这里直接把回调传递下去了
<Children ref={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
2. Hooks
Hook 是 React 16.8 的新增特性。什么时候我会用 Hook? 如果你在编写函数组件
并意识到需要向其添加一些 state
,以前的做法是必须将其转化为 class。现在你可以在现有的函数组件中使用 Hook
。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
2.1 State Hook (useState)
调用 useState 方法的时候做了什么? 它定义一个 “state 变量”。我们的变量叫 count, 但是我们可以叫他任何名字,比如 banana。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。
useState 需要哪些参数? useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState() 两次即可。)
useState 方法的返回值是什么? 返回值为:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.count 和 this.setState 类似,唯一区别就是你需要成对的获取它们。如果你不熟悉我们使用的语法,我们会在本章节的底部介绍它。
import React, { useState } from 'react';
function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
- 读取 State
当我们想在 class 中显示当前的 count,我们读取 this.state.count:
<p>You clicked {this.state.count} times</p>
在函数中,我们可以直接用 count:
<p>You clicked {count} times</p>
- 更新 State
在 class 中,我们需要调用 this.setState() 来更新 count 值:
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
在函数中,我们已经有了 setCount 和 count 变量,所以我们不需要 this:
<button onClick={() => setCount(count + 1)}>
Click me
</button>
2.2 Effect Hook (useEffect)
你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
在 React 的 class 组件中,render 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。
这就是为什么在 React class 中,我们把副作用操作放到 componentDidMount 和 componentDidUpdate 函数中。回到示例中,这是一个 React 计数器的 class 组件。它在 React 对 DOM 进行操作之后,立即更新了 document 的 title 属性。
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。现在让我们来看看如何使用 useEffect 执行相同的操作。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect 做了什么?
通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用 useEffect?
将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect 会在每次渲染后都执行吗
是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要
同步地执行
。在个别情况下(例如测量布局),有单独的useLayoutEffect Hook
供你使用,其 API 与 useEffect 相同。
2.2.1 effect 清除
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!在 React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在执行清除操作时调用它:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。
2.2.2 effect 性能优化
在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
这是很常见的需求,所以它被内置到了 useEffect 的 Hook API 中。如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
上面这个示例中,我们传入 [props.friend.id]
作为第二个参数。这个参数是什么作用呢?如果 [props.friend.id]
的值是 5,而且我们的组件重渲染的时候 [props.friend.id]
还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。
当渲染时,如果 [props.friend.id]
的值更新成了 6,React 将会把前一次渲染时的数组 [5] 和这次渲染的数组 [6] 中的元素进行对比。这次因为 5 !== 6,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
2.3 Hook 规则
2.3.1 只在最顶层使用 Hook
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。
function Form() {
// 1. Use the name state variable
const [name, setName] = useState('Mary');
// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
// 3. Use the surname state variable
const [surname, setSurname] = useState('Poppins');
// 4. Use an effect for updating the title
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});
// ...
}
那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:
// ------------
// 首次渲染
// ------------
useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state
useEffect(persistForm) // 2. 添加 effect 以保存 form 操作
useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
useEffect(updateTitle) // 4. 添加 effect 以更新标题
// -------------
// 二次渲染
// -------------
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
useEffect(persistForm) // 2. 替换保存 form 的 effect
useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略)
useEffect(updateTitle) // 4. 替换更新标题的 effect
只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?
// 🔴 在条件语句中使用 Hook 违反第一条规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
在第一次渲染中 name !== ‘’ 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm) // 🔴 此 Hook 被忽略!
useState('Poppins') // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle) // 🔴 3 (之前为 4)。替换更新标题的 effect 失败
React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。
useEffect(function persistForm() {
// 👍 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});
2.3.2 只在 React 函数中调用 Hook
不要在普通的 JavaScript 函数中调用 Hook。你可以:
✅ 在 React 的函数组件中调用 Hook
✅ 在自定义 Hook 中调用其他 Hook
2.4 自定义 Hook
在我们学习使用 Effect Hook 时,我们已经见过这个聊天程序中的组件,该组件用于显示好友的在线状态:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
现在我们假设聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色。我们可以把上面类似的逻辑复制并粘贴到 FriendListItem 组件中来,但这并不是理想的解决方案:
import React, { useState, useEffect } from 'react';
function FriendListItem(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。 例如,下面的 useFriendStatus 是我们第一个自定义的 Hook:
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
我们一开始的目标是在 FriendStatus 和 FriendListItem 组件中去除重复的逻辑,即:这两个组件都想知道好友是否在线。现在我们已经把这个逻辑提取到 useFriendStatus 的自定义 Hook 中,然后就可以使用它了:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
===========================================================================================
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}