ReactHook学习(第二篇-N)

文章目录


上篇地址

ref的研究(useRef)

使用 ref 引用值

当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref

给你的组件添加 ref

你可以通过从 React 导入 useRef Hook 来为你的组件添加一个 ref:

import { useRef } from 'react';

在你的组件内,调用 useRef Hook 并传入你想要引用的初始值作为唯一参数。例如,这里的 ref 引用的值是“0”:

const ref = useRef(0);

useRef 返回一个这样的对象:

{ 
  current: 0 // 你向 useRef 传入的值
}

你可以用 ref.current 属性访问该 ref 的当前值。这个值是有意被设置为可变的,意味着你既可以读取它也可以写入它。就像一个 React 追踪不到的、用来存储组件信息的秘密“口袋”。(这就是让它成为 React 单向数据流的“应急方案”的原因 —— 详见下文!)

这里,每次点击按钮时会使 ref.current 递增:

import { useRef } from 'react';

export default function Counter() {
	const ref = useRef(0);

	function handleClick() {
		ref.current = ref.current + 1;
		console.log('你点击了 ' + ref.current + ' 次!');
	}

	return <button onClick={handleClick}>点击我!</button>;
}

这里的 ref 指向一个数字,但是,像 state 一样,你可以让它指向任何东西:字符串、对象,甚至是函数。与 state 不同的是,ref 是一个普通的 JavaScript 对象,具有可以被读取和修改的 current 属性。

请注意,组件不会在每次递增时重新渲染。 与 state 一样,React 会在每次重新渲染之间保留 ref。但是,设置 state 会重新渲染组件,更改 ref 不会!

示例:制作秒表

你可以在单个组件中把 ref 和 state 结合起来使用。例如,让我们制作一个秒表,用户可以通过按按钮来使其启动或停止。为了显示从用户按下“开始”以来经过的时间长度,你需要追踪按下“开始”按钮的时间和当前时间。此信息用于渲染,所以你会把它保存在 state 中:

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

当用户按下“开始”时,你将用 setInterval 每 10 毫秒更新一次时间:

import { useState } from 'react';

export default function Stopwatch() {
	const [startTime, setStartTime] = useState(null);
	const [now, setNow] = useState(null);

	function handleStart() {
		// 开始计时。
		setStartTime(Date.now());
		setNow(Date.now());

		setInterval(() => {
			// 每 10ms 更新一次当前时间。
			setNow(Date.now());
		}, 10);
	}

	let secondsPassed = 0;
	if (startTime != null && now != null) {
		secondsPassed = (now - startTime) / 1000;
	}

	return (
		<>
			<h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
			<button onClick={handleStart}>开始</button>
		</>
	);
}

当按下“停止”按钮时,你需要取消现有的 interval,以便让它停止更新 now state 变量。你可以通过调用 clearInterval 来完成此操作。但你需要为其提供 interval ID,此 ID 是之前用户按下 Start、调用 setInterval 时返回的。你需要将 interval ID 保留在某处。 由于 interval ID 不用于渲染,你可以将其保存在 ref 中:

import { useRef, useState } from 'react';

export default function Stopwatch() {
	const [startTime, setStartTime] = useState(null);
	const [now, setNow] = useState(null);
	const intervalRef = useRef(null);

	function handleStart() {
		setStartTime(Date.now());
		setNow(Date.now());

		clearInterval(intervalRef.current);
		intervalRef.current = setInterval(() => {
			setNow(Date.now());
		}, 10);
	}

	function handleStop() {
		clearInterval(intervalRef.current);
	}

	let secondsPassed = 0;
	if (startTime != null && now != null) {
		secondsPassed = (now - startTime) / 1000;
	}

	return (
		<>
			<h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
			<button onClick={handleStart}>开始</button>
			<button onClick={handleStop}>停止</button>
		</>
	);
}

当一条信息用于渲染时,将它保存在 state 中。当一条信息仅被事件处理器需要,并且更改它不需要重新渲染时,使用 ref 可能会更高效。

ref 和 state 的不同之处

也许你觉得 ref 似乎没有 state 那样“严格” —— 例如,你可以改变它们而非总是必须使用 state 设置函数。但在大多数情况下,我们建议你使用 state。ref 是一个“应急方案”,你并不会经常用到它。 以下是 state 和 ref 的对比:

refstate
useRef(initialValue)返回 { current: initialValue }useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue])
更改时不会触发重新渲染更改时触发重新渲染。
可变 —— 你可以在渲染过程之外修改和更新 current 的值。“不可变” —— 你必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。
你不应在渲染期间读取(或写入) current 值。你可以随时读取 state。但是,每次渲染都有自己不变的 state 快照

这是一个使用 state 实现的计数器按钮:

import { useState } from 'react';

export default function Counter() {
	const [count, setCount] = useState(0);

	function handleClick() {
		setCount(count + 1);
	}

	return <button onClick={handleClick}>你点击了 {count} 次</button>;
}

因为 count 的值将会被显示,所以为其使用 state 是合理的。当使用 setCount() 设置计数器的值时,React 会重新渲染组件,并且屏幕会更新以展示新的计数。

如果你试图用 ref 来实现它,React 永远不会重新渲染组件,所以你永远不会看到计数变化!看看点击这个按钮如何 不更新它的文本

import { useRef } from 'react';

export default function Counter() {
	const countRef = useRef(0);

	function handleClick() {
		// 这样并未重新渲染组件!
		countRef.current = countRef.current + 1;
		console.log('countRef.current: ', countRef.current);
	}

	return <button onClick={handleClick}>你点击了 {countRef.current} 次</button>;
}

这就是为什么在渲染期间读取 ref.current 会导致代码不可靠的原因。如果需要,请改用 state。

何时使用 ref

通常,当你的组件需要“跳出” React 并与外部 API 通信时,你会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:

  • 存储计时器ID timeout ID
  • 存储和操作 DOM 元素
  • 存储不需要被用来计算 JSX 的其他对象。

如果你的组件需要存储一些值,但不影响渲染逻辑,请选择 ref。

ref 的最佳实践

遵循这些原则将使你的组件更具可预测性:

  • 将 ref 视为应急方案。 当你使用外部系统或浏览器 API 时,ref 很有用。如果你很大一部分应用程序逻辑和数据流都依赖于 ref,你可能需要重新考虑你的方法。
  • 不要在渲染过程中读取或写入 ref.current 如果渲染过程中需要某些信息,请使用 state 代替。由于 React 不知道 ref.current 何时发生变化,即使在渲染时读取它也会使组件的行为难以预测。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在第一次渲染期间设置一次 ref。)

React state 的限制不适用于 ref。例如,state 就像 每次渲染的快照,并且 不会同步更新。但是当你改变 ref 的 current 值时,它会立即改变:

ref.current = 5;
console.log(ref.current); // 5

这是因为 ref 本身是一个普通的 JavaScript 对象, 所以它的行为就像对象那样。

当你使用 ref 时,也无需担心 避免变更。只要你改变的对象不用于渲染,React 就不会关心你对 ref 或其内容做了什么。

ref 和 DOM

你可以将 ref 指向任何值。但是,ref 最常见的用法是访问 DOM 元素。例如,如果你想以编程方式聚焦一个输入框,这种用法就会派上用场。当你将 ref 传递给 JSX 中的 ref 属性时,比如 <div ref={myRef}>,React 会将相应的 DOM 元素放入 myRef.current 中。你可以在 使用 ref 操作 DOM 中阅读更多相关信息。

使用 ref 操作 DOM

由于 React 会自动处理更新 DOM 以匹配你的渲染输出,因此你在组件中通常不需要操作 DOM。但是,有时你可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个节点获得焦点、滚动到它或测量它的尺寸和位置。在 React 中没有内置的方法来做这些事情,所以你需要一个指向 DOM 节点的 ref 来实现。

获取指向节点的 ref

要访问由 React 管理的 DOM 节点,首先,引入 useRef Hook:

import { useRef } from 'react';

然后,在你的组件中使用它声明一个 ref:

const myRef = useRef(null);

最后,将其作为 ref 属性传给 DOM 节点:

<div ref={myRef}>

useRef Hook 返回一个对象,该对象有一个名为 current 的属性。最初,myRef.currentnull。当 React 为这个 <div> 创建一个 DOM 节点时,React 会把对该节点的引用放入 myRef.current。然后,你可以从 事件处理器 访问此 DOM 节点,并使用在其上定义的内置浏览器 API

// 你可以使用任意浏览器 API,例如:
myRef.current.scrollIntoView();
示例: 使文本输入框获得焦点

在本例中,单击按钮将使输入框获得焦点:

import { useRef } from 'react';

export default function Form() {
	const inputRef = useRef(null);
  
	function handleClick() {
    console.log('inputRef: ', inputRef);
		inputRef.current.focus();
	}

	return (
		<>
			<input ref={inputRef} />
			<button onClick={handleClick}>聚焦输入框</button>
		</>
	);
}

要实现这一点:

  1. 使用 useRef Hook 声明 inputRef
  2. <input ref={inputRef}> 这样传递它。这告诉 React 将这个 <input> 的 DOM 节点放入 inputRef.current
  3. handleClick 函数中,从 inputRef.current 读取 input DOM 节点并使用 inputRef.current.focus() 调用它的 focus()
  4. onClickhandleClick 事件处理器传递给 <button>

虽然 DOM 操作是 ref 最常见的用例,但 useRef Hook 可用于存储 React 之外的其他内容,例如计时器 ID 。与 state 类似,ref 能在渲染之间保留。你甚至可以将 ref 视为设置它们时不会触发重新渲染的 state 变量!你可以在使用 Ref 引用值中了解有关 ref 的更多信息。

示例: 滚动至一个元素

一个组件中可以有多个 ref。在这个例子中,有一个由三张图片和三个按钮组成的轮播,点击按钮会调用浏览器的 scrollIntoView() 方法,在相应的 DOM 节点上将它们居中显示在视口中:

import { useRef } from 'react';

export default function CatFriends() {
	const firstCatRef = useRef(null);
	const secondCatRef = useRef(null);
	const thirdCatRef = useRef(null);

	function handleScrollToFirstCat() {
		firstCatRef.current.scrollIntoView({
			behavior: 'smooth',
			block: 'nearest',
			inline: 'center',
		});
	}

	function handleScrollToSecondCat() {
		secondCatRef.current.scrollIntoView({
			behavior: 'smooth',
			block: 'nearest',
			inline: 'center',
		});
	}

	function handleScrollToThirdCat() {
		thirdCatRef.current.scrollIntoView({
			behavior: 'smooth',
			block: 'nearest',
			inline: 'center',
		});
	}

	return (
		<>
			<nav>
				<button onClick={handleScrollToFirstCat}>Tom</button>
				<button onClick={handleScrollToSecondCat}>Maru</button>
				<button onClick={handleScrollToThirdCat}>Jellylorum</button>
			</nav>
			<div>
				<ul>
					<li>
						<img src='https://placekitten.com/g/200/200' alt='Tom' ref={firstCatRef} />
					</li>
					<li>
						<img src='https://placekitten.com/g/300/200' alt='Maru' ref={secondCatRef} />
					</li>
					<li>
						<img src='https://placekitten.com/g/250/200' alt='Jellylorum' ref={thirdCatRef} />
					</li>
				</ul>
			</div>
		</>
	);
}

访问另一个组件的 DOM 节点

当你将 ref 放在像 <input /> 这样输出浏览器元素的内置组件上时,React 会将该 ref 的 current 属性设置为相应的 DOM 节点(例如浏览器中实际的 <input /> )。

但是,如果你尝试将 ref 放在 你自己的 组件上,例如 <MyInput />,默认情况下你会得到 null。这个示例演示了这种情况。请注意单击按钮 并不会 聚焦输入框:

import { useRef } from 'react';

function MyInput(props) {
	return <input {...props} />;
}

export default function MyForm() {
	const inputRef = useRef(null);

	function handleClick() {
		inputRef.current.focus();
	}

	return (
		<>
			<MyInput ref={inputRef} />
			<button onClick={handleClick}>聚焦输入框</button>
		</>
	);
}

为了帮助您注意到这个问题,React 还会向控制台打印一条错误消息:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

发生这种情况是因为默认情况下,React 不允许组件访问其他组件的 DOM 节点。甚至自己的子组件也不行!这是故意的。Refs 是一个应急方案,应该谨慎使用。手动操作 另一个 组件的 DOM 节点会使你的代码更加脆弱。

相反,想要 暴露其 DOM 节点的组件必须选择该行为。一个组件可以指定将它的 ref “转发”给一个子组件。下面是 MyInput 如何使用 forwardRef API:

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

它是这样工作的:

  1. <MyInput ref={inputRef} /> 告诉 React 将对应的 DOM 节点放入 inputRef.current 中。但是,这取决于 MyInput 组件是否允许这种行为, 默认情况下是不允许的。
  2. MyInput 组件是使用 forwardRef 声明的。 这让从上面接收的 inputRef 作为第二个参数 ref 传入组件,第一个参数是 props
  3. MyInput 组件将自己接收到的 ref 传递给它内部的 <input>

现在,单击按钮聚焦输入框起作用了:

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef(function (props, ref) {
	return <input {...props} ref={ref} />;
});

export default function Form() {
	const inputRef = useRef(null);
  
	function handleClick() {
    console.log('inputRef: ', inputRef);
		inputRef.current.focus();
	}

	return (
		<>
			<MyInput ref={inputRef} />
			<button onClick={handleClick}>聚焦输入框</button>
		</>
	);
}

在设计系统中,将低级组件(如按钮、输入框等)的 ref 转发到它们的 DOM 节点是一种常见模式。另一方面,像表单、列表或页面段落这样的高级组件通常不会暴露它们的 DOM 节点,以避免对 DOM 结构的意外依赖。

React 何时添加 refs

在 React 中,每次更新都分为 两个阶段

  • 渲染 阶段, React 调用你的组件来确定屏幕上应该显示什么。
  • 提交 阶段, React 把变更应用于 DOM。

通常,你 不希望 在渲染期间访问 refs。这也适用于保存 DOM 节点的 refs。在第一次渲染期间,DOM 节点尚未创建,因此 ref.current 将为 null。在渲染更新的过程中,DOM 节点还没有更新。所以读取它们还为时过早。

React 在提交阶段设置 ref.current。在更新 DOM 之前,React 将受影响的 ref.current 值设置为 null。更新 DOM 后,React 立即将它们设置到相应的 DOM 节点。

通常,你将从事件处理器访问 refs。 如果你想使用 ref 执行某些操作,但没有特定的事件可以执行此操作,你可能需要一个 effect。我们将在下一页讨论 effect。

使用 refs 操作 DOM 的最佳实践

Refs 是一个应急方案。你应该只在你必须“跳出 React”时使用它们。这方面的常见示例包括管理焦点、滚动位置或调用 React 未暴露的浏览器 API。

如果你坚持聚焦和滚动等非破坏性操作,应该不会遇到任何问题。但是,如果你尝试手动修改 DOM,则可能会与 React 所做的更改发生冲突。

为了说明这个问题,这个例子包括一条欢迎消息和两个按钮。第一个按钮使用 条件渲染state 切换它的显示和隐藏,就像你通常在 React 中所做的那样。第二个按钮使用 remove() DOM API 将其从 React 控制之外的 DOM 中强行移除.

尝试按几次“通过 setState 切换”。该消息会消失并再次出现。然后按 “从 DOM 中删除”。这将强行删除它。最后,按 “通过 setState 切换”:

import { useRef, useState } from 'react';

export default function Counter() {
	const [show, setShow] = useState(true);
	const ref = useRef(null);

	return (
		<div>
			<button onClick={() => setShow(!show)}>通过 setState 切换</button>
			<button onClick={() => ref.current.remove()}>从 DOM 中删除</button>
			{show && <p ref={ref}>Hello world</p>}
		</div>
	);
}

在你手动删除 DOM 元素后,尝试使用 setState 再次显示它会导致崩溃。这是因为你更改了 DOM,而 React 不知道如何继续正确管理它。

避免更改由 React 管理的 DOM 节点。 对 React 管理的元素进行修改、添加子元素、从中删除子元素会导致不一致的视觉结果,或与上述类似的崩溃。

但是,这并不意味着你完全不能这样做。它需要谨慎。 你可以安全地修改 React *没有理由* 更新的部分 DOM。 例如,如果某些 <div> 在 JSX 中始终为空,React 将没有理由去变动其子列表。 因此,在那里手动增删元素是安全的。

Effect的研究(useEffect)

与Effect同步

某些组件需要与外部系统同步。例如,您可能希望根据 React 状态控制非 React 组件,设置服务器连接,或者在组件出现在屏幕上时发送分析日志。effect允许你在渲染后运行一些代码,以便你可以将你的组件与 React 之外的一些系统同步。

什么是Effect,它们与事件有何不同?

在进入Effects之前,你需要熟悉 React 组件中的两种逻辑:

  • 呈现代码在描述 UI 中介绍)位于组件的顶层。这是你获取道具和状态的地方,转换它们,然后返回你想在屏幕上看到的JSX。呈现代码必须是纯的。就像数学公式一样,它应该只计算结果,而不做任何其他事情。
  • 事件处理程序在添加交互性中介绍)是组件中的嵌套函数,用于执行操作,而不仅仅是计算它们。事件处理程序可能会更新输入字段、提交 HTTP POST 请求以购买产品或将用户导航到另一个屏幕。事件处理程序包含由特定用户操作(例如,按钮单击或键入)引起的“副作用”(它们更改程序的状态)。

有时这还不够。考虑一个组件ChatRoom,只要它在屏幕上可见,就必须连接到聊天服务器。连接到服务器不是纯粹的计算(这是一种副作用),因此在渲染期间不会发生。但是,没有像单击这样的单个特定事件会导致ChatRoom显示。

**Effect 允许您指定由呈现本身而不是特定事件引起的副作用。**在聊天中发送消息是一个事件,因为它是由用户单击特定按钮直接引起的。但是,设置服务器连接是一种 Effect ,因为无论哪种交互导致组件出现,它都应该发生。效果在屏幕更新后的提交结束时运行。这是将 React 组件与某些外部系统(如网络或第三方库)同步的好时机。

注意

在这里和后面的这篇文章中,大写的“Effect”指的是上面特定于 React 的定义,即由渲染引起的副作用。为了参考更广泛的编程概念,我们将说“副作用”。

您可能不需要Effect

**不要急于将Effect添加到组件中。**请记住,Effect 通常用于“跳出”您的 React 代码并与某些外部系统同步。这包括浏览器 API、第三方小部件、网络等。如果Effect仅根据其他状态调整某些状态,则可能不需要Effect。

如何编写Effect

若要编写Effect,请按照以下三个步骤操作:

  1. **声明Effect。**默认情况下,Effect将在每次渲染后运行。
  2. **指定Effect依赖项。**大多数Effect只应在需要时重新运行,而不是在每次渲染后重新运行。例如,淡入动画应仅在组件出现时触发。仅当组件出现和消失或聊天室更改时,才应连接和断开与聊天室的连接。您将学习如何通过指定依赖项来控制这一点
  3. **根据需要添加清理。**某些Effect需要指定如何停止、撤消或清理它们正在执行的操作。例如,“连接”需要“断开连接”,“订阅”需要“取消订阅”,“获取”需要“取消”或“忽略”。您将学习如何通过返回清理函数来执行此操作。

让我们详细看一下这些步骤中的每一个。

步骤 1:声明Effect

要在组件中声明一个 Effect,请从 React 导入 useEffect Hook

import { useEffect } from 'react';

然后,在组件的顶层调用它,并在 Effect 中放置一些代码:

function MyComponent() {

  useEffect(() => {
    // Code here will run after *every* render
  });

  return <div />;
}

每次组件渲染时,React 都会更新屏幕*,然后在*useEffect里面运行代码。换句话说,useEffect 会“延迟”一段代码的运行,直到该渲染反映在屏幕上。

让我们看看如何使用 Effect 与外部系统同步。考虑一个 React 组件<VideoPlayer>。通过向它传递isPlaying来控制它是在播放还是暂停:

<VideoPlayer isPlaying={isPlaying} />;

您的自定义组件isPlaying会基于浏览器内置的 <video> 标签来渲染:

function VideoPlayer({ src, isPlaying }) {
  // TODO: do something with isPlaying
  return <video src={src} />;
}

但是,浏览器<video>标签没有isPlayingprop。控制它的唯一方法是在 DOM 元素上手动调用 play()pause() 方法。您需要将 isPlaying prop 的值与 play()pause() 的调用同步,该值告诉视频当前是否应该正在播放。

我们需要首先获取 DOM 节点<video>ref

您可能会在渲染期间尝试调用play()或者pause(),但这是不正确的:

import { useRef, useState } from 'react';

function VideoPlayer({ src, isPlaying }) {
	const ref = useRef(null);

	if (isPlaying) {
		ref.current.play(); // Calling these while rendering isn't allowed.
	} else {
		ref.current.pause(); // Also, this crashes.
	}

	return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
	const [isPlaying, setIsPlaying] = useState(false);
	return (
		<>
			<button onClick={() => setIsPlaying(!isPlaying)}>{isPlaying ? '暂停' : '播放'}</button>
			<VideoPlayer isPlaying={isPlaying} src='https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4' />
		</>
	);
}

运行的报错

App.js: Cannot read properties of null (reading 'pause') (9:16)

6 |   if (isPlaying) {
7 |     ref.current.play();  // Calling these while rendering isn't allowed.
8 |   } else {
>  9 |     ref.current.pause(); // Also, this crashes.
                       ^
  10 |   }
  11 | 
  12 |   return <video ref={ref} src={src} loop playsInline />;

此代码不正确的原因是它尝试在渲染期间对 DOM 节点执行某些操作。在 React 中,渲染应该是 JSX 的纯粹计算,不应该包含修改 DOM 等副作用。

而且,当VideoPlayer第一次调用时,它的 DOM 还不存在!目前还没有一个 DOM 节点可以调用play()或者pause(),因为 React 不知道要创建什么 DOM,直到你返回 JSX。

这里的解决方案是使用 useEffect 包装副作用,将其移出渲染计算:

import { useEffect, useRef } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

通过将 DOM 更新包装在 Effect 中,您可以让 React 首先更新屏幕。然后你的 Effect 运行。

VideoPlayer组件渲染时(第一次或重新渲染),会发生一些事情。首先,React 将更新屏幕,确保<video>标签在 DOM 中使用正确的props。然后 React 将运行你的 Effect。最后,您的 Effect 将调用play()|或pause()取决于isPlaying 的值。

多次按播放/暂停键,然后查看视频播放器如何与该isPlaying值保持同步:

import { useEffect, useRef, useState } from 'react';

function VideoPlayer({ src, isPlaying }) {
	const ref = useRef(null);

	useEffect(() => {
		if (isPlaying) {
			ref.current.play();
		} else {
			ref.current.pause();
		}
	});

	return <video ref={ref} src={src} loop playsInline controls />;
}

export default function App() {
	const [isPlaying, setIsPlaying] = useState(false);
	return (
		<>
			<button onClick={() => setIsPlaying(!isPlaying)}>{isPlaying ? '暂停' : '播放'}</button>
			<VideoPlayer isPlaying={isPlaying} src='https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4' />
		</>
	);
}

在此示例中,您同步到 React 状态的“外部系统”是浏览器媒体 API。你可以使用类似的方法将遗留的非 React 代码(如 jQuery 插件)包装到声明式 React 组件中。

请注意,在实践中控制视频播放器要复杂得多。调用play()可能会失败,用户可能会使用内置浏览器控件播放或暂停等等。这个例子非常简化和不完整。

陷阱

默认情况下,Effect在每次渲染后运行。这就是为什么这样的代码会产生无限循环的原因:

const [count, setCount] = useState(0);

useEffect(() => {

setCount(count + 1);

});

Effect 作为渲染的结果运行。设置状态会触发渲染。在 Effect 中立即设置状态就像将电源插座插入自身一样。Effect 运行,它设置状态,这会导致重新渲染,这会导致 Effect 运行,它再次设置状态,这会导致另一个重新渲染,依此类推。

Effect 通常应将组件与外部系统同步。如果没有外部系统,而您只想根据其他状态调整某些状态,则可能不需要Effect 。

步骤 2:指定Effect依赖项

默认情况下,Effect 在每次渲染后运行。通常,这不是您想要的:

  • 有时,它很慢。与外部系统同步并不总是即时的,因此除非必要,否则您可能希望跳过同步。例如,您不希望每次击键都重新连接到聊天服务器。
  • 有时,这是错误的。例如,您不希望在每次击键时触发组件淡入动画。动画只应在组件首次出现时播放一次。

为了演示此问题,下面是前面的示例,其中包含一些调用console.log和更新父组件状态的文本输入。请注意键入如何导致 Effect 重新运行:

import { useEffect, useRef, useState } from 'react';

function VideoPlayer({ src, isPlaying }) {
	const ref = useRef(null);

	useEffect(() => {
		if (isPlaying) {
			console.log('Calling video.play()');
			ref.current.play();
		} else {
			console.log('Calling video.pause()');
			ref.current.pause();
		}
	});

	return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
	const [isPlaying, setIsPlaying] = useState(false);
	const [text, setText] = useState('');
	return (
		<>
			<input value={text} onChange={e => setText(e.target.value)} />
			<button onClick={() => setIsPlaying(!isPlaying)}>{isPlaying ? '暂停' : '播放'}</button>
			<VideoPlayer isPlaying={isPlaying} src='https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4' />
		</>
	);
}

你可以告诉 React 跳过不必要的重新运行 Effect,方法是指定一个依赖项数组作为调用useEffect的第二个参数。首先在第 14 行向上面的示例添加一个空数组[]

useEffect(() => {
  // ...
}, []);

您应该看到一个错误,指出:React Hook useEffect has a missing dependency: 'isPlaying'

import { useEffect, useRef, useState } from 'react';

function VideoPlayer({ src, isPlaying }) {
	const ref = useRef(null);

	useEffect(() => {
		if (isPlaying) {
			console.log('Calling video.play()');
			ref.current.play();
		} else {
			console.log('Calling video.pause()');
			ref.current.pause();
		}
	}, []); // This causes an error

	return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
	const [isPlaying, setIsPlaying] = useState(false);
	const [text, setText] = useState('');
	return (
		<>
			<input value={text} onChange={e => setText(e.target.value)} />
			<button onClick={() => setIsPlaying(!isPlaying)}>{isPlaying ? '暂停' : '播放'}</button>
			<VideoPlayer isPlaying={isPlaying} src='https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4' />
		</>
	);
}

lint错误

14:6 - React Hook useEffect has a missing dependency: 'isPlaying'. Either include it or remove the dependency array.

问题是 Effect 中的代码依赖于isPlaying prop 来决定做什么,但这种依赖关系没有明确声明。若要解决此问题,请添加isPlaying到依赖项数组:

 useEffect(() => {
    if (isPlaying) { // It's used here...
      // ...
    } else {
      // ...
    }
  }, [isPlaying]); // ...so it must be declared here!

现在声明了所有依赖项,因此没有错误。指定为[isPlaying]依赖数组告诉 React 如果isPlaying与上次渲染期间相同,它应该跳过重新运行您的 Effect。通过此更改,在输入中键入不会导致效果重新运行,但按播放/暂停会导致:

import { useEffect, useRef, useState } from 'react';

function VideoPlayer({ src, isPlaying }) {
	const ref = useRef(null);

	useEffect(() => {
		if (isPlaying) {
			console.log('Calling video.play()');
			ref.current.play();
		} else {
			console.log('Calling video.pause()');
			ref.current.pause();
		}
	}, [isPlaying]);

	return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
	const [isPlaying, setIsPlaying] = useState(false);
	const [text, setText] = useState('');
	return (
		<>
			<input value={text} onChange={e => setText(e.target.value)} />
			<button onClick={() => setIsPlaying(!isPlaying)}>{isPlaying ? 'Pause' : 'Play'}</button>
			<VideoPlayer isPlaying={isPlaying} src='https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4' />
		</>
	);
}

依赖项数组可以包含多个依赖项。只有当你指定的所有依赖项都具有与上一个渲染期间完全相同的值时,React 才会跳过重新运行 Effect 。React 使用 Object.is 比较来比较依赖值。有关详细信息,请参阅 useEffect 参考

**请注意,不能“选择”依赖项。**如果您指定的依赖项与 React 根据 Effect 中的代码所期望的不匹配,您将收到 lint 错误。这有助于捕获代码中的许多错误。如果不希望重新运行某些代码,请编辑 Effect 代码本身,使其“不需要”该依赖项。

陷阱

没有依赖关系数组和[]依赖关系数组的行为是不同的:

useEffect(() => {
// 这在每次渲染后运行
});

useEffect(() => {
// 这只在mount时运行(当组件出现时)。
}, []);

useEffect(() => {
// 这运行在mount *和*如果a或b自上次渲染以来发生了变化
}, [a, b]);

我们将在下一步中仔细研究“mount”的含义。

步骤 3:根据需要添加清理

考虑一个不同的例子。您正在编写一个组件ChatRoom,该组件需要在聊天服务器出现时连接到聊天服务器。您将获得一个 createConnection() API,该 API 返回一个带有 connect()disconnect() 方法的对象。如何在向用户显示组件时保持连接?

首先编写效果逻辑:

useEffect(() => {
  const connection = createConnection();
  connection.connect();
});

每次重新渲染后连接到聊天会很慢,因此您添加依赖项数组:

useEffect(() => {
  const connection = createConnection();
  connection.connect();
}, []);

效果中的代码不使用任何属性或状态,因此依赖项数组为 [](空)。这告诉 React 只在组件“挂载”时运行这段代码,即第一次出现在屏幕上。

让我们尝试运行此代码:

// App.js

import { useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}


// chat.js
export function createConnection() {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting...');
    },
    disconnect() {
      console.log('❌ Disconnected.');
    }
  };
}

此效果仅在组件挂载时运行,因此您可能希望"✅ Connecting..."在控制台中打印一次。但是,如果您检查控制台,“正在连接...”✅会被打印两次。为什么会这样?

假设该ChatRoom组件是具有许多不同屏幕的较大应用程序的一部分。用户在ChatRoom页面上开始他们的旅程。组件装载并调用 connection.connect()。然后假设用户导航到另一个屏幕,例如,导航到“设置”页面。ChatRoom组件将卸载。最后,用户单击“返回”并再次挂载 ChatRoom组件。这将建立第二个连接,但第一个连接从未被销毁!当用户在应用中导航时,连接会不断堆积。

如果没有大量的手动测试,像这样的错误很容易被遗漏。为了帮助您快速发现它们,在开发中,React 会在初始挂载后立即重新挂载每个组件一次。

查看"✅ Connecting..."日志两次有助于您注意到真正的问题:当组件卸载时,您的代码不会关闭连接。

要解决此问题,请从 Effect 返回清理函数

useEffect(() => {
  const connection = createConnection();
  connection.connect();
  return () => {
    connection.disconnect();
  };
}, []);

React 每次都会在 Effect 再次运行之前调用你的清理函数,最后一次在组件卸载(被删除)时调用。让我们看看实现清理功能时会发生什么:

// App.js

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the chat!</h1>;
}


// chat.js
export function createConnection() {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting...');
    },
    disconnect() {
      console.log('❌ Disconnected.');
    }
  };
}

现在,您可以在开发中获得三个控制台日志:

  1. "✅ Connecting..."
  2. "❌ Disconnected."
  3. "✅ Connecting..."

**这是开发中的正确行为。**通过重新挂载你的组件,React 会验证离开和返回导航不会破坏你的代码。断开连接然后再次连接正是应该发生的事情!当您很好地实现清理时,运行一次效果与运行它、清理它和再次运行它之间应该没有用户可见的区别。有一个额外的连接/断开连接调用对,因为 React 正在探测您的代码是否存在开发中的错误。这是正常的 - 不要试图让它消失!

**在生产中,您只会看到“连接...”✅打印一次。**重新挂载组件仅在开发中发生,以帮助您找到需要清理的效果。您可以关闭严格模式以选择退出开发行为,但我们建议您保持打开状态。这使您可以找到许多类似于上面的错误。

如何处理开发中两次的 Effect 发射?

React 有意在开发中重新挂载组件以查找错误,如上一个例子所示。正确的问题不是“如何运行一次效果”,而是“如何修复我的 Effect,使其在重新安装后正常工作”。

通常,答案是实现清理功能。清理功能应停止或撤消 Effect 正在执行的任何操作。经验法则是,用户不应能够区分运行一次的 Effect(如在生产中)和设置→清理→设置序列(如您在开发中看到的那样)。

您将编写的大多数 Effect 都适合以下常见模式之一。

控制非 React 小部件

有时你需要添加未写入 React 的 UI 小部件。例如,假设您要向页面添加地图组件。它有一个setZoomLevel()方法,您希望使缩放级别zoomLevel与 React 代码中的状态变量保持同步。您的效果将如下所示:

useEffect(() => {
  const map = mapRef.current;
  map.setZoomLevel(zoomLevel);
}, [zoomLevel]);

请注意,在这种情况下不需要清理。在开发中,React 会调用 Effect 两次,但这不是问题,因为用相同的setZoomLevel调用两次不会做任何事情。它可能稍微慢一些,但这并不重要,因为它不会在生产中不必要地重新装载。

某些 API 可能不允许连续调用它们两次。例如,内置 dialog 元素的 showModal 方法在调用两次时会引发。实现清理功能并使其关闭对话框:

useEffect(() => {
  const dialog = dialogRef.current;
  dialog.showModal();
  return () => dialog.close();
}, []);

在开发中,您的 Effect 将调用 showModal(),然后立即调用 close(),然后再次调用 showModal()。这与调用showModal()一次具有相同的用户可见行为,正如您在生产中看到的那样。

订阅活动

如果您的效果订阅了某些内容,清理功能应取消订阅:

useEffect(() => {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY);
  }
  window.addEventListener('scroll', handleScroll);
  return () => window.removeEventListener('scroll', handleScroll);
}, []);

在开发中,您的 Effect 将调用 addEventListener(),然后立即调用 removeEventListener(),然后使用相同的处理程序再次调用 addEventListener()。因此,一次只有一个活动订阅。这与在生产中调用一次addEventListener()具有相同的用户可见行为。

触发动画

如果 Effect 在其中对某些内容进行动画处理,则清理函数应将动画重置为初始值:

useEffect(() => {
  const node = ref.current;
  node.style.opacity = 1; // Trigger the animation
  return () => {
    node.style.opacity = 0; // Reset to the initial value
  };
}, []);

在开发中,不透明度将设置为 1,然后设置为 0,然后再设置为 1。这应该与将其设置为1直接具有相同的用户可见行为,这是在生产中发生的情况。如果使用支持补间的第三方动画库,则清理函数应将时间轴重置为其初始状态。

获取数据

如果您的 Effect 获取了某些内容,则清理函数应该中止获取或忽略其结果:

useEffect(() => {
  let ignore = false;

  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }

  startFetching();

  return () => {
    ignore = true;
  };
}, [userId]);

您无法“撤消”已经发生的网络请求,但清理功能应确保不再相关的提取不会继续影响您的应用程序。如果userId'Alice' 更改为 'Bob',则清理可确保'Alice'响应被忽略,即使响应在 'Bob' 之后到达。

在**开发中,您将在“网络”选项卡中看到两个提取。**这没有错。使用上述方法,第一个 Effect 将立即被清理,因此其变量ignore将设置为 true。因此,即使有额外的请求,由于检查if (!ignore),它不会影响状态。

**在生产中,只有一个请求。**如果开发中的第二个请求困扰您,最好的方法是使用一个解决方案来删除重复数据的请求并在组件之间缓存它们的响应:

function TodoList() {

  const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);

  // ...

这不仅可以改善开发体验,还可以使您的应用程序感觉更快。例如,按“后退”按钮的用户不必等待某些数据再次加载,因为它将被缓存。您可以自己构建这样的缓存,也可以使用效果中手动获取的众多替代方法之一。

发送分析

请考虑以下代码,该代码在页面访问时发送分析事件:

useEffect(() => {
  logVisit(url); // Sends a POST request
}, [url]);

在开发中,对于每个 URL ,logVisit 将被调用两次,因此您可能会尝试尝试解决此问题。**我们建议保持此代码不变。**与前面的示例一样,运行一次和运行两次之间没有用户可见的行为差异。从实际的角度来看,logVisit不应在开发中执行任何操作,因为您不希望来自开发计算机的日志扭曲生产指标。每次保存其文件时,组件都会重新挂载,因此无论如何它都会在开发中记录额外的访问。

在生产环境中,不会有重复的访问日志。

若要调试要发送的分析事件,可以将应用部署到过渡环境(在生产模式下运行),或暂时选择退出严格模式及其仅限开发的重新装载检查。您还可以从路由更改事件处理程序而不是效果发送分析。为了进行更精确的分析,交集观察器可以帮助跟踪视口中有哪些组件以及它们保持可见的时间。

非效果:初始化应用程序

某些逻辑只应在应用程序启动时运行一次。您可以将其放在组件之外:

if (typeof window !== 'undefined') { // Check if we're running in the browser.
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}

这保证了此类逻辑在浏览器加载页面后仅运行一次。

无效:购买产品

有时,即使您编写了清理函数,也无法防止用户看到运行 Effect 两次的后果。例如,您的效果可能会发送一个 POST 请求,例如购买产品:

useEffect(() => {
  // 🔴 Wrong: This Effect fires twice in development, exposing a problem in the code.
  fetch('/api/buy', { method: 'POST' });
}, []);

您不会想购买该产品两次。但是,这也是您不应该将此逻辑放在效果中的原因。如果用户转到另一个页面,然后按“返回”,该怎么办?您的效果将再次运行。您不想在用户访问页面时购买产品;您希望在用户单击“购买”按钮时购买它。

购买不是由渲染引起的;它是由特定的交互引起的。它应仅在用户按下按钮时运行。删除效果并将 /api/buy 请求移动到“购买”按钮事件处理程序中:

 function handleClick() {
    // ✅ Buying is an event because it is caused by a particular interaction.
    fetch('/api/buy', { method: 'POST' });
  }

这**说明,如果重新挂载破坏了应用程序的逻辑,这通常会发现现有的错误。**从用户的角度来看,访问页面不应与访问页面、单击链接并按“返回”没有什么不同。React 通过在开发中重新挂载它们来验证您的组件是否符合此原则。

将一切整合在一起

这个游乐场可以帮助您“感受”效果在实践中的工作方式。

此示例使用 setTimeout 计划一个控制台日志,其中输入文本在效果器运行三秒后显示。清理功能取消挂起的超时。首先按“安装组件”:

import { useEffect, useState } from 'react';

function Playground() {
	const [text, setText] = useState('a');

	useEffect(() => {
		function onTimeout() {
			console.log('⏰ ' + text);
		}

		console.log('🔵 Schedule "' + text + '" log');
		const timeoutId = setTimeout(onTimeout, 3000);

		return () => {
			console.log('🟡 Cancel "' + text + '" log');
			clearTimeout(timeoutId);
		};
	}, [text]);

	return (
		<>
			<label>
				What to log: <input value={text} onChange={e => setText(e.target.value)} />
			</label>
			<h1>{text}</h1>
		</>
	);
}

export default function App() {
	const [show, setShow] = useState(false);
	return (
		<>
			<button onClick={() => setShow(!show)}>{show ? 'Unmount' : 'Mount'} the component</button>
			{show && <hr />}
			{show && <Playground />}
		</>
	);
}

您将首先看到三个日志:Schedule "a" logCancel "a" log 和再次 Schedule "a" log。三秒钟后还会有日志说 a。正如你之前所知道的,额外的调度/取消对是因为 React 在开发过程中重新挂载组件,以验证你是否很好地实现了清理。

现在编辑输入以说 abc。如果操作速度足够快,则会看到Cancel "ab" log紧跟 Schedule "ab" logSchedule "abc" log。**React 总是在下一个渲染的效果之前清理上一个渲染的效果。**这就是为什么即使您快速输入输入,一次最多安排一个超时。编辑输入几次并观看控制台,以了解效果是如何清理的。

在输入中键入内容,然后立即按“卸载组件”。请注意卸载如何清理上次渲染的效果。在这里,它会在有机会触发之前清除最后一次超时。

最后,编辑上面的组件并注释掉清理功能,以免取消超时。尝试快速键入abcde。你期望在三秒钟内发生什么?超时内console.log(text)会打印最新text并生成五个abcde日志吗?尝试检查一下您的直觉!

三秒钟后,您应该看到一系列日志(aababcabcdabcde),而不是五个abcde日志。**每个效果都从其相应的渲染中“捕获”text值。**状态text改变并不重要:text = 'ab'渲染 Effect 将始终看到 'ab'。换句话说,每个渲染的效果彼此隔离。如果你好奇这是如何工作的,你可以阅读关于闭包的信息。

Effect的生命周期

React 16.8 版本正式发布了 Hook 机制,React生命周期分为 Class Component(类组件) 生命周期与 Function Component (函数组件)生命周期。

Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。

回顾下在 Class Component 的数据请求:

  1. componentDidMount 初始化发请求;
  2. componentDidUpdate 判断参数是否变化,变化就调用请求函数重新请求数据;
  3. componentWillUnmount 生命周期取消发送请求。

那么在函数组件中我们该如何做呢?答案是 useEffect

useEffect

useEffect 就是一个 Effect Hook ,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

useEffect 做了什么:

  • 使用 useEffect 相当于告诉 React 组件需要在渲染后执行某些操作,React 将在执行 DOM 更新之后调用它。
  • React 保证了每次运行 useEffect 的时候,DOM 已经更新完毕。

useEffect 默认情况下,它在第一次渲染之后和每次更新之后都会执行。

Class 组件 Demo:

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>
    );
  }
}

Function Component 重写该案例:

  • 请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect。
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

需要清除的 effect

在 class 组件中,我们去监听原生 DOM 事件时,会在 componentDidMount 这个生命周期中去做,因为在这里可以获取到已经挂载的真实 DOM。我们也会在组件卸载的时候去取消事件监听避免内存泄露。那么在 useEffect 中该如何实现呢?

通过在 useEffect 中返回一个函数,它便可以清理副作用:

  • 如果需要清除操作 useEffect 函数需返回一个清除函数
  • 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染(通常如此),则在执行下一个 effect 之前,上一个 effect 就已被清除
useEffect(() => {
  console.log('effect');
  return () => {
    console.log("清除函数");
  };
});

清理规则是:

  • 首次渲染不会进行清理,会在下一次渲染,清除上一次的副作用;
  • 卸载阶段也会执行清除操作。

Effect依赖

如果需要 useEffect 按照某种条件运行,可以给 useEffect 传递第二个参数

  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
useEffect(() => {
  console.log('effect');
}, []);
  • [count] 只有当 count 的值发生变化时该 userEffect 才会执行
useEffect(() => {
  console.log('effect', props.number);
  return () => {
    console.log('清除函数');
  };
}, [count]);

网络请求中的应用

useEffect 中我们会去请求后台数据,通过前面的学习我们也了解到每次更新组件时我们都会再次去执行 useEffect ,但其实我们并不需要每次更新组件都发送请求。那么碰到这样的问题如何处理呢?

回顾上面是不是类似于 componentDidUpdate 中发送请求呢?直觉是对的,在componentDidUpdate 中我们是通过判断参数是否变化来避免每次都发送请求,那么在 useEffect hook 中我们也是异曲同工,通过第二个参数是否发生变换来决定是否重复执行,如果第二参数为空数组,则表示只在初始化执行一次,后续更新不会再次调用。

useEffect(() => {
  fetchData(instanceId){...}
  
  fetchData(instanceId);
}, [instanceId]);

上面例子是通过 fetchData 函数去请求后台数据,具体函数体我们就省略了,然后你会发现useEffect 的第二个参数添加了一个数组,其中添加了一个参数 instanceId,它表示只有当instanceId 变化时,我们才去再次执行 useEffect。这样就可以避免我们多次请求后台数据。

当然我们的依赖项还可以传入一个空数组,就表示只在初始化时执行一次:

useEffect(() => {
  fetchData(instanceId){...}
  
  fetchData(instanceId);
}, []);

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a);
  },
  [a],
);

把内联回调函数及依赖项数组作为参数传入 useCallback ,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

通俗来讲当参数 a 发生变化时,会返回一个新的函数引用赋值给 memoizedCallback 变量,因此这个变量就可以当做 useEffect 第二个参数。这样就有效的将逻辑分离出来。

function Parent(){
	const [query,setQuery] = useState('q');
  const fetchData = useCallback(()=>{
  	...省略函数体的具体实现
  },[query]);
  return <Child fetchData={fetchData} />
}
  
function Child({fetchData}){
  const [data,setData] = useState(null);
	useEffect(()=>{
  	fetchData().then(setData);
  },[fetchData])
}

经过 useCallback 包装过的函数可以当作普通变量作为 useEffect 的依赖。 useCallback做的事情,就是在其依赖变化时,返回一个新的函数引用,触发 useEffect 的依赖变化,并激活其重新执行。

现在我们不需要在 useEffect 依赖中直接对比 query 参数了,而可以直接对比 fetchData 函数。useEffect 只要关心 fetchData 函数是否变化,而 fetchData 参数的变化在 useCallback 时关心,能做到 依赖不丢、逻辑内聚,从而容易维护。

表单绑定

表单的组件分为受控组件和非受控组件

  • 受控组件:由React管理了表单元素的value
  • 非受控组件:表单元素的value就是原生的DOM管理的

受控组件

在 HTML 中,表单元素(如<input><textarea><select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。

我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

对于受控组件来说,输入的值始终由 React 的 state 驱动。

在input上监听输入框的变化使用onChange监听事件:input原生DOM中change事件是输入变化并失去焦点时触发,在react中onChange是输入变化时触发,类似原生DOM的input事件

input[type=text]双向绑定,input组件绑定的是value属性的值:

  • value 绑定状态
  • onChange 监听事件并修改状态
import { useState } from 'react';

export default function App() {
	const [formData, setFormData] = useState({
		username: '',
		age: '',
	});

	// 提交表单数据
	function handleSubmit(ev) {
		ev.preventDefault();
		const { username, age } = formData;
		console.log('提交的数据 username, age: ', username, age);
	}
	// 保存表单数据到state中
	function handleChange(ev) {
		const { name, value } = ev.target;
		setFormData(() => ({
			...formData,
			...{
				[name]: value,
			},
		}));
	}

	return (
		<div>
			<form onSubmit={handleSubmit}>
				<input type='text' name='username' value={formData.username} onChange={handleChange} /> <br />
				<input type='text' name='age' value={formData.age} onChange={handleChange} /> <br />
				<input type='submit' value='提交' />
			</form>
		</div>
	);
}

给多个input组件绑定同一个函数的时候,也可以采用下面的写法

function handleChange(ev, field) {
  const { value } = ev.target;
  setFormData(() => ({
    ...formData,
    ...{
      [field]: value,
    },
  }));
}


<input type='text' name='username' value={formData.username} onChange={ev => handleChange(ev, 'username')} /> <br />
<input type='text' name='age' value={formData.age} onChange={ev => handleChange(ev, 'age')} /> <br />

或者使用函数柯里化

function handleChange(field) {
  return function (ev) {
    const { value } = ev.target;
    setFormData(() => ({
      ...formData,
      ...{
        [field]: value,
      },
    }));
  };
}

<input type='text' name='username' value={formData.username} onChange={handleChange('username')} /> <br />
<input type='text' name='age' value={formData.age} onChange={handleChange('age')} /> <br />

非受控组件

在大多数情况下,我们推荐使用受控组件来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。

要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用 ref 来从 DOM 节点中获取表单数据。

在 React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value

  • 使用defaultValue 的组件,其value值就是用户输入的内容,React完全不管理输入的过程

同样,<input type="checkbox"><input type="radio"> 支持 defaultChecked<select><textarea> 支持 defaultValue

import { useRef, useState } from 'react';

export default function App() {
	const nameRef = useRef(null);
	const ageRef = useRef(null);

	const [formData, setFormData] = useState({
		username: '',
		age: '',
	});

	// 提交表单数据
	function handleSubmit(ev) {
		ev.preventDefault();
		console.log('提交的数据 username, age: ', nameRef.current.value, ageRef.current.value);
	}

	return (
		<div >
			<form onSubmit={handleSubmit}>
				<input type='text' name='username' defaultValue={formData.username} ref={nameRef} /> <br />
				<input type='text' name='age' defaultValue={formData.age} ref={ageRef} /> <br />
				<input type='submit' value='提交' />
			</form>
		</div>
	);
}

因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

  • 文件输入:在 HTML 中,<input type="file"> 可以让用户选择一个或多个文件上传到服务器,或者通过使用 File API 进行操作。
<input type="file" />

在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。

对比受控组件和非受控组件

  • 非受控组件: 用户输入A --> input 中显示A;
  • 受控组件: 用户输入A --> 触发onChange事件 --> saveData中设置:formData.username= “A” --> 渲染input使他的value变成A;

正是因为这样,强烈推荐使用受控组件,因为它能更好的控制组件的生命流程。

其他受控组件

  • textarea绑定的是value属性的值:双向绑定用与input[type=text]法一致
import { useState } from 'react';

export default function App() {
	const [message, setMessage] = useState('');

	return (
		<div>
			<textarea value={message} onChange={(ev)=>setMessage(ev.target.value)}></textarea>
		</div>
	);
}
  • 复选框 checkbox 绑定的不是 value 属性 ,而是 checked 属性,绑定的是布尔值:
    • checked 绑定状态
    • onChange 监听事件
import { useState } from 'react';

export default function App() {
	const [formData, setFormData] = useState({
		isChoose: false,
	});

	function handleChange(ev) {
		setFormData({ isChoose: ev.target.checked });
	}

	return (
		<div>
			性别:
			<input type='checkbox' checked={formData.isChoose} onChange={handleChange} />
			{formData.isChoose ? '男' : '女'}
		</div>
	);
}
  • 单选框 radio 绑定value属性的值
import { useState } from 'react';

export default function App() {
	const [formData, setFormData] = useState({
		sex: '',
	});

	function handleChange(ev) {
		setFormData({ sex: ev.target.value });
	}

	return (
    <div>
			性别:
			<input type='radio' name='sex' value='男' onChange={handleChange} />男
			<input type='radio' name='sex' value='女' onChange={handleChange} />女
		</div>
	);
}

  • select绑定绑定的是option标签value属性的值:双向绑定用法与input[type=text]一致
import { useState } from 'react';

export default function App() {
	const [hobby, setHobby] = useState('');

	return (
		<div>
			<p> 1:{formData.hobby}</p>
			选择喜欢的专业:
			<select value={hobby} onChange={(ev)=>setHobby(ev.target.value)}>
				<option value='' disabled>
					请选择
				</option>
				<option value='html'>html</option>
				<option value='js'>js</option>
				<option value='css'>css</option>
			</select>
		</div>
	);
}

组件间共享数据 (状态提升)

有时候,你希望两个组件的状态始终同步更改。要实现这一点,可以将相关 state 从这两个组件上移除,并把 state 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为“状态提升”,这是编写 React 代码时常做的事。

状态提升的例子

在这个例子中,父组件 MyApp 渲染了 2 个独立的 MyButton 组件。

  • MyApp
    • MyButton
    • MyButton

每个 MyButton 组件都有一个 count ,用于控制点击的次数。

import { useState } from 'react';

export default function MyApp() {
	return (
		<div>
			<h1>计数器</h1>
			<MyButton />
			<MyButton />
		</div>
	);
}

function MyButton() {
	const [count, setCount] = useState(0);

	return <button onClick={() => setCount(count + 1)}>点击了 {count} 次</button>;
}

在这个示例中,每个 MyButton 都有自己独立的 count,当每个按钮被点击时,只有被点击按钮的 count 才会发生改变,发现点击其中一个按钮并不会影响另外一个,他们是独立的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7uZphrb-1682432582912)(https://zh-hans.react.dev/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fsharing_data_child.png&w=640&q=75)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T0NFVjsY-1682432582913)(https://zh-hans.react.dev/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fsharing_data_child_clicked.png&w=640&q=75)]
起初,每个 MyButtoncount state 均为 0第一个 MyButton 会将 count 更新为 1

假设现在您想改变这种行为,以便在任何时候共享数据并一起更新。 在这种设计下,为了使得 MyButton 组件显示相同的 count 并一起更新,您该如何做到这一点呢?你需要将各个按钮的 state “向上” 移动到最接近包含所有按钮的组件之中。

要协调好这两个按钮,我们需要分 3 步将状态“提升”到他们的父组件中。

  1. 从子组件中 移除 state 。
  2. 从父组件 传递 硬编码数据。
  3. 为共同的父组件添加 state ,并将其与事件处理函数一起向下传递。

这样, MyApp 父组件就可以控制 2 个 MyButton组件,保证同两个MyButton组件共享数据。

状态提升三步走

第 1 步: 从子组件中移除状态

你将把 MyButton 组件对 count 的控制权交给他们的父组件。这意味着,父组件会将 count 作为 prop 传给子组件 MyButton

首先,将 MyButtonstate 上移到 MyApp 中,先从 MyButton 组件中 删除下面这一行

const [count, setCount] = useState(0);

然后,把 count 加入 MyButton 组件的 props 中:

function MyButton({ count }) {

现在 MyButton 的父组件就可以通过 向下传递 prop控制 count。但相反地,MyButton 组件对 count 的值 没有控制权 —— 现在完全由父组件决定!

export default function MyApp() {

  return (
    <div>
      <h1>计数器</h1>
      <MyButton />
      <MyButton />
    </div>
  );
}


function MyButton({ count = 0 }) {
  // ... we're moving code from here ...
  return <button>点击了 {count} 次</button>;
}

第 2 步: 从公共父组件传递数据

为了实现状态提升,必须定位到你想协调的 两个 子组件最近的公共父组件:

  • MyApp(最近的公共父组件)
    • MyButton
    • MyButton

在这个例子中,公共父组件是 MyApp。因为它位于两个按钮之上,可以控制它们的 props,所以它将成为当前按钮数据的“控制之源”。通过 MyApp 组件将硬编码值 count(例如 1 )传递给两个按钮:

export default function MyApp() {

  return (
    <div>
      <h1>计数器</h1>
      <MyButton count={1} />
      <MyButton count={2} />
    </div>
  );
}

function MyButton({ count = 0 }) {
  return <button>点击了 {count} 次</button>;
}

你可以尝试修改 MyApp 组件中 count 的值,并在屏幕上查看结果。

第 3 步: 为公共父组件添加状态

在这个例子中,共享数据并一起更新。这意味着 MyApp 这个父组件需要记录 按钮 被点击的次数。在 MyApp 组件中添加以下代码,来记录按钮被点击的次数,并添加 handleClick 函数来改变count的值:

const [count, setCount] = useState(0);

function handleClick() {
  setCount(count + 1);
}

在任意一个 MyButton 中点击按钮都需要更改 MyApp 中的count的值。 MyButton 中无法直接设置状态 count 的值,因为该状态是在 MyApp 组件内部定义的。 MyApp 组件需要 显式允许 MyButton 组件通过 将事件处理程序作为 prop 向下传递 来更改其状态:

MyApp 中的点击事件处理函数handleClick以及 state (count)一同向下传递到 每个 MyButton 中:

<MyButton count={count} onCountChange={handleClick} />

<MyButton count={count} onCountChange={handleClick} />

最后,改变 MyButton读取 从父组件传递来的 prop:

function MyButton({ count = 0, onCountChange }) {
	return <button onClick={onCountChange}>点击了 {count} 次</button>;
}

现在 MyButton 组件中的 <button> 将使用 onCountChange 这个属性作为其点击事件的处理程序:

import { useState } from 'react';
export default function MyApp() {
	const [count, setCount] = useState(0);

	function handleClick() {
		setCount(count + 1);
	}

	return (
		<div>
			<h1>计数器</h1>
			<MyButton count={count} onCountChange={handleClick} />
			<MyButton count={count} onCountChange={handleClick} />
		</div>
	);
}


function MyButton({ count = 0, onCountChange }) {
	return <button onClick={onCountChange}>点击了 {count} 次</button>;
}

当你点击按钮时,onClick 处理程序会启动。每个按钮的 onCountChange prop 会被设置为 MyApp 内的 handleClick 函数,所以函数内的代码会被执行。该代码会调用 setCount(count + 1),使得 state 变量 count 递增。新的 count 值会被作为 prop 传递给每个按钮,因此它们每次展示的都是最新的值。这被称为“状态提升”。通过向上移动 state,我们实现了在组件间共享它。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SbKxZxDI-1682432582914)(https://zh-hans.react.dev/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fsharing_data_parent.png&w=640&q=75)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rEpoyxzA-1682432582914)(https://zh-hans.react.dev/_next/image?url=%2Fimages%2Fdocs%2Fdiagrams%2Fsharing_data_parent_clicked.png&w=640&q=75)]
起初,MyAppcount state 为 0 并传递给了两个子组件点击后,MyAppcount state 更新为 1,并将其传递给两个子组件

此刻,当你点击任何一个按钮时,MyApp 中的 count 都将改变,同时会改变 MyButton 中的两个 count。

这样,我们就完成了对状态的提升!将状态移至公共父组件中可以让你更好的管理这两个按钮。使用count记录点击的次数。而通过向下传递事件处理函数handleClick可以让子组件修改父组件的状态。

每个状态都对应唯一的数据源

React 应用中,很多组件都有自己的状态。一些状态可能“活跃”在叶子组件(树形结构最底层的组件)附近,例如输入框。另一些状态可能在应用程序顶部“活动”。例如,客户端路由库也是通过将当前路由存储在 React 状态中,利用 props 将状态层层传递下去来实现的!

**对于每个独特的状态,都应该存在且只存在于一个指定的组件中作为 state **。这一原则也被称为拥有 “可信单一数据源”。它并不意味着所有状态都存在一个地方——对每个状态来说,都需要一个特定的组件来保存这些状态信息。你应该 将状态提升 到公共父级,或 将状态传递 到需要它的子级中,而不是在组件之间复制共享的状态。

你的应用会随着你的操作而变化。当你将状态上下移动时,你依然会想要确定每个状态在哪里“活跃”。这都是过程的一部分!

组件之间的通信

  • 父传子:父组件向子组件传值使用props,React数据流动是单向的,子组件只能使用props中的数据不能修改。

  • 子传父:子组件向子组件传值,需要父组件提前传一个函数给子组件,以便子组件在适当的时候,将子组件中的数据通过调用这个函数,再传递给父组件

  • 兄弟组件之间通信:

    • 状态提升

    • 消息发布订阅

    • 状态管理Redux

父传子

// 父组件
export default function App() {
	const [count, setCount] = useState(0);
	const [zhangsan, setZhangsan] = useState({
    name: '张三',
    age: 18,
  });

	return (
		<div>
			<h1>Hello world!</h1>
			<button onClick={() => setCount(val => val + 1)}>按钮 count = {count} </button>
			<hr />
 			{/* 
 				count={count} 直接传递count属性,在子组件的props中接收
 				{...zhangsan} 传递一个对象的属性,在子组件中,使用 props.name 和 props.age 获取传递的数据
 			*/}
			<MA count={count} zhangsan={zhangsan} {...zhangsan}></MA>
		</div>
	);
}
// 子组件
export default function MA(props) {
	const { count, name, age, zhangsan } = props;
	return (
		<div>
			<h2>MA组件</h2>
			<p> count: {count}</p>
			<p> name: {name}</p>
			<p> age: {age}</p>
			<p>zhangsan: {zhangsan.name}- {zhangsan.age}</p>
		</div>
	);
}

子传父

// 父组件

export default function App() {
	const [count, setCount] = useState(0);

	// countChange是在父组件定义的函数,但是是在子组件中调用的函数
	const countChange = val => {
		console.log('countChange 执行了 val:', val);
		setCount(val);
	};

	return (
		<div>
			<h1>Hello world!</h1>
      <p> count: {count}</p>
			<hr />
			{/* onCountChange={countChange} 把父组件的函数传递给子组件 */}
			<MA onCountChange={countChange}></MA>
		</div>
	);
}
// 子组件
export default function MA({ onCountChange }) {

	const btnClick = () => {
		// 调用父组件传递的函数并传值
		onCountChange(100);
	};
	return (
		<div>
			<h2>MA组件</h2>
			<button onClick={btnClick}>传值到父组件</button>
		</div>
	);
}

兄弟组件

状态提升

  • 通过状态提升,把组件的状态定义在父组件中,父组件作为数据的中转
// 父组件
export default function App() {
	const [count, setCount] = useState(0);

	// countChange是在父组件定义的函数,但是是在子组件中调用的函数
	const countChange = val => {
		console.log('countChange 执行了 val:', val);
		setCount(val);
	};

	return (
		<div>
			<h1>Hello world!</h1>
			<hr />
			<MA count={count} onCountChange={countChange}></MA>
			<MB count={count} onCountChange={countChange}></MB>
		</div>
	);
}
//子组件1
export default function MA({ count, onCountChange }) {

  const btnClick = () => {
    onCountChange(count+ 1);
	};
	return (
		<div>
			<h2>MA组件</h2>
			<p> count: {count}</p>
			<button onClick={btnClick}>传值到父组件</button>
		</div>
	);
}
//子组件2
export default function MB({ count, onCountChange }) {

  const btnClick = () => {
    onCountChange(count + 2);
	};
	return (
		<div>
			<h2>MB组件</h2>
			<p> count: {count}</p>
			<button onClick={btnClick}>传值到父组件</button>
		</div>
	);
}

消息发布订阅

pubsub.js消息发布订阅(推荐使用)

  • 这种发布订阅方式,是目前开发中比较常用的兄弟组件通信方法。
  • 其实pubsub.js不只适用于兄弟组件通信,其实任意层级、任意关系的组件通信,都可以使用pubsub的发布订阅通信,功能很强大。
  • vue中也可以使用这个插件,因为这个插件是用原生js写的
  • 文档:https://www.npmjs.com/package/pubsub-js
第一步:下载pubsub.js
npm install pubsub-js --save
第二步:在组件MA中发布消息
//父组件
export default function App() {
	return (
		<div>
			<MA></MA>
			<MB></MB>
		</div>
	);
}
// 子组件1
import PubSub from 'pubsub-js';

export default function MA() {
	const [num, setNum] = useState(0);

	const btnClick = () => {
		// 如果需要使用 next 状态,可以在将其传递给函数之前将其保存在变量中:
		const nextNum = num + 1;
		setNum(() => nextNum);
		// 发布消息
		// 参数1:消息名
		// 参数2:数据,可以是数字、字符串、对象等类型
		PubSub.publish('send-data', { val: nextNum });
	};

	return (
		<div>
			<h2>MA组件</h2>
			<p> num: {num}</p>
			<button onClick={btnClick}>传值到MB组件</button>
		</div>
	);
}
第三步:在组件MB中订阅消息
// 子组件2

export default function MB() {
	const [num, setNum] = useState(0);

	// 订阅消息
	// 参数1:消息名
	// 参数2:收到消息的回调,
	// msg:是消息名, data:传递的数据
	const token = PubSub.subscribe('send-data', (msg, data) => {
		console.log('msg:', msg, 'data:', data);
		setNum(data.val);
	});

	useEffect(() => {
		return () => {
			console.log('清除函数');
			// 移除订阅
			PubSub.unsubscribe(token);
		};
	}, []);

	return (
		<div>
			<h2>MB组件</h2>
			<p> num: {num}</p>
		</div>
	);
}

自定义 Hook

文档:https://zh-hans.legacy.reactjs.org/docs/hooks-custom.html

import { useState } from 'react';
import MA from './components/MA';

function App() {
	const [list] = useState([
		{ name: '张三', age: 20, id: 2 },
		{ name: '李四', age: 21, id: 6 },
		{ name: '王五', age: 22, id: 8 },
	]);
	return (
		<div className='App'>
			<h1>App</h1>
      {list.map(user => (
				<MA {...user} key={user.id}></MA>
			))}
		</div>
	);
}
  • 自定义hooks
// config/hooks.jsx

import { useState, useEffect } from 'react';

export const useFriendStatus = friendID => {
	const [isOnline, setIsOnline] = useState(null);
	useEffect(() => {
		if (friendID > 5) {
			setIsOnline(true);
		} else {
			setIsOnline(false);
		}
		return () => {
			setIsOnline(null);
		};
	});
	return isOnline;
};
  • 使用hooks
import { useFriendStatus } from '../../config/hooks';

const MA = props => {
	const { name, age, id } = props;
	const isOnline = useFriendStatus(id);

	return (
		<div className='m-a' style={{ background: '#e1e1e1' }}>
			<p style={{ color: isOnline ? 'red' : 'blue' }}> 姓名:{name} </p>
			<p> 年龄:{age} </p>
		</div>
	);
};

示例2:获取窗口宽高变化

实现目标:通过 useWindowSize()来实时获取窗口的宽高

新建一个hook文件useWindowSize.ts,代码如下:

import { useEffect, useState } from "react";

//定义size对象
interface WindowSize {
    width: number,
    height: number
}
const useWindowSize = () => {
    const [size, setSize] = useState<WindowSize>({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
    })

    useEffect(() => {
        const fun = () => {
            setSize({
                width: document.documentElement.clientWidth,
                height: document.documentElement.clientHeight
            })
        }
        window.addEventListener('resize', fun)
        return () => {
            window.removeEventListener('resize', fun)
        }
    },[])
    return size
}

export default useWindowSize

组件中这样使用:

import useWindowSize from './hooks/useWindowSize';

function App() {

  const size = useWindowSize()

  return (
    <div>
      <div>页面宽度:{size.width}</div>
      <div>页面高度:{size.height}</div>
    </div>
  )
}

export default App

在浏览器拖动放大缩小时,页面上的数据可动态变化

示例2:获取滚动偏移量变化

目标:通过 useWindowScroll()来实时获取页面的滚动偏移量

新建一个hook文件useWindowScroll.ts,代码如下:

import { useEffect, useState } from "react"

//定义偏移量对象
interface ScrollOffset {
    x: number,
    y: number
}

const useWindowScroll = () => {
    const [off, setOff] = useState<ScrollOffset>({
        x: window.scrollX, 
        y: window.scrollY
    })
    useEffect(() => {
    
        const fun = () => {
            setOff({
                x: window.scrollX,
                y: window.scrollY
            })
        }
        //监听
        window.addEventListener('scroll', fun)
        return () => {
            //移除监听
            window.removeEventListener('scroll', fun)
        }
    })
    return off
}

export default useWindowScroll

组件中这样使用:

import useWindowScroll from './hooks/useWindowScroll';

function App() {

  const offSet = useWindowScroll()

  return (
    <div style={{height: '10000px', width: '10000px'}}>
      <div>滚动y:{offSet.y}</div>
      <div>滚动x:{offSet.x}</div>
    </div>
  )
}

export default App

示例:自动同步至localStorage

目标:通过 const [value, setValue] = useLocalStorage('key', 'value') 可以传入默认的初始value和key,且每次修改value可以自动同步到localStorage中

新建一个hook类useLocalStorage,代码如下:

import { useEffect, useState } from "react"

const useLocalStorage = (key, defaultValue)  => {
    const [value, setValue] = useState(defaultValue)
    useEffect(() => {
        window.localStorage.setItem(key, value)
    },[key, value])
    return [value, setValue]
}

export default useLocalStorage

组件中使用:

import useLocalStorage from './hooks/useLocalStorage';

function App() {

  const [value, setValue] = useLocalStorage('key', 'react')

  return (
    <div>

    <button onClick={() => {
        //点击修改value,会自动同步至本地
        setValue('vue')
      }}>点击</button>
      <div>{ value }</div>
    </div>
  )
}

export default App

其他Hook函数

正确理解 useMemo、useCallback、memo 的使用场景

  • useMemo是一个 React Hook,可让您在重新渲染之间缓存计算结果。

    const cachedValue = useMemo(calculateValue, dependencies)
    
  • useCallback是一个 React Hook,可让您在重新渲染之间缓存函数定义。

    const cachedFn = useCallback(fn, dependencies)
    

在我们平时的开发中很多情况下我们都在滥用 useMemo、useCallback这两个 hook, 实际上很多情况下我们不需要甚至说是不应该使用,因为这两个 hook 在首次 render 时需要做一些额外工作来提供缓存,减少react里不必要的re-render

同时既然要提供缓存那必然需要额外的内存来进行缓存,综合来看这两个 hook 其实并不利于页面的首次渲染甚至会拖慢首次渲染,这也是我们常说的“不要在一开始就优化你的组件,出现问题的时候再优化也不迟”的根本原因。

那什么时候应该使用呢,无非以下两种情况:

  1. 缓存 useEffect 的引用类型依赖;
  2. 缓存子组件 props 中的引用类型。

缓存 useEffect 的引用类型依赖

import { useEffect } from 'react'
export default () => {
  const msg = {
    info: 'hello world',
  }
  useEffect(() => {
    console.log('msg:', msg.info)
  }, [msg])
}

此时 msg 是一个对象该对象作为了 useEffect 的依赖,这里本意是 msg 变化的时候打印 msg 的信息。但是实际上每次组件在render 的时候 msg 都会被重新创建,msg 的引用在每次 render 时都是不一样的,所以这里 useEffect 在每次render 的时候都会重新执行,和我们预期的不一样,此时 useMemo 就可以派上用场了:

import { useEffect, useMemo } from "react";
const App = () => {
  const msg = useMemo(() => {
    return {
      info: "hello world",
    };
  }, []);
  useEffect(() => {
    console.log("msg:", msg.info);
  }, [msg]);
};

export default App;

同理对于函数作为依赖的情况,我们可以使用 useCallback:

import { useEffect, useCallback } from "react";
const App = (props) => {
  const print = useCallback(() => {
    console.log("msg", props.msg);
  }, [props.msg]);
  useEffect(() => {
    print();
  }, [print]);
};

export default App;

缓存子组件 props 中的引用类型。

做这一步的目的是为了防止组件非必要的重新渲染造成的性能消耗,所以首先要明确组件在什么情况下会重新渲染。

  1. 组件的 props 或 state 变化会导致组件重新渲染
  2. 父组件的重新渲染会导致其子组件的重新渲染

这一步优化的目的是:在父组件中跟子组件没有关系的状态变更导致的重新渲染可以不渲染子组件,造成不必要的浪费。

大部分时候我们是明确知道这个目的的,但是很多时候却并没有达到目的,存在一定的误区:

误区一:

import { useCallback, useState } from "react";

const Child = (props) => {};
const App = () => {
  const handleChange = useCallback(() => {}, []);
  const [count, setCount] = useState(0);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1);
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} />
    </>
  );
};

export default App;

项目中有很多地方存在这样的代码,实际上完全不起作用,因为只要父组件重新渲染,Child 组件也会跟着重新渲染,这里的 useCallback 完全是白给的。

误区二:

import { useCallback, useState, memo } from "react";

const Child = memo((props) => {});
const App = () => {
  const handleChange = () => {};
  const [count, setCount] = useState(0);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1);
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} />
    </>
  );
};

export default App;

对于复杂的组件项目中会使用 memo 进行包裹,目的是为了对组件接受的 props 属性进行浅比较来判断组件要不要进行重新渲染。这当然是正确的做法,但是问题出在 props 属性里面有引用类型的情况,例如数组、函数,如果像上面这个例子中这样书写,handleChange 在 App 组件每次重新渲染的时候都会重新创建生成,引用当然也是不一样的,那么势必会造成 Child 组件重新渲染。所以这种写法也是白给的。

正确姿势:

import { useCallback, useState, memo, useMemo } from "react";

const Child = memo((props) => {});
const App = () => {
  const [count, setCount] = useState(0);
  const handleChange = useCallback(() => {}, []);
  const list = useMemo(() => {
    return [];
  }, []);
  return (
    <>
      <div
        onPress={() => {
          setCount(count + 1);
        }}
      >
        increase
      </div>
      <Child handleChange={handleChange} list={list} />
    </>
  );
};

export default App;


其实总结起来也很简单,memo 是为了防止组件在 props 没有变化时重新渲染,但是如果组件中存在类似于上面例子中的引用类型,还是那个原因每次渲染都会被重新创建,引用会改变,所以我们需要缓存这些值保证引用不变,避免不必要的重复渲染。

useContext 使用注意事项

  • createContext 能让你创建一个 context 以便组件能够提供和读取。

    const SomeContext = createContext(defaultValue)
    
  • useContext 是一个 React Hook,可以让你读取和订阅组件中的 context

    const value = useContext(SomeContext)
    
  • useReducer是一个 React Hook,可让您向组件添加化简器

    const [state, dispatch] = useReducer(reducer, initialArg, init?)
    

在项目中我们已经重度依赖于 useContext 这个 api,同时结合 useReducer 代替 redux 来做状态管理,这也引入了一些问题。我们把官方Demo整合下,先来看看如何结合使用 useContext 和 useReducer。

import React, { createContext, useContext, useReducer } from "react";

const ContainerContext = createContext({ count: 0 });
const initialState = { count: 0 };

function reducer(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 { state, dispatch } = useContext(ContainerContext);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </>
  );
}

function Tip() {
  return <span>计数器</span>;
}

function Container() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <ContainerContext.Provider value={{ state, dispatch }}>
      <Counter />
      <Tip />
    </ContainerContext.Provider>
  );
}

export default Container;

使用起来非常方便,乍一看似乎都挺美好的,但是其实有不少陷阱或者误区在里面。

useContext 的机制是使用这个 hook 的组件在 context 发生变化时都会重新渲染。这样会导致一些问题,我把我遇到过的和能想到的问题总结到下面,如果有补充的可以再讨论。

1. Provider 单独封装

在上面的 demo 中我们应该看到了在 Provider 中有两个组件,Counter 组件在 state 发生变化的时候需要重新渲染这个没什么问题,那 Tip 组件呢,在 Tip 组件里面显然没有用到 Context 实际上是没有必要进行重新渲染的。但是现在这种写法每次state变化都会导致 Provider 中所有的子组件都跟着渲染。有没有什么办法解决呢,实际上也很简单,我们把状态管理单独封装到一个 Provider 组件里面,然后把子组件通过 props.children 的方式传进去

...
function Provider(props) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <ContainerContext.Provider value={{ state, dispatch }}>
      {props.children}
    </ContainerContext.Provider>
  );
}

const App = () => {
  return (
    <Provider>
      <Counter />
      <Tip />
    </Provider>
  );
};
...

这个时候 APP 组件就成为了无状态组件,state 变化的时候 props.children 不会改变,不会被重新渲染,这个时候再看 Tip 组件,状态更新的时候就不会跟着重新渲染了。

那这样是不是就万事大吉呢,对不起没有,还有坑,接着看第二点。

2. 缓存 Provider value

官方文档里面也提到了这个坑,简单说就是,如果 Provider 组件还有父组件,当 Provider 的父组件进行重渲染时,Provider 的value 属性每次渲染都会重新创建,原理和上面 useMemo useCallback 中提到的一样,所以最好的办法是对 value 进行缓存:

...
function Provider(props) {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => ({ state, dispatch }), [state]);
  return (
    <ContainerContext.Provider value={value}>
      {props.children}
    </ContainerContext.Provider>
  );
}
...

3. memo 优化直接被穿透,不再起作用

在开发中我们会使用 memo 来对组件进行优化,如上文中提到的,但是很多时候我们又会在使用 memo 的组件中使用 context,用 context 的地方在context发生变化的时候无论如何都会发生重新渲染,所以很多时候会导致 memo 优化实效,具体可以看这里的讨论,react 官方解释说设计如此,同时也给出了相应的建议,我们项目中主要解决方案是把 context 往上提,然后通过属性传递,就是说我们的组件一开始是这样写的:

React.memo(()=> {
 const {count} = useContext(ContainerContext);
 return <span>{count}</span>
})

这个时候context更新了,memo 属于是白给,我们把 context 往上提一层,其实就可以解决这个问题:

const Child = useMemo((props)=>{
    ....
})
function Parent() {
  const {count} = useContext(ContainerContext);
  return <Child count={count} />;
}

这样保证了 Child 组件的外部状态的变化只会来自于 props,这样当然 memo 可以完美工作了。

4. 对 context 进行拆分整合

context 的使用场景应该是为一组享有公共状态的组件提供便利来获取状态的变化。 但是随着业务代码越来越复杂,在不经意间我们就会把一些不相关的数据放在同一个context 里面。这样就导致了context 中任何数据的变化都会导致使用这个 context 的组件重新 render。这显然不是我们想看到的。这种情况下我们应该要对contex 进行更细粒度的拆分,把真正相关的数据整合在一起,然后再提供给组件,至少这样不相关组件的状态变化不会相互影响,也就不会导致多余的重复渲染。

useRef && forwardRef && useImperativeHandle

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

这个ref对象只有一个current属性,你把一个东西保存在内,它的地址一直不会变。

  • 如果你需要一个值,在组件不断render时保持不变
    • 初始化:const count = useRef(0)
    • 读取:count.current
  • 为什么需要current?
    • 为了保证两次useRef是同一个值(只有引用能做到)
  • 和变更数据有关的hooks
示例1

就是相当于全局作用域,一处被修改,其他地方全更新

import React, { useRef } from "react";
export default function App() {
  const r = useRef(0);
  console.log(r);
  const add = () => {
    r.current += 1;
    console.log(`r.current:${r.current}`);
  };
  return (
    <div className="App">
      <h1>r的current:{r.current}</h1>
      <button onClick={add}>点击+1</button>
    </div>
  );
}

useRef变化不会主动使页面渲染,点击上方的按钮,让current+1,可以看到页面没有主动渲染,但是新的current的值已经变成了1。

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变

如果需要一个值,在组件不断render时保持不变,那就可以使用 useRef

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

我们改变的只是 refContainer.current 这个属性的值。refContainer 在每次渲染时的地址是不变的。useRef 会在每次渲染时返回同一个 ref 对象

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。只能自己手动渲染。比如在变更 .current 之后,再随便setState一个数据,这会使App再次执行。

示例2

普遍操作,用来操作dom

function CustomTextInput(props) {
  // 这里必须声明 textInput,这样 ref 才可以引用它
  const textInput = useRef(null);//创建一个包含current属性的对象

  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input type="text" ref={textInput} />
      <input type="button" value="Focus the text input" onClick={handleClick} />
    </div>
  );
}

我们可以发现只要把ref挂到某个react元素上,就可以拿到它的dom。

一共分两个步骤

  • useRef创建一个ref对象
  • ref={xx}挂到react元素上

然后就可以使用这个元素了。官方例子是取到这个元素并且通过点击按钮让元素聚焦。我们目前大概理解了ref是怎样用的。

forwardRef

forwardRef让您的组件使用 ref 向父组件公开 DOM 节点,返回值是react组件

import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
  // ...
});

何时使用 Ref?下面是几个适合使用 ref 的情况:

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。
function CustomTextInput(props) {
  // 这里必须声明 textInput,这样 ref 才可以引用它
  const textInput = useRef(null);//创建一个包含current属性的对象

  console.log(textInput);
  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input type="text" ref={textInput} />//挂到内部dom上
      <input type="button" value="Focus the text input" onClick={handleClick} />
    </div>
  );
}

子组件上使用ref,上面的方法不能直接在子组件上使用,也许你会这样写

<Child ref={textInput} />

但是这样还拿不到子组件的DOM,我们需要使用forwardRef配合使用。

如果你的函数组件想要接受别人传来的ref参数,就必须把函数组件用 forwardRef 包起来。这样就可以接受ref作为第二个参数。不然就只有props这一个参数。forwardRef 会把别人传给你的ref帮你传进来。

上面的例子可以写成这样

function CustomTextInput(props) {
  // 这里必须声明 textInput,这样 ref 才可以引用它
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }
  return (
    <div>
      <Child ref={textInput} />  //**依然使用ref传递**
      <input type="button" value="Focus the text input" onClick={handleClick} />
    </div>
  );
}
const Child = forwardRef((props, ref) => {  //** 看我 **
  return <input type="text" ref={ref} />;//** 看我挂到对应的dom上 **
});

上面是通过forwardRefChild函数包起来,然后传入第二个参数ref最后挂载ref={ref} 这样就可以拿到对应的DOM了,控制台打印一下看看

current:

拿到子组件的DOM元素了。

useImperativeHandle

什么是 useImperativeHandle

当我们需要操作子组件中的某个 DOM 节点时,forwardRef 能很好的满足我们的需求。但是,如果我们要操作子组件中的某些方法或属性该怎么办呢?

useImperativeHandle 是 React 中的一个钩子函数,它可以暴露一个组件的 ref,从而使得父组件可以调用子组件的某些方法和属性。

useImperativeHandle 钩子函数有着非常广泛的用途,灵活运用这个钩子函数能为我们开发带来极大的便利。比如,我们在子组件中封装了一个播放器,父组件可能需要控制播放器的播放、暂停、停止等操作,这时就可以使用 useImperativeHandle 将这些操作暴露给父组件。

再比如上面通过回调函数暴露子组件中 otherOperate 的示例,我们完全可以使用 useImperativeHandle 来实现,同时父组件还能直接访问子组件的内部状态和属性。

useImperativeHandle 的基本用法
useImperativeHandle(ref, createHandle, [deps]);

useImperativeHandle 接受三个参数:

  • ref:一个 Ref 对象,通常来说,是从父组件传递过来的。
  • createHandle:一个回调函数,该函数返回一个对象,这个对象的属性和方法会被暴露给父组件。
  • [deps]:可选参数,一个数组,用于指定回调函数的依赖项。当这些依赖项发生变化时,回调函数会被重新执行。如果不指定依赖项,则回调函数只会在组件首次渲染时执行一次。

在子组件中使用 useImperativeHandle 钩子函数时,我们需要将 ref 从父组件传递过来,并在回调函数中返回一个对象。这个对象中的属性和方法会被暴露给父组件以供使用。需要注意的是,只有在回调函数中返回的对象属性和方法才会暴露出去,而子组件中的其他属性和方法则不会。

在使用 useImperativeHandle 时,我们还可以通过 [deps] 参数指定回调函数的依赖项,从而避免不必要的重复渲染。当这些依赖项发生变化时,回调函数才会被重新执行。而如果不指定依赖项,则回调函数只会在组件首次渲染时执行一次。

计数器示例

首先,我们编写计数器组件,代码如下:

import React, { forwardRef, useImperativeHandle, useState } from 'react';

// 使用 forwardRef 函数创建一个 Counter 组件,并将 ref 参数传递下去
const Counter = forwardRef((props, ref) => {
  // 使用 useState 创建一个名为 count 的状态,初始值为 0
  const [count, setCount] = useState(0);

  // 创建 increase 函数,用于增加计数器的值
  const increase = () => {
    setCount(count + 1);
  };

  // 创建函数 decrease,用于减少计数器的值
  const decrease = () => {
    setCount(count - 1);
  };

  // 使用useImperativeHandle hook,将ref暴露给父组件,并返回一个对象,对
  // 象中包含了increase和decrease两个方法,使得父组件可以直接调用这两个方法
  // 来修改计数器的值
  useImperativeHandle(ref, () => ({
    increase,
    decrease,
  }));

  // 返回一个包含当前计数器值的div元素
  return <div>{count}</div>;
});

// 导出 Counter 组件
export default Counter;

在上面的代码中,我们使用了 useImperativeHandle 来暴露 increasedecrease 两个方法,使得父组件可以直接调用这两个方法来修改计数器的值。注意,在回调函数中返回的对象属性和方法才会被暴露出来,而其他属性和方法则不会。在这里,我们只暴露了 increasedecrease 两个方法,而 count 状态则没有被暴露出来。

接下来,在父组件中引用这个计数器组件 Counter,并演示如何调用它暴露的方法来操作计数器的值。代码如下:

import React, { useRef } from 'react';
import Counter from './Counter';

const App = () => {
  // 使用 useRef hook 创建一个名为 counterRef 的引用
  const counterRef = useRef();

  // 创建一个名为 handleIncrease 的函数,用于增加计数器的值
  const handleIncrease = () => {
    // 通过 counterRef.current 获取 Counter 组件实例,并调用它暴露的 increase 方法
    counterRef.current.increase();
  };

  // 创建一个名为 handleDecrease 的函数,用于减少计数器的值
  const handleDecrease = () => {
    // 通过 counterRef.current 获取 Counter 组件实例,并调用它暴露的 decrease 方法
    counterRef.current.decrease();
  };

  // 返回一个包含 Counter 组件和两个按钮的 div 元素,
  // 点击按钮会触发子组件暴露出来的 handleIncrease 和 handleDecrease 函数,从而操作计数器的值
  return (
    <div>
      <Counter ref={counterRef} />
      <button onClick={handleIncrease}>Increase</button>
      <button onClick={handleDecrease}>Decrease</button>
    </div>
  );
};

export default App;

在上面的代码中,我们使用 useRef 创建了一个 Ref 对象 counterRef,并将它传递给了 Counter 组件。接着,我们定义了 handleIncreasehandleDecrease 两个函数,函数内部通过 counterRef.current 分别调用计数器组件暴露出来的 increasedecrease 方法。这样,我们就可以通过父组件中的这两个按钮来增加或减少子组件计数器的值了。

怎么样,和用回调函数的方式相比是不是这种方法更加灵活呢。其实用回调函数有许多弊端,如果一个子组件接收好多个回调函数,我么维护起来会非常难受的。而使用 useImperativeHandle 钩子函数就能避免给子组件传入多个回调函数。再者,回调函数只能在触发特定的事件后才能访问到子组件暴露出来的某些方法或属性,而 useImperativeHandle 则可以随时让我们访问到子组件中的方法和属性。因此,总的来说,如果遇到需要在父组件中访问子组件中方法和属性的场景,直接上 useImperativeHandle 肯定没错。

高阶组件(HOC)

什么是高阶组件(HOC)

在 React 中,高阶组件(HOC)是一个接收组件作为参数并返回一个新组件的函数。换句话说,它是一种组件的转换器。高阶组件通常用于在组件之间复用逻辑,例如状态管理、数据获取、访问控制等。

HOC 的一个常见示例是 React-Redux 的 connect 函数,它将 Redux store 连接到 React 组件,使组件可以访问和更新 store 中的状态。

创建和使用高阶组件

让我们通过一个简单的示例来了解如何创建和使用高阶组件。我们将创建一个名为 withLoading 的高阶组件,它将在加载状态下显示一个加载指示器。

创建 withLoading 高阶组件

import React from "react";

function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div>Loading...</div>;
    } else {
      return <WrappedComponent {...props} />;
    }
  };
}

export default withLoading;

在上述代码中,我们定义了一个 withLoading 函数,它接受一个组件作为参数(WrappedComponent),并返回一个新的组件 WithLoadingComponent。新组件接收一个名为 isLoading 的属性,如果 isLoadingtrue,则显示一个加载指示器;否则,渲染 WrappedComponent

使用 withLoading 高阶组件

假设我们有一个名为 DataList 的组件,它接收一个名为 data 的属性,并将其渲染为一个列表。我们希望在获取数据时显示一个加载指示器。为此,我们可以使用 withLoading 高阶组件。

import React from "react";
import withLoading from "./withLoading";

function DataList({ data }) {
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

const DataListWithLoading = withLoading(DataList);

export default DataListWithLoading;

在这个示例中,我们首先导入了 withLoading 高阶组件,然后使用它来包装我们的 DataList 组件。这将创建一个名为 DataListWithLoading 的新组件,它在加载状态下显示一个加载指示器,否则显示数据列表。现在我们可以在其他组件中使用 DataListWithLoading 组件,例如:

import React, { useState, useEffect } from "react";
import DataListWithLoading from "./DataListWithLoading";

function App() {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      const response = await fetch("https://api.example.com/data");
      const data = await response.json();
      setData(data);
      setIsLoading(false);
    };

    fetchData();
  }, []);

  return (
    <div>
      <h1>Data List</h1>
      <DataListWithLoading data={data} isLoading={isLoading} />
    </div>
  );
}

export default App;

在这个 App 组件中,我们使用 useStateuseEffect Hooks 来获取数据,并在数据获取过程中设置 isLoadingtrue。我们将 dataisLoading 作为属性传递给 DataListWithLoading 组件。当数据正在加载时,组件将显示加载指示器;当数据加载完成时,组件将显示数据列表。

这个示例展示了如何使用高阶组件来为现有组件添加额外的功能(在本例中是加载状态)而无需修改现有组件的代码。

高阶组件的应用场景

高阶组件在实际应用中有多种用途:

  1. 复用逻辑:HOC 可以帮助我们在组件之间复用逻辑,避免重复代码。在上面的示例中,我们可以将加载状态的逻辑复用在多个组件中,而无需在每个组件中单独实现。
  2. 修改 props:HOC 可以用来修改传递给组件的 props,从而改变组件的行为。例如,我们可以使用 HOC 来根据权限级别显示或隐藏组件的某些部分。
  3. 条件渲染:HOC 可以用来根据特定条件决定是否渲染组件。例如,在上面的示例中,我们根据 isLoading 属性的值来决定是渲染加载指示器还是渲染 WrappedComponent
  4. 提供额外的功能:HOC 可以用来为组件提供额外的功能,例如错误处理、性能监控或者数据获取。

高阶组件示例

接下来,我们将介绍一些更高级的高阶组件示例,以展示其在实际项目中的应用。

示例 1:权限控制

假设我们有一个应用程序,需要根据用户权限来显示或隐藏某些组件。我们可以创建一个名为 withAuthorization 的高阶组件来实现此功能。

import React from "react";

function withAuthorization(WrappedComponent, requiredPermission) {
  return function WithAuthorizationComponent({ userPermission, ...props }) {
    if (userPermission >= requiredPermission) {
      return <WrappedComponent {...props} />;
    } else {
      return <div>您没有查看此内容的权限。</div>;
    }
  };
}

export default withAuthorization;

在这个高阶组件中,我们接受一个 WrappedComponent 和一个 requiredPermission 作为参数。然后,我们返回一个新的组件,该组件检查 userPermission 是否大于等于 requiredPermission。如果满足条件,则渲染 WrappedComponent;否则,显示一条权限不足的消息。

我们可以使用 withAuthorization 高阶组件来包装需要进行权限控制的组件,例如:

import React from "react";
import withAuthorization from "./withAuthorization";

function AdminDashboard({ data }) {
  // ... 管理面板
}

const AdminDashboardWithAuthorization = withAuthorization(AdminDashboard, 3);

export default AdminDashboardWithAuthorization;

在这个示例中,我们将 AdminDashboard 组件与 withAuthorization 高阶组件结合使用,要求用户具有 3 级权限才能查看此组件。

示例 2:错误边界

在 React 中,错误边界是一种用于捕获子组件树中发生的错误并显示友好错误信息的技术。我们可以使用高阶组件来实现一个通用的错误边界组件。

import React, { Component } from "react";

function withErrorBoundary(WrappedComponent) {
  return class WithErrorBoundaryComponent extends Component {
    constructor(props) {
      super(props);
      this.state = { hasError: false };
    }

    static getDerivedStateFromError() {
      return { hasError: true };
    }

    componentDidCatch(error, info) {
      // 处理错误记录
      console.error("Error:", error, "Info:", info);
    }

    render() {
      if (this.state.hasError) {
        return <div>Something went wrong. Please try again later.</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  };
}

export default withErrorBoundary;

在这个高阶组件中,我们返回一个类组件,因为错误边界需要使用生命周期方法 componentDidCatch 和静态方法 getDerivedStateFromError。我们在组件的状态中记录是否发生了错误,并在渲染方法中根据 hasError 的值来决定是显示错误消息还是渲染 WrappedComponent

我们可以使用 withErrorBoundary 高阶组件来包装任何需要捕获错误的组件,例如:

import React from "react";
import withErrorBoundary from "./withErrorBoundary";

function UserProfile({ user }) {
  // ... 用户配置文件
}

const UserProfileWithErrorBoundary = withErrorBoundary(UserProfile);

export default UserProfileWithErrorBoundary;

在这个示例中,我们将 UserProfile 组件与 withErrorBoundary 高阶组件结合使用。当 UserProfile 组件或其子组件发生错误时,用户将看到一个友好的错误消息。

示例 3:性能监控

假设我们想要跟踪某些组件的性能指标,例如渲染时间。我们可以使用高阶组件来实现这个功能。

import React, { useEffect, useRef } from "react";

function withPerformance(WrappedComponent) {
  return function WithPerformanceComponent(props) {
    const startTime = useRef(Date.now());

    useEffect(() => {
      const endTime = Date.now();
      const renderTime = endTime - startTime.current;
      console.log(`${WrappedComponent.name} render time: ${renderTime} ms`);
    }, []);

    return <WrappedComponent {...props} />;
  };
}

export default withPerformance;

在这个高阶组件中,我们使用 useRefuseEffect Hooks 来计算 WrappedComponent 的渲染时间。当组件被渲染时,我们记录开始时间,然后在 useEffect 中计算渲染所花费的时间,并将结果打印到控制台。

我们可以使用 withPerformance 高阶组件来包装需要监控性能的组件,例如:

import React from "react";
import withPerformance from "./withPerformance";

function ExpensiveComponent({ data }) {
  // ... 渲染组件
}

const ExpensiveComponentWithPerformance = withPerformance(ExpensiveComponent);

export default ExpensiveComponentWithPerformance;

在这个示例中,我们将 ExpensiveComponent 组件与 withPerformance 高阶组件结合使用。每当 ExpensiveComponent 组件被渲染时,我们将在控制台中看到渲染时间。

结论

高阶组件是 React 中一种强大的模式,可以帮助我们在组件间复用逻辑、修改 props、实现条件渲染以及提供额外的功能。通过熟练掌握高阶组件的概念和使用方法,我们可以提高代码的可维护性和可读性,构建更加健壮、高效的应用程序。在实际项目中,我们可能会遇到各种高阶组件的应用场景,因此掌握高阶组件的使用方法对于 React 开发者来说至关重要。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萧寂173

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值