14【react-Hook (上)】
14【react-Hook (上)】
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
1.准备
1.1 什么是 Hook
Hooks 译为钩子,Hooks 就是在函数组件内,负责钩进外部功能的函数。
React 提供了一些常用钩子,React 也支持自定义钩子,这些钩子都是用于为函数引入外部功能。
当我们在组件中,需要引入外部功能时,就可以使用 React 提供的钩子,或者自定义钩子。
1.2 动机
在组件之间复用状态逻辑很难 React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。 你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。 复杂组件变得难以理解 我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。 Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
难以理解的 class 你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。 class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。 为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术。
1.3 Hook API
1.4 什么时候会用 Hook
如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class。现在你可以在现有的函数组件中使用 Hook。
注意:
在组件中有些特殊的规则,规定什么地方能使用 Hook,什么地方不能使用。我们将在 Hook 规则中学习它们。
2.使用 State Hook
2.1 声明 State 变量
首先我们需要明确一点,函数式组件没有自己的 this
在 class 中,我们通过在构造函数中设置 this.state
为 { count: 0 }
来初始化 count
state 为 0
:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
在函数组件中,我们没有 this
,所以我们不能分配或读取 this.state
。我们直接在组件中调用 useState
Hook:
import React, { useState } from 'react';
function Example() {
// 声明一个叫 “count” 的 state 变量
const [count, setCount] = useState(0);
console.log(count, setCount)
}
调用 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
类似,唯一区别就是你需要成对的获取它们。如果你不熟悉我们使用的语法,我们会在本章节的底部介绍它。
简单说
它让函数式组件能够维护自己的
state
,它接收一个参数,作为初始化state
的值,赋值给count
,因此useState
的初始值只有第一次有效,它所映射出的两个变量count
和setCount
我们可以理解为setState
来使用useState 能够返回一个数组,第一个元素是 state ,第二个是更新 state 的函数
既然我们知道了 useState
的作用,我们的示例应该更容易理解了:
我们声明了一个叫 count
的 state 变量,然后把它设为 0
。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount
来更新当前的 count
。
注意
你可能想知道:为什么叫
useState
而不叫createState
?“Create” 可能不是很准确,因为 state 只在组件首次渲染的时候被创建。在下一次重新渲染时,
useState
返回给我们当前的 state。否则它就不是 “state”了!这也是 Hook 的名字总是以use
开头的一个原因。我们将在后面的 Hook 规则中了解原因。
2.2 读取 State
当我们想在 class 中显示当前的 count,我们读取 this.state.count
:
<p>You clicked {this.state.count} times</p>
在函数中,我们可以直接用 count
:
<p>You clicked {count} times</p>
2.3 更新 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.4 使用多个 state 变量
将 state 变量声明为一对 [something, setSomething]
也很方便,因为如果我们想使用多个 state 变量,它允许我们给不同的 state 变量取不同的名称:
function ExampleWithManyStates() {
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
在以上组件中,我们有局部变量 age
,fruit
和 todos
,并且我们可以单独更新它们:
function handleOrangeClick() {
// 和 this.setState({ fruit: 'orange' }) 类似
setFruit('orange');
}
你不必使用多个 state 变量。State 变量可以很好地存储对象和数组,因此,你仍然可以将相关数据分为一组。然而,不像 class 中的 this.setState
,更新 state 变量总是替换它而不是合并它。
2.5 总结
现在让我们来仔细回顾一下学到的知识,看下我们是否真正理解了。
1: import React, { useState } from 'react';
2:
3: function Example() {
4: const [count, setCount] = useState(0);
5:
6: return (
7: <div>
8: <p>You clicked {count} times</p>
9: <button onClick={() => setCount(count + 1)}>
10: Click me
11: </button>
12: </div>
13: );
14: }
- 第一行: 引入 React 中的
useState
Hook。它让我们在函数组件中存储内部 state。 - 第四行: 在
Example
组件内部,我们通过调用useState
Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。我们把变量命名为count
,因为它存储的是点击次数。我们通过传0
作为useState
唯一的参数来将其初始化为0
。第二个返回的值本身就是一个函数。它让我们可以更新count
的值,所以我们叫它setCount
。 - 第九行: 当用户点击按钮后,我们传递一个新的值给
setCount
。React 会重新渲染Example
组件,并把最新的count
传给它。
乍一看这似乎有点太多了。不要急于求成!如果你有不理解的地方,请再次查看以上代码并从头到尾阅读。我们保证一旦你试着”忘记” class 里面 state 是如何工作的,并用新的眼光看这段代码,就容易理解了。
3.使用 Effect Hook
3.1 副作用
React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。
例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。
3.2 React.StrictMode
编写React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了React.StrictMode
标签,那么React会非常“智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的。
React并不能自动替你发现副作用,但是它会想办法让它显现出来,从而让你发现它。那么它是怎么让你发现副作用的呢?React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次:
类组件的的 constructor
, render
, 和 shouldComponentUpdate
方法 类组件的静态方法 getDerivedStateFromProps
函数组件的函数体 参数为函数的setState
参数为函数的useState
, useMemo
, or useReducer
重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log
你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。
如果你无法通过浏览器正常安装React Developer Tools可以通过点击这里下载。
3.3 Effect 基本使用
在类式组件中,提供了一些声明周期钩子给我们使用,我们可以在组件的特殊时期执行特定的事情,例如 componentDidMount
,能够在组件挂载完成后执行一些东西
在函数式组件中也可以实现,它采用的是 Effect Hook
,它的语法更加的简单,同时融合了 componentDidUpdata
生命周期,极大的方便了我们的开发
Effect Hook
可以让你在函数组件中执行副作用操作,专门用来处理那些不能直接写在组件内部的代码。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// 类似于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用这个bom api更新网页标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
我们为计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。
useEffect()
中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()
中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染。
通过使用这个Hook,我设置了React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。
提示
如果你熟悉 React class 的生命周期函数,你可以把
useEffect
Hook 看做componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个函数的组合。
在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的。我们来更仔细地看一下他们之间的区别。
3.2 无需清除的 effect
有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码。**比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。
3.2.1 使用 class 的示例
在 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
执行相同的操作。
3.2.2 使用 Hook 的示例
我们在本章节开始时已经看到了这个示例,但让我们再仔细观察它:
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 都已经更新完毕。
3.2.3 详细说明
现在我们已经对 effect 有了大致了解,下面这些代码应该不难看懂了:
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
}
我们声明了 count
state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect
Hook。此函数就是我们的 effect。然后使用 document.title
浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count
值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。
经验丰富的 JavaScript 开发人员可能会注意到,传递给 useEffect
的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count
的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。我们将在本章节后续部分更清楚地了解这样做的意义。
提示
与
componentDidMount
或componentDidUpdate
不同,使用useEffect
调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffect
Hook 供你使用,其 API 与useEffect
相同。
3.3 需要清除的 effect
之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。
3.3.1 使用 Class 的示例
在 React class 中,你通常会在 componentDidMount
中设置订阅,并在 componentWillUnmount
中清除它。例如,假设我们有一个 ChatAPI
模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
}
componentDidMount() {
ChatAPI.subscribe(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribe(
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';
}
}
你会注意到 componentDidMount
和 componentWillUnmount
之间相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。
注意
眼尖的读者可能已经注意到了,这个示例还需要编写
componentDidUpdate
方法才能保证完全正确。我们先暂时忽略这一点,本章节中后续部分会介绍它。
3.3.2 使用 Hook 的示例
如何使用 Hook 编写这个组件。
你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect
的设计是在同一个地方执行。如果你的 effect 返回一个函数,React 将会在下一次effect执行前调用它,我们可以在这个函数中清除掉前一次effect执行所带来的影响。
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribe(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribe(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
React 何时清除 effect? React 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。
注意
并不是必须为 effect 中返回的函数命名。这里我们将其命名为
cleanup
是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。
3.4 通过跳过 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(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
上面这个示例中,我们传入 [count]
作为第二个参数。这个参数是什么作用呢?如果 count
的值是 5
,而且我们的组件重渲染的时候 count
还是等于 5
,React 将对前一次渲染的 [5]
和后一次渲染的 [5]
进行比较。因为数组中的所有元素都是相等的(5 === 5
),React 会跳过这个 effect,这就实现了性能的优化。
当渲染时,如果 count
的值更新成了 6
,React 将会把前一次渲染时的数组 [5]
和这次渲染的数组 [6]
中的元素进行对比。这次因为 5 !== 6
,React 就会再次调用 effect。如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect。
对于有清除操作的 effect 同样适用:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribe(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribe(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
未来版本,可能会在构建时自动添加第二个参数。
注意:
如果你要使用此优化方式,请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。参阅文档,了解更多关于如何处理函数以及数组频繁变化时的措施内容。
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(
[]
)作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。如果你传入了一个空数组(
[]
),effect 内部的 props 和 state 就会一直拥有其初始值。尽管传入[]
作为第二个参数更接近大家更熟悉的componentDidMount
和componentWillUnmount
思维模式,但我们有更好的方式来避免过于频繁的重复调用 effect。除此之外,请记得 React 会等待浏览器完成画面渲染之后才会延迟调用useEffect
,因此会使得额外操作很方便。我们推荐启用
eslint-plugin-react-hooks
中的exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
4.useRef
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内持续存在。
一个常见的用例便是命令式地访问子组件:
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>
</>
);
}
我们要获取元素的真实DOM对象,首先我们需要使用useRef()这个钩子函数获取一个对象,这个对象就是一个容器,React会自动将DOM对象传递到容器中。代码const divRef = useRef()
就是通过钩子函数在创建这个对象,并将其存储到变量中。
创建对象后,还需要在被获取引用的元素上添加一个ref属性,该属性的值就是刚刚我们所声明的变量,像是这样ref={divRef}
这句话的意思就是将对象的引用赋值给变量divRef。这两个步骤缺一不可,都处理完了,就可以通过divRef来访问原生DOM对象了。
useRef()返回的是一个普通的JS对象,JS对象中有一个current属性,它指向的便是原生的DOM对象。上例中,如果想访问div的原生DOM对象,只需通过divRef.current
即可访问,它可以调用DOM对象的各种方法和属性,但还是要再次强调:慎用!
尽量减少在React中操作原生的DOM对象,如果实在非得操作也尽量是那些不会对数据产生影响的操作,像是设置焦点、读取信息等。
useRef()
所返回的对象就是一个普通的JS对象,所以上例中即使我们不使用钩子函数,仅仅创建一个形如{current:null}
的对象也是可以的。只是我们自己创建的对象组件每次渲染时都会重新创建一个新的对象,而通过useRef()
创建的对象可以确保组件每次的重渲染获取到的都是相同的对象。
5.useReducer
5.1 基本使用
为了解决复杂State
带来的不便,React
为我们提供了一个新的使用State
的方式。Reducer
横空出世,reduce单词中文意味减少,而reducer我觉得可以翻译为“当你的state的过于复杂时,你就可以使用的可以对state进行整合的工具”。当然这是个玩笑话,个人认为Reducer
可以翻译为“整合器”,它的作用就是将那些和同一个state
相关的所有函数都整合到一起,方便在组件中进行调用。
当然工具都有其使用场景,Reducer
也不例外,它只适用于那些比较复杂的state
,对于简单的state
使用Reducer
只能是徒增烦恼。
const [state, dispatch] = useReducer(reducer, initialArg, init);
它的返回值和useState()
类似,第一个参数是state
用来读取state
的值,第二个参数同样是一个函数,不同于setState()
这个函数我们可以称它是一个“派发器”,通过它可以向reducer()
发送不同的指令,控制reducer()
做不同的操作。
它的参数有三个,第三个我们暂且忽略,只看前两个。reducer()
是一个函数,也是我们所谓的“整合器”。它的返回值会成为新的state
值。当我们调用dispatch()
时,dispatch()
会将消息发送给reducer()
,reducer()
可以根据不同的消息对state
进行不同的处理。initialArg
就是state
的初始值,和useState()
参数一样。
以下是用 reducer
写的的计数器示例:
/*
* 参数:
* reducer : 整合函数
* 对于我们当前state的所有操作都应该在该函数中定义
* 该函数的返回值,会成为state的新值
* reducer在执行时,会收到两个参数:
* state 当前最新的state
* action 它需要一个对象
* 在对象中会存储dispatch所发送的指令
* initialArg : state的初始值,作用和useState()中的值是一样
* 返回值:
* 数组:
* 第一个参数,state 用来获取state的值
* 第二个参数,state 修改的派发器
* 通过派发器可以发送操作state的命令
* 具体的修改行为将会由另外一个函数(reducer)执行
* */
// 为了避免reducer会重复创建,通常reducer会定义到组件的外部
function countReducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [count, countDispatch] = useReducer(countReducer, {count: 0});
// 这里本来初始值是直接给0的,但是为了countReducer函数中的state写成对象形式
return (
<>
Count: {count.count}
<button onClick={() => countDispatch({type: 'decrement'})}>-</button>
<button onClick={() => countDispatch({type: 'increment'})}>+</button>
</>
);
}
5.2 state初始化的两种方式
指定初始 state
有两种不同初始化 useReducer
state 的方式,你可以根据使用场景选择其中的一种。将初始 state 作为第二个参数传入 useReducer
是最简单的方法:
const [state, dispatch] = useReducer(
reducer,
{count: 0} );
惰性初始化
你可以选择惰性地创建初始 state。为此,需要将 init
函数作为 useReducer
的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
。
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:
export default function App() {
return (
<div>
<Counter initialCount={0} />
</div>
)
}
function countInit(initialCount) {
return {count: initialCount};
}
function countReducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return countInit(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [count, countDispatch] = useReducer(countReducer, initialCount, countInit);
return (
<>
Count: {count.count}
<button
onClick={() => countDispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => countDispatch({type: 'decrement'})}>-</button>
<button onClick={() => countDispatch({type: 'increment'})}>+</button>
</>
);
}
5.3 跳过 dispatch
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is
比较算法 来比较 state。)
这里的state如果是个对象,还是会渲染子组件,因为我们返回的是一个新对象,我想应该比较的是地址,如果直接将state返回,子组件是不会重新渲染的
需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo
来进行优化。