WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

React Hooks 深度探索 提升您的 React 技能 React Hooks Deep Dive Elevate Your React Skills #203

Open WangShuXian6 opened 1 day ago

WangShuXian6 commented 1 day ago

React Hooks 深度探索 提升您的 React 技能 React Hooks Deep Dive Elevate Your React Skills

1 - Introduction 1 - Understanding modern React 2 - What You Should Know Before Watching This Course

2 - Intro to React Hooks 1 - React Hooks History 2 - Installing Create React App 3 - Reviewing the project

3 - The useState Hook 1 - Reviewing Array and Object destructuring 2 - Incorporating the useState Hook 3 - Building A Checkbox With Usestate 4 - Working with component trees 5 - Sending interactions up component trees

4 - The useEffect Hook 1 - Introducing useEffect 2 - Working with the dependency array 3 - Fetching Data With Useeffect

5 - Additional Hooks 1 - Handling Complex State With Usereducer 2 - Refactoring Usestate To Usereducer 3 - Dispatching Actions With Usereducer 4 - Managing Form Inputs With Useref 5 - Creating controlled components with useState

6 - Creating Custom Hooks 1 - Reusing form logic with custom Hooks 2 - Placing Data In Context 3 - Retrieving Data With Usecontext 4 - Creating A Custom Hook With Context 5 - Data fetching with a Fetch Hook 6 - Building a fetch component

7 - Conclusion 1 - Next Steps


1 - 介绍 1 - 理解现代 React 2 - 观看本课程前你需要了解的内容

2 - React Hooks 介绍 1 - React Hooks 的历史 2 - 安装 Create React App 3 - 项目回顾

3 - useState Hook 1 - 回顾数组和对象解构 2 - 引入 useState Hook 3 - 用 useState 构建复选框 4 - 处理组件树 5 - 将交互向上传递到组件树

4 - useEffect Hook 1 - 介绍 useEffect 2 - 使用依赖数组 3 - 使用 useEffect 获取数据

5 - 其他 Hooks 1 - 用 useReducer 处理复杂状态 2 - 将 useState 重构为 useReducer 3 - 使用 useReducer 派发操作 4 - 使用 useRef 管理表单输入 5 - 用 useState 创建受控组件

6 - 创建自定义 Hooks 1 - 用自定义 Hooks 复用表单逻辑 2 - 将数据放入 Context 3 - 使用 useContext 获取数据 4 - 使用 Context 创建自定义 Hook 5 - 用 Fetch Hook 获取数据 6 - 构建一个获取数据的组件

7 - 总结 1 - 下一步


WangShuXian6 commented 1 day ago

1 - 介绍

1 - 理解现代 React

如果你关注过 React 库的发展,你可能听说过 React Hooks。

Hooks 是一些函数,我们可以用它们来处理状态、获取数据以及与浏览器 API 进行交互。

Hooks 还为我们提供了一种编写可以跨文件和项目共享的函数的方法,便于代码复用。

在过去的六年里,我几乎每天都在使用 React。

我非常喜欢使用 Hooks,因为它们功能强大、灵活,并且可以扩展到各种项目中。

由于像 Twitter、Netflix、Airbnb 和 PayPal 这样的巨型公司都在使用 React,

对 React 的深入理解可以带来更多的职业机会。

在本课程中,我们将探讨为什么要使用 Hooks 以及它们可以帮助你解决哪些问题。

这不仅仅是追随潮流,而是让我们的开发工作更加有趣。

所以,让我们来探索一些使用 Hooks 现代化我们的 React 代码的方法吧。

2 - 观看本课程前你需要了解的内容

在开始之前,这里有一些关于课程的注意事项。

React 是一个 JavaScript 库,因此在开始这门课程之前,最好具备一定的 JavaScript 技能。

如果你想提升 JavaScript 技能,我推荐 Morten Rand Hendrickson 的《JavaScript 基础训练》。

此外,这是一门中级 React 课程,因此你最好熟悉创建组件、处理 props 和使用 NPM 安装包。

如果你需要复习 React 的基础知识,可以参加我的课程《Learning React.js》来了解基本概念。

你不需要太多准备就可以开始这门课程。我们只需要一个代码编辑器。

我将使用 Visual Studio Code,但我也会向你展示如何使用 Code Sandbox,这只需要一个浏览器。

这就是我们所需的一切。让我们开始吧!

WangShuXian6 commented 1 day ago

2 - React Hooks 介绍

1 - React Hooks 的历史

[MUSIC]

如果你熟悉 React 的历史,你可能知道在不同的时期,创建组件的方法有所不同。

如果我们乘坐时光机回到 2013 到 2017 年之间(并不是很久以前),你会发现当时我们是使用一个叫做 createClass 的函数来创建组件的。

然后,createClass 函数中有一个 render 方法,我们可以用它来返回一些 UI。

到了 2017 年左右,createClass 开始不那么受欢迎了,因为 JavaScript 实现了类语法,这非常适合用于创建组件。

这意味着我们的组件开始变成了这个样子:

我们有了一个类,它会继承一个叫做 React.Component 的基类,整体上看起来与之前差不多。

不过在那个时候,使用函数来创建组件也是可能的。

但每次你添加状态变量时,你都必须将函数组件重构为类组件,这非常耗时。

然后 React Hooks 出现了。

Hooks 允许你在函数组件中添加状态,也允许你将逻辑抽象为独立的函数。它们其实就是一些函数。

每当你使用 React 或其他库中的 Hook 时,函数名都必须以 use 开头。

useStateuseReduceruseQueryuseContext 等等,所有 Hook 都是以 use 开头的。

往前看,最好使用函数而不是类,因为类语法在未来的某个时候可能会从 React 库中移除。

在下一个视频中,我们将生成一个 React 项目,以便我们可以实际测试这些 Hooks。

2 - 安装 Create React App

Create React App 是创建新 React 应用程序最常用的工具之一。

Create React App 为我们提供了一个 React 项目结构,支持最新的 JavaScript 特性,并提供优化应用的工具,方便生产环境使用。

开始使用 Create React App

要开始使用 Create React App,你需要做几件事:

1. 打开终端

输入 node -v,查看当前的 Node 版本。只要你的版本高于 8.10,你就可以使用 Create React App。

2. 检查 npm 版本

接下来检查 npm。npm 是 Node 的包管理器,我们要确保它的版本是 56 或更高版本。

如果你的版本不符合要求,你可以前往 nodejs.org 下载最新版本。

3. 切换到项目文件夹

确认有正确的版本后,进入一个用来运行项目的文件夹。使用 cd(改变目录)切换到桌面,然后运行 Create React App,使用 npx 命令。

4. 运行 npx 创建项目

npx 是 npm 附带的一个包执行工具,它允许我们在不需要全局安装 Create React App 的情况下运行它。这非常方便。

我们将使用以下命令:

npx create-react-app React-Hooks

这个安装过程可能需要一点时间。

5. 启动项目

安装完成后,切换到 React Hooks 目录,然后运行以下命令:

npm start

npm start 会在 localhost:3000 启动我们的项目,这将自动在浏览器中打开并显示启动项目。

6. 处理可能出现的问题

如果过程中出现问题,或者安装未成功,屏幕看起来与预期不同,或者你遇到了一堆错误,你可以打开一个新窗口,输入 react.new

react.new 会打开 Code Sandbox,这是一个基于浏览器的 IDE,你可以用它进行 React 开发。如果你更愿意在这样的环境下学习,你可以选择在这里进行操作。

你可以使用 GitHub 或 Google 账号登录。我会使用 GitHub 登录。

登录后,输入任何更改,比如 "Hello world",你将立即在输出中看到变化。这是快速开始 React 项目的一种非常方便的方式。

无论你是在浏览器中的 Code Sandbox 工作,还是在本地项目中工作,你现在已经准备好了。

下一步

在接下来的课程中,我们将学习如何创建一个组件。

3 - 项目回顾

在上一节视频中,我们学习了如何使用 Create React App 来生成一个 React 项目。

现在让我们稍微了解一下这个项目的结构。

package.json 文件

package.json 文件中,我们可以看到包含了 React 以及一些测试依赖。

index.js 文件

index.js 文件中,有一个 service worker。不过我们不需要它,所以我会将其删除,并移除所有与 service worker 相关的代码。

React Strict Mode

我还想指出一个最近版本的 React 变化,即 React 的严格模式(StrictMode)。这是一个可以包裹组件的包装器。

使用 StrictMode 包裹组件的好处是它会给出一些警告,让你在错误发生之前修复潜在的 bug。

如果你违反了任何 React 规则,StrictMode 会让你知道。

app.js 文件

接下来,让我们打开 app.js 文件。这是我们的应用组件,也是自动生成的主要组件。

我们先删除其中的所有内容。

然后添加一个 H1 标题,写上 "Hello world"。

<h1>Hello world</h1>

当我们完成这个操作后,它应该会立即渲染到浏览器中。

这就是我们所需的所有设置。

再次强调,我们的应用组件通过 ReactDOM.render 渲染到页面上。

所以现在我们已经具备了开始开发所需的一切。

WangShuXian6 commented 1 day ago

3 - useState Hook

1 - 回顾数组和对象解构

在 React 中,使用对象解构最常见的地方是在组件内部。

使用 Props

这里我们有一个渲染 "Hello world" 的组件,但我们也可以传递一个名为 props 的对象到任意组件中。

这意味着我们可以将 "world" 替换为 props.name

然后我们回到 index.js 文件中,传递一个 name 属性,例如 "Alex"。

<App name="Alex" />

这样,组件将使用动态数据进行渲染。

简化语法 - 对 props 解构

我们可以通过对 props 对象进行解构来简化代码。因为 props 只是一个对象,我们可以使用解构来直接提取出我们需要的 name

const { name } = props;

保存后,尽管界面没有任何变化,但一切都按预期工作。

数组解构

解构不仅仅可以用于对象,还可以用于数组,尤其是在 React Hooks 中大量使用。

示例:数组解构

index.js 文件中,我们创建一个包含一些人的数组:

const people = ['Alex', 'Allie', 'Anna'];

通常,如果我们想访问数组的第一个元素,我们会使用它的索引:

console.log(people[0]); // Alex

但通过数组解构,我们可以直接为数组中的每个元素指定变量名称。

const [first, second] = people;
console.log(first); // Alex
console.log(second); // Allie

如果你只想获取数组中的第三个元素,你可以跳过前两个元素:

const [, , third] = people;
console.log(third); // Anna

这种语法让我们可以为数组中的特定位置分配变量名称,非常简洁。

结论

无论是对象还是数组解构,理解这两者将帮助你更高效地与现代 React 一起工作。

2 - 引入 useState Hook

移除数组并简化代码

现在我们已经更好地了解了数组解构,让我们先移除这个数组。

我们还会在应用中移除 name 属性。

为了让一切显示在同一个屏幕上,我不会从 app.js 文件导入应用组件,而是直接在 index.js 中创建它。

使用 React Hooks 添加状态

在本视频中,我们将为应用组件添加状态。使用 React Hooks 来实现这一点需要调用一个函数。

首先,我们将返回一个 div,并在其中添加一些内容,例如:

return <div>The package is</div>;

使用 useState Hook

接下来,我们在第 6 行创建一个常量,并调用 useState 函数:

const [status, setStatus] = useState('not delivered');

为了使用 useState,我们还需要从 React 库中导入它:

import { useState } from 'react';

useState 返回一个数组,其中的第一个元素是当前的状态值,第二个元素是一个函数,用于更新状态。

我们可以通过控制台输出结果来验证它的返回值:

console.log(status);

显示初始状态

现在,我们已经定义了一个初始状态 "not delivered"。在 div 中显示该状态:

<div>The package is {status}</div>

更新状态

除了显示状态,我们还需要一种方法来更改状态。我们将创建一个按钮,并在点击时更新状态:

<button onClick={() => setStatus('delivered')}>Deliver</button>

这样,每次点击按钮时,状态都会从初始状态 "not delivered" 更新为 "delivered"。

测试状态更新

让我们点击按钮,查看状态是否变为 "delivered"。

这是 useState 工作原理的最简单示例。每次调用 useState 时,它会返回一个包含两个元素的数组:

  1. 当前的状态值
  2. 用于更新状态的函数

通常,函数命名为 set 加上状态变量的名称,例如 setStatus。虽然这不是强制要求,但这是常见的命名模式。

3 - 用 useState 构建复选框

使用 useState 结合输入元素

另一种利用 useState 的方法是将其与输入元素结合使用。在这个例子中,我们将复选框的状态(选中或未选中)作为一个状态变量来管理。

首先,我们需要移除之前的代码,并清空之前的 useState 调用。

创建复选框和状态变量

div 中添加一个输入元素,类型为 checkbox

<input type="checkbox" />

接着,创建一个状态变量 checked 和一个对应的状态更新函数 setChecked

const [checked, setChecked] = useState(false);

初始状态设置为 false,表示复选框一开始是未选中的。

绑定状态变量到输入框

checked 变量绑定到输入框的值上:

<input type="checkbox" checked={checked} />

然后在旁边添加一个段落标签,用来显示复选框的状态:

<p>{checked ? "Checked" : "Not checked"}</p>

处理复选框的状态变化

到目前为止,我们可以看到复选框的初始状态正确显示,但点击事件还没有生效。我们需要添加一个 onChange 事件处理函数来更新复选框的状态。

<input 
  type="checkbox" 
  checked={checked} 
  onChange={() => setChecked(!checked)} 
/>

这里我们使用 setChecked(!checked) 来设置复选框的状态为 checked 的相反值,即点击时切换状态。

测试组件

现在,复选框可以正确地切换状态,每次点击时都会在 "Checked" 和 "Not checked" 之间切换。

通过这个简单的例子,我们创建了一个复选框组件,并使用 useState 钩子来帮助我们跟踪复选框的状态。

4 - 处理组件树

多组件状态管理

在大多数情况下,React 应用程序不是由单一组件组成的。理解如何在组件树中管理状态是一个非常重要的概念。

构建星级评分组件

在本节课程中,我们将构建一个星级评分组件,可以用于对书籍、电影、餐馆等进行评分。这种组件可以在不同类型的应用程序中复用。

渲染 StarRating 组件

首先,App 组件只负责渲染另一个组件,我们将其命名为 StarRating

return <StarRating />;

使用 Star 子组件

StarRating 组件将使用一个更小的组件,名为 Star,这个组件将构成所有的星级评分。

我们将从一个名为 React Icons 的独立库中导入星形图标。

npm install react-icons

使用 React Icons

现在我们导入 FAStar(Font Awesome 的星形图标)作为星级评分的星形组件。

import { FaStar } from "react-icons/fa";

创建星级数组

为了在页面上显示多个星星,我们需要创建一个辅助函数 createArray,它根据给定长度返回一个数组。

const createArray = (length) => [...Array(length)];

渲染星星

接下来,我们定义 StarRating 组件,并使用 createArray 函数来渲染星星。我们将使用 map 来遍历数组并渲染每个星星。

const StarRating = ({ totalStars = 5 }) => {
  return (
    <>
      {createArray(totalStars).map((n, i) => (
        <Star key={i} />
      ))}
    </>
  );
};

设置星星的总数

App 组件中,我们可以传递一个 totalStars 属性来设置显示星星的数量。

<StarRating totalStars={10} />

如果没有传递 totalStars,默认将显示 5 颗星。

添加颜色属性

接下来,我们为 Star 组件添加颜色属性。未来用户将能够点击星星并更改它们的颜色。

const Star = ({ selected = false }) => (
  <FaStar color={selected ? "red" : "gray"} />
);

目前,所有星星都是灰色的。接下来,我们将实现点击时更改星星颜色的功能。

预告:使用 useState 进行状态管理

在下一节课程中,我们将使用 useState 来实现点击星星时切换颜色的功能。

5 - 将交互向上传递到组件树

管理星级评分组件的点击事件

我们已经使用 React 的 props 创建了几个星星组件。接下来,我们需要处理点击星星的事件。为此,我们将使用 useState 来管理组件的状态。

创建状态变量

我们首先需要在 StarRating 组件中创建一个状态变量,用于跟踪已选中的星星数:

const [selectedStars, setSelectedStars] = useState(0);

我们初始化 selectedStars0,表示刚开始没有任何星星被选中。

为星星组件传递状态

每个星星都是一个可点击的组件,因此我们需要为每个星星传递 selected 属性,使其能够访问状态变量。我们将根据星星的索引值来判断该星星是否被选中:

<Star
  selected={selectedStars > i}
  onSelect={() => setSelectedStars(i + 1)}
  key={i}
/>

selected 属性将根据索引 i 来决定当前星星是否被选中,onSelect 用来更新状态,设置 selectedStars 为点击星星的索引加 1。

点击事件处理

onSelect 函数通过传递的 setSelectedStars 方法来更新组件状态,并将点击的星星数更新为 i + 1,确保正确捕获当前点击的星星:

const setStarRating = (index) => {
  setSelectedStars(index + 1);
};

添加状态显示

为了直观地显示当前选中的星星数量,我们可以在页面上显示一条信息,如 "已选星数/总星数"。我们通过添加一个段落标签来实现:

<p>
  {selectedStars} of {totalStars} stars
</p>

最终效果

每次点击星星时,应用状态会被更新,选中的星星数量将动态变化,显示效果如 "0/5", "3/5", "5/5" 等等。

总结

我们使用 useState 管理星星的选中状态,并通过点击事件动态更新状态。每个星星组件都接收当前状态,并通过 onSelect 方法与 setSelectedStars 函数互动,更新界面上的选中状态。这样,我们就实现了一个完整的星级评分功能。

WangShuXian6 commented 1 day ago

4 - useEffect Hook

1 - 介绍 useEffect

使用 useEffect 处理副作用

React 库提供了另一个钩子 useEffect,它允许你在函数组件中处理副作用。

副作用可以是类似 console.log 的输出,或与某种 DOM API 交互,比如 historywindow

调整组件

首先,我们将清理代码,移除星级评分组件,并用一个简单的 section 替代:

<section>
  <p>Congratulations</p>
</section>

设置状态管理

为了使用 useEffect,我们首先需要设置一个状态变量。我们将定义一个 name 变量,以及用于更新状态的 setName 函数,并设置初始状态为 "Jan":

const [name, setName] = useState("Jan");

然后在 section 内显示该状态:

<section>
  <p>Congratulations {name}</p>
</section>

添加按钮和点击事件

接着,我们添加一个按钮,用来改变赢家的名字:

<button onClick={() => setName("Will")}>Change Winner</button>

每次点击按钮时,名字将从 "Jan" 变为 "Will"。

使用 useEffect

useEffect 允许我们在组件渲染后或状态更新后执行副作用。接下来,我们将使用 useEffect 来改变页面的标题。

useEffect(() => {
  document.title = `Celebrate ${name}`;
}, [name]);

这个 useEffect 函数会在每次 name 状态改变时运行,并更新文档标题。例如,当页面加载时,标题会显示为 "Celebrate Jan",点击按钮后,标题会变为 "Celebrate Will"。

总结

在本例中,我们使用了 useState 来管理名字状态,使用 useEffect 来处理副作用,即每次状态改变时更新页面标题。通过这种方式,我们可以轻松地在 React 中处理副作用。

在接下来的课程中,我们将学习如何自定义 useEffect 的触发时机。

2 - 使用依赖数组

控制 useEffect 的执行时机

我们在上一个例子中看到,每次状态变化时,useEffect 都会被调用。然而,有时我们不希望它在每次渲染或状态更新后都运行,这时我们可以通过依赖项数组来控制 useEffect 的执行时机。

传递空数组作为依赖项

当我们给 useEffect 传递一个空数组作为第二个参数时,useEffect 只会在组件首次渲染时运行一次,而不会在后续的状态更新时再次触发。

useEffect(() => {
  console.log("Effect called");
}, []);

在这个例子中,useEffect 只会在组件首次加载时调用。

依赖状态变量

我们还可以将特定的状态变量传递给 useEffect,让它在这些状态变量改变时触发。例如,如果我们只希望 useEffectname 变量变化时运行,我们可以这样写:

useEffect(() => {
  document.title = `Celebrate ${name}`;
}, [name]);

name 状态变化时,文档标题将更新为 "Celebrate {name}",其他状态变化不会触发此 useEffect

多个状态变量和 useEffect

现在我们添加一个新状态 admin,并根据 admin 状态显示用户是否为管理员:

const [admin, setAdmin] = useState(false);

<p>{admin ? "Logged in" : "Not logged in"}</p>
<button onClick={() => setAdmin(true)}>Login</button>

我们可以使用另一个 useEffect 来监控 admin 状态的变化,并在状态变化时输出日志信息:

useEffect(() => {
  console.log(`The user is ${admin ? "admin" : "not admin"}`);
}, [admin]);

当点击 "Login" 按钮时,admin 状态会改变,触发 useEffect,输出 "The user is admin"。

控制多个 useEffect 调用

通过依赖项数组,我们可以确保每个 useEffect 只在对应的状态变量变化时触发。例如:

useEffect(() => {
  console.log(`Celebrating ${name}`);
}, [name]);

useEffect(() => {
  console.log(`The user is ${admin ? "admin" : "not admin"}`);
}, [admin]);

在此例中,name 变化时只触发第一个 useEffect,而 admin 变化时只触发第二个 useEffect

依赖项数组的作用

使用依赖项数组可以帮助我们控制副作用的执行时机,避免不必要的代码运行。这使我们的应用更加高效和可控。

总结来说,通过使用依赖项数组,我们可以精确控制 useEffect 的执行时机,确保在状态变化时,只执行需要的副作用。

3 - 使用 useEffect 获取数据

使用 useEffect 进行数据获取

useEffect 另一个常见的用例是用于数据获取。假设我们有一个 API,比如 api.github.com/users,这是一个 REST API,返回一些 JSON 数据。我们可以使用 useEffect 结合 useState 来从 API 获取数据,并将其加载到页面中。

初始化组件和状态

首先,我们从头开始构建组件,并导入 useStateuseEffect

import { useState, useEffect } from "react";

接着,在 App 组件中创建一个 data 状态变量和更新它的 setData 函数,初始状态为一个空数组:

const [data, setData] = useState([]);

然后,我们可以在渲染时返回一段简单的文本,表示当前没有用户数据:

return <p>No users</p>;

使用 useEffect 获取数据

我们通过 useEffect 来在组件首次加载时从 API 获取数据:

useEffect(() => {
  fetch("https://api.github.com/users")
    .then((response) => response.json())
    .then(setData);
}, []);

在这里,fetch 函数获取 API 数据,then 方法将返回的响应解析为 JSON,并将其传递给 setData 来更新组件的状态。

显示用户数据

如果 data 中存在用户数据,我们希望将其显示为一个无序列表:

return (
  <ul>
    {data.map((user) => (
      <li key={user.id}>{user.login}</li>
    ))}
  </ul>
);

我们使用 map 方法遍历 data 中的每个用户,并根据 user.id 作为 key,显示用户的登录名 user.login

添加依赖项数组

为了防止 useEffect 在每次渲染时都发送请求,我们需要在 useEffect 中添加一个依赖项数组,确保该副作用只在组件首次加载时运行:

useEffect(() => {
  fetch("https://api.github.com/users")
    .then((response) => response.json())
    .then(setData);
}, []);  // 空数组确保仅在首次渲染时调用

添加移除数据按钮

接着,我们添加一个按钮,用来清空用户数据:

<button onClick={() => setData([])}>Remove Data</button>

点击按钮时,setData([]) 将状态重置为空数组,清空页面上的用户数据。

未处理依赖项数组的问题

如果我们移除依赖项数组,useEffect 会在每次状态更新时不断发送 API 请求,导致页面闪烁并不断重新加载数据。这就是为什么依赖项数组非常重要,它可以防止不必要的重复请求:

useEffect(() => {
  fetch("https://api.github.com/users")
    .then((response) => response.json())
    .then(setData);
}, []);  // 确保不会重复请求

总结

在本例中,我们使用 useEffect 进行数据获取,并通过依赖项数组控制副作用的调用次数,避免重复的 API 请求。每次需要从外部获取数据时,记得使用依赖项数组,以确保不会发起多余的 HTTP 请求。

WangShuXian6 commented 1 day ago

5 - 其他 Hooks

1 - 用 useReducer 处理复杂状态

使用 useReducer 管理状态

除了 useState,还有另一种管理状态变量的方法,那就是使用 React 中的另一个钩子 useReducer

初始化 useReducer

首先,我们导入 useReducer 并清理现有的代码,确保组件返回一个简单的 h1 元素:

import { useReducer } from 'react';

return <h1>{number}</h1>;

接下来,我们使用 useReducer 来管理数字状态。

使用 useReducer 定义状态

useState 不同,useReducer 需要两个参数:

  1. 一个函数,这个函数定义了如何更新状态。
  2. 初始状态,例如这里我们将初始状态设置为 0

定义状态和更新状态的函数:

const [number, setNumber] = useReducer((number, newNumber) => number + newNumber, 0);

在这个例子中,第一个参数是一个接收当前状态 number 和更新值 newNumber 的函数。每次调用 setNumber 时,都会将当前数字和新数字相加,并返回新的状态。

触发状态更新

为了让状态更新生效,我们可以在 h1 元素上添加一个点击事件处理函数。当用户点击 h1 时,调用 setNumber 函数,传递一个更新值:

<h1 onClick={() => setNumber(1)}>{number}</h1>

每次点击 h1 时,number 将增加 1。

总结

useReducer 的主要结构包括:

  1. 第一个参数 是一个处理状态更新的函数。
  2. 第二个参数 是初始状态。

每次调用 setNumber,都会根据传入的新值更新状态。接下来的课程中,我们将深入理解 useReducer,并通过将其他组件重构为使用 useReducer 来进一步探索它的工作原理。

2 - 将 useState 重构为 useReducer

使用 useReducer 重构复选框状态管理

在课程前面,我们创建了一个复选框,并通过 useState 来管理其选中状态。每次点击复选框时,我们调用 setChecked 函数来更新状态,并返回与当前状态相反的值。

现在,我们可以使用 useReducer 来重构这个功能,以减少潜在的 bug,特别是在与团队合作时。

使用 useReducer 替换 useState

首先,我们从 useState 改为导入 useReducer,并在代码中替换 useState 的使用:

import { useReducer } from 'react';

定义 useReducer 函数

我们将定义一个 toggle 函数来处理复选框状态切换。useReducer 需要两个参数:

  1. Reducer 函数:处理状态变化的逻辑。
  2. 初始状态:这里我们将初始状态设置为 false,表示复选框初始为未选中状态。

定义 reducer 函数和初始状态:

const [checked, toggle] = useReducer((checked) => !checked, false);

在这个例子中,reducer 函数接收当前状态 checked,并返回其相反值,实现复选框的选中状态切换。

绑定 toggle 到复选框的 onChange 事件

接着,我们将 onChange 事件的处理函数替换为 toggle,使每次点击复选框时调用 toggle 函数更新状态:

<input type="checkbox" checked={checked} onChange={toggle} />

优化和总结

通过将状态管理逻辑抽象到 useReducer 中,我们不再需要在每个 onChange 事件中嵌入复杂逻辑。每次调用 toggle 都会执行状态切换,代码变得更加简洁和可维护。

随着状态变得更加复杂,使用 useReducer 可以帮助我们集中管理功能逻辑,使代码更易于理解和维护。

3 - 使用 useReducer 派发操作

使用 useReducerdispatch 模式

在使用 useReducer 时,我们可以定义一组操作(actions),并在组件中通过 dispatch 进行分发。这使得状态管理更加灵活和可维护。

初始化组件

首先,我们创建一个组件,显示消息的状态值。接下来,使用 useReducer 来管理这个消息状态。

import { useReducer } from "react";

const initialState = { message: "Hi" };

function reducer(state, action) {
  switch (action.type) {
    case "yell":
      return { message: "HEY!" };
    case "whisper":
      return { message: "Excuse me..." };
    default:
      return state;
  }
}

export default function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>{state.message}</p>
      <button onClick={() => dispatch({ type: "yell" })}>Yell</button>
      <button onClick={() => dispatch({ type: "whisper" })}>Whisper</button>
    </div>
  );
}

useReducer 函数

useReducer 接收两个参数:

  1. Reducer 函数:处理状态更新逻辑,接收当前状态和一个操作对象(action),根据操作的 type 返回新的状态。
  2. 初始状态:在这里初始状态是 message: "Hi"
const initialState = { message: "Hi" };

function reducer(state, action) {
  switch (action.type) {
    case "yell":
      return { message: "HEY!" };
    case "whisper":
      return { message: "Excuse me..." };
    default:
      return state;
  }
}

通过 dispatch 触发操作

我们通过按钮来触发不同的操作(yellwhisper)。当用户点击按钮时,dispatch 会发送相应的操作类型:

<button onClick={() => dispatch({ type: "yell" })}>Yell</button>
<button onClick={() => dispatch({ type: "whisper" })}>Whisper</button>

参考先前状态的更新

有时候新状态依赖于之前的状态,我们可以在 reducer 函数中使用模板字符串,参考之前的 message 状态值:

case "yell":
  return { message: `HEY! I just said ${state.message}` };
case "whisper":
  return { message: `Excuse me... I just said ${state.message}` };

这样,每次更新状态时都会显示上一个状态的信息。

总结

useReducer 函数适用于需要管理复杂状态的场景,它通过 dispatch 分发操作,并根据不同的操作类型更新状态。使用 reducer 函数可以让状态管理更加清晰,尤其是在处理依赖于之前状态的更新时。

4 - 使用 useRef 管理表单输入

使用 useRef 处理表单数据

React 提供的 useRef 钩子可以让我们访问 DOM 元素并读取其当前的值,这在处理表单时非常有用。在这个例子中,我们使用 useRef 来捕获用户输入的文本和颜色。

初始化表单

首先,我们导入 useRef,并清理之前的代码。接着,我们创建一个包含文本输入框、颜色选择器和提交按钮的表单:

import { useRef } from 'react';

export default function App() {
  const sound = useRef();
  const color = useRef();

  const submit = (e) => {
    e.preventDefault(); // 防止表单默认的提交行为
    const soundValue = sound.current.value;
    const colorValue = color.current.value;
    alert(`${soundValue} sounds like ${colorValue}`);
    sound.current.value = ''; // 重置输入框
    color.current.value = ''; // 重置颜色选择器
  };

  return (
    <form onSubmit={submit}>
      <input ref={sound} type="text" placeholder="Enter sound" />
      <input ref={color} type="color" />
      <button>Add</button>
    </form>
  );
}

访问表单元素

通过 useRef,我们可以为表单元素创建引用并访问它们的当前值。比如,我们创建了 soundcolor 引用,分别用于文本输入框和颜色选择器:

const sound = useRef();
const color = useRef();

在表单的提交函数中,我们通过 sound.current.valuecolor.current.value 获取输入值,并将其显示为 alert

const soundValue = sound.current.value;
const colorValue = color.current.value;
alert(`${soundValue} sounds like ${colorValue}`);

重置表单

提交表单后,我们可以通过将 current.value 设置为空字符串来重置输入框和颜色选择器:

sound.current.value = '';
color.current.value = '';

总结

useRef 是处理表单和访问 DOM 元素的强大工具。在这个例子中,我们展示了如何使用 useRef 访问用户的输入数据,并通过 alert 显示这些数据,同时在提交后重置表单元素。这种模式可以帮助你捕获和处理用户输入,并将其传递给数据库或其他外部服务。

5 - 用 useState 创建受控组件

使用受控组件处理表单输入

在 React 中,另一种处理表单输入的方式是使用受控组件。受控组件的表单输入通过状态变量进行管理,表单的值由状态决定。

使用 useState 替代 useRef

首先,我们用 useState 替代之前的 useRef 来管理表单输入的状态:

import { useState } from "react";

export default function App() {
  const [sound, setSound] = useState('');
  const [color, setColor] = useState('#000000'); // 初始状态为黑色

  const submit = (e) => {
    e.preventDefault(); // 防止表单默认的提交行为
    alert(`${sound} sounds like ${color}`);
    setSound(''); // 重置输入框
    setColor('#000000'); // 重置颜色选择器
  };

  return (
    <form onSubmit={submit}>
      <input 
        type="text" 
        placeholder="Enter sound" 
        value={sound} 
        onChange={(e) => setSound(e.target.value)} 
      />
      <input 
        type="color" 
        value={color} 
        onChange={(e) => setColor(e.target.value)} 
      />
      <button>Add</button>
    </form>
  );
}

受控组件中的状态管理

在受控组件中,我们通过 value 属性来控制表单输入的值,同时使用 onChange 事件来更新状态。这里我们为每个输入框和颜色选择器分别绑定状态和事件处理:

提交表单并重置状态

在表单提交时,我们通过 alert 显示当前的状态值,然后将输入框和颜色选择器的状态重置为初始值:

const submit = (e) => {
  e.preventDefault(); // 防止页面刷新
  alert(`${sound} sounds like ${color}`);
  setSound(''); // 重置输入框
  setColor('#000000'); // 重置颜色选择器
};

总结

通过使用受控组件,表单的输入值完全由组件的状态控制。我们可以更灵活地管理表单数据,并确保输入的值在组件中始终是同步的。每次用户输入时,状态会更新,相应的值也会反映在输入框中。

WangShuXian6 commented 1 day ago

6 - 创建自定义 Hooks

1 - 用自定义 Hooks 复用表单逻辑

创建自定义 Hook:useInput

除了使用 React 库提供的内置钩子外,你还可以根据需要创建自定义 Hook。当你发现多个组件中有重复逻辑时,可以将这些逻辑抽象到自定义 Hook 中。比如,我们在处理表单输入时,可以创建一个 useInput Hook,来简化获取和重置输入值的逻辑。

实现 useInput

首先,我们在项目的 src 文件夹下创建一个名为 useInput.js 的文件,并编写以下代码:

import { useState } from "react";

export default function useInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  // 用于更新输入值的函数
  const onChange = (e) => {
    setValue(e.target.value);
  };

  // 用于重置输入值的函数
  const reset = () => {
    setValue(initialValue);
  };

  return [{ value, onChange }, reset];
}

使用 useInput 的组件

接下来,我们可以在表单组件中使用这个自定义 Hook 来替代之前的 useState 逻辑。我们会在输入字段中使用 useInput,并将 valueonChange 处理逻辑抽象出来。

import useInput from './useInput';

export default function App() {
  const [titleProps, resetTitle] = useInput(''); // 使用空字符串初始化
  const [colorProps, resetColor] = useInput('#000000'); // 使用黑色初始化

  const submit = (e) => {
    e.preventDefault();
    alert(`${titleProps.value} sounds like ${colorProps.value}`);
    resetTitle();  // 重置输入框
    resetColor();  // 重置颜色选择器
  };

  return (
    <form onSubmit={submit}>
      <input type="text" {...titleProps} placeholder="Enter sound" />
      <input type="color" {...colorProps} />
      <button>Add</button>
    </form>
  );
}

自定义 Hook 的好处

通过创建 useInput Hook,我们将表单输入逻辑抽象为一个可复用的模块。每当我们需要处理表单输入时,只需调用 useInput 即可。这样可以减少代码重复,提高可维护性。具体好处包括:

总结

自定义 Hook 允许你根据项目需求提取出常用的逻辑,使代码更易维护、更具复用性。在这个例子中,我们将表单输入逻辑封装到了 useInput 中,减少了重复代码的同时,也增强了组件的灵活性。

2 - 将数据放入 Context

使用 Context 提供全局数据

有时我们希望某些值可以在整个组件树中共享,而不必通过 props 一层一层传递数据。在这种情况下,我们可以使用 React 的 Context,让所有子组件都可以访问这些数据。通过使用 React 的 createContext 和 Hooks,可以更加方便地处理这种场景。

创建 Context 并提供数据

在这个例子中,我们将创建一个 treesContext,用于存储一些树的信息,并将其提供给整个应用中的组件:

import React, { createContext } from "react";

// 创建 treesContext
export const TreesContext = createContext();

// 数据,包含树的 ID 和类型
const trees = [
  { id: 1, type: "Maple" },
  { id: 2, type: "Oak" },
  { id: 3, type: "Family Tree" },
  { id: 4, type: "Component Tree" },
];

export default function App() {
  return (
    // 使用 TreesContext.Provider 来提供数据
    <TreesContext.Provider value={trees}>
      <div>
        <h1>These are the trees I've heard of:</h1>
        {/* 其他组件将通过 Context 来消费这些数据 */}
      </div>
    </TreesContext.Provider>
  );
}

使用 Context 提供数据给子组件

在上面的代码中,我们创建了 TreesContext,并使用 TreesContext.Provider 包裹了整个应用的组件树。通过 Providervalue 属性,我们将 trees 数组传递给了子组件。这样,任何位于 Provider 之下的组件都可以访问这个上下文数据。

在子组件中消费 Context

要在子组件中使用这些数据,我们可以使用 React 的 useContext 钩子来消费 treesContext 中的数据。让我们在下一步中实现这个功能。

在接下来的部分,我们将通过子组件展示这些树的信息。

3 - 使用 useContext 获取数据

使用 useContext 消费上下文数据

在上一节中,我们通过 Provider 将数据传递给子组件。这一次,我们将学习如何使用 useContext 钩子从上下文中读取数据。

从上下文中读取数据

首先,我们需要在组件中引入 useContext 钩子,并将 treesContext 中的数据传递给组件。我们将展示如何在 App 组件中使用这些数据。

import React, { useContext } from "react";
import { TreesContext } from "./index"; // 引入上下文

export default function App() {
  // 使用 useContext 读取上下文数据
  const trees = useContext(TreesContext);

  return (
    <div>
      <h1>These are the trees I've heard of:</h1>
      <ul>
        {trees.map(tree => (
          <li key={tree.id}>{tree.type}</li>  // 显示每棵树的类型
        ))}
      </ul>
    </div>
  );
}

代码说明

  1. 导入 useContext:我们从 React 中引入 useContext,并使用它来消费 treesContext
  2. 使用 useContext 读取数据:我们使用 useContext 来读取上下文提供的数据。在这个例子中,我们从 treesContext 中获取到树的数据,并将它存储在 trees 变量中。
  3. 映射数据并渲染:通过遍历 trees 数组,我们将每棵树的信息作为列表项渲染到页面上。

结果

通过这种方式,任何嵌套在 Provider 下的组件都可以使用 useContext 来读取共享的数据。无论组件在树中的位置如何,它们都能够访问这些数据,而无需通过 props 一层层传递。

这大大简化了在大型应用中管理数据的复杂性。

4 - 使用 Context 创建自定义 Hook

创建自定义 Hook 以简化上下文使用

在 React 中,可以通过创建自定义 Hook 来简化使用上下文的流程。接下来,我们将介绍如何通过创建一个名为 useTrees 的自定义 Hook,使得获取上下文数据更加简洁和可复用。

创建 useTrees 自定义 Hook

  1. 导入 useContext:首先,在 index.js 文件中导入 useContext
  2. 创建自定义 Hook:我们将创建一个名为 useTrees 的自定义 Hook,它会简化我们在组件中使用上下文的方式。
// index.js

import React, { createContext, useContext } from 'react';

// 创建 TreesContext
const TreesContext = createContext();

// 定义自定义 Hook
export const useTrees = () => {
  return useContext(TreesContext);
};

// 提供上下文数据
export default function TreesProvider({ children }) {
  const trees = [
    { id: 1, type: 'Maple' },
    { id: 2, type: 'Oak' },
    { id: 3, type: 'Family Tree' },
    { id: 4, type: 'Component Tree' }
  ];

  return (
    <TreesContext.Provider value={trees}>
      {children}
    </TreesContext.Provider>
  );
}

在组件中使用 useTrees 自定义 Hook

现在我们有了 useTrees 自定义 Hook,它简化了获取上下文数据的过程。我们可以在组件中直接调用 useTrees 而不需要每次手动使用 useContext

// App.js

import React from 'react';
import { useTrees } from './index'; // 使用自定义 Hook

export default function App() {
  const trees = useTrees(); // 调用自定义 Hook 获取数据

  return (
    <div>
      <h1>Trees I've heard of:</h1>
      <ul>
        {trees.map(tree => (
          <li key={tree.id}>{tree.type}</li> // 显示树的类型
        ))}
      </ul>
    </div>
  );
}

代码说明

  1. useTrees Hook:自定义 Hook useTrees 内部使用 useContext,将 TreesContext 中的数据直接提供给调用它的组件。
  2. 简化代码:在 App 组件中,我们不再需要显式调用 useContext,而是通过 useTrees 直接访问上下文数据。这使得代码更加简洁和可复用。
  3. 清除多余依赖:通过自定义 Hook,我们不再需要在每个使用上下文的组件中重复引入 useContext 和上下文对象,只需要引入自定义 Hook 即可。

结果

通过创建自定义 Hook,我们使得从上下文中获取数据更加简便。这在需要在多个组件中使用相同上下文数据时尤其有用,减少了重复代码,提升了代码的可维护性和简洁性。

5 - 用 Fetch Hook 获取数据

使用自定义 Hook 进行数据获取

在这个视频中,我们将通过创建自定义 Hook 来实现数据的获取操作。这样,我们可以更加优雅地管理数据请求,并处理加载、成功和错误状态。

创建 useFetch 自定义 Hook

useFetch 是一个自定义 Hook,它将处理从 API 获取数据的所有逻辑,并返回加载状态、数据和错误信息。

// useFetch.js

import { useState, useEffect } from 'react';

// 定义 useFetch 自定义 Hook
export const useFetch = (uri) => {
  // 定义状态变量
  const [data, setData] = useState(null);     // 数据
  const [loading, setLoading] = useState(true);  // 加载状态
  const [error, setError] = useState(null);    // 错误状态

  // 使用 useEffect 进行数据获取
  useEffect(() => {
    if (!uri) return;  // 如果没有 URI,直接返回

    setLoading(true);  // 设置加载状态为 true

    // 执行数据请求
    fetch(uri)
      .then(response => response.json())  // 将响应解析为 JSON
      .then(data => {
        setData(data);        // 设置获取到的数据
        setLoading(false);    // 设置加载状态为 false
      })
      .catch(error => {
        setError(error);      // 如果有错误,设置错误状态
        setLoading(false);    // 设置加载状态为 false
      });
  }, [uri]);  // 当 URI 改变时,重新发起请求

  // 返回加载状态、数据和错误状态
  return { loading, data, error };
};

在组件中使用 useFetch 自定义 Hook

我们现在可以在任何组件中使用 useFetch 来获取数据。例如,下面的 App 组件会使用 useFetch 来从 GitHub API 获取用户数据,并根据不同的状态显示相应的内容。

// App.js

import React from 'react';
import { useFetch } from './useFetch';

export default function App() {
  // 使用自定义 Hook 获取数据
  const { loading, data, error } = useFetch('https://api.github.com/users');

  // 渲染不同的状态
  if (loading) return <h1>Loading...</h1>;       // 加载中
  if (error) return <p>Error: {error.message}</p>;  // 错误

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.login}</li>  // 显示用户登录名
      ))}
    </ul>
  );
}

代码说明

  1. 自定义 Hook useFetch

    • useFetch 接收一个 URI 作为参数,并根据这个 URI 发起 HTTP 请求。
    • 它管理三个状态变量:loading(是否正在加载)、data(请求返回的数据)和 error(请求错误信息)。
    • 当 URI 改变时,useEffect 会重新执行数据获取。
  2. 在组件中使用 useFetch

    • App 组件调用 useFetch 并传入 GitHub API 的 URL 来获取用户数据。
    • 根据 loadingerrordata 的不同状态,组件渲染不同的内容:加载时显示 "Loading...",如果发生错误则显示错误信息,成功则渲染用户列表。

总结

通过创建 useFetch 自定义 Hook,我们成功地将数据获取逻辑封装在一个通用的 Hook 中,使其可以在多个组件中重用。这不仅简化了代码,还提高了可维护性和可读性。

6 - 构建一个获取数据的组件

使用 useFetch Hook 来获取数据

在这个视频中,我们已经创建了一个自定义的 useFetch Hook,并在 App 组件中使用它来从 GitHub API 获取用户数据。以下是具体的步骤和代码展示:

1. 创建 App 组件

index.js 文件中,我们将使用 useFetch 来获取用户数据并展示在页面上。以下是 App 组件的代码:

// App.js

import React from 'react';
import { useFetch } from './useFetch';

export default function App({ login }) {
  // 使用自定义 Hook 获取数据
  const { loading, data, error } = useFetch(`https://api.github.com/users/${login}`);

  // 如果正在加载,显示加载状态
  if (loading) return <h1>Loading...</h1>;

  // 如果有错误,显示错误信息
  if (error) return (
    <pre>{JSON.stringify(error, null, 2)}</pre>
  );

  // 显示用户数据
  return (
    <div>
      <img src={data.avatar_url} alt={data.login} />
      <h1>{data.login}</h1>
      {data.name && <p>Name: {data.name}</p>}
      {data.location && <p>Location: {data.location}</p>}
    </div>
  );
}

2. 使用 useFetch Hook

我们使用 useFetch 来从 API 获取数据。它会返回三个状态变量:loading(表示数据是否正在加载)、data(获取到的数据)、error(如果有错误,则显示错误信息)。

3. 在 index.js 中渲染 App 组件

我们在 index.js 中将 App 组件渲染出来,并传入一个 login 属性作为 GitHub 用户名。

// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <App login="your-github-username" />,  // 传入 GitHub 用户名
  document.getElementById('root')
);

代码说明

  1. 处理加载状态:通过判断 loading 是否为 true,决定是否显示加载状态。
  2. 处理错误状态:如果发生错误,则返回错误信息,并将其格式化为 JSON 输出到页面。
  3. 展示用户信息:在数据加载完成并且没有错误的情况下,显示用户的头像、用户名、真实姓名(如果有)以及位置信息(如果有)。

总结

通过使用 useFetch Hook,我们简化了组件内部的数据获取逻辑,并处理了数据加载的不同状态。这使得代码更加简洁、可复用,并且可以轻松地在整个应用程序中使用该自定义 Hook 进行数据获取。

WangShuXian6 commented 1 day ago

7 - 总结

1 - 下一步

React Hooks 总结

感谢大家一路走来,一起学习 React Hooks 及其在各种场景中的应用。我们探讨了许多库,如 React Router、Relay、Apollo 等等,它们都将 Hooks 作为其 API 的一部分。本课程旨在为你打下基础,让你能够更好地探索 React 生态系统的其他领域。

如果你想了解更多关于开发自定义 Hooks 的内容,我强烈推荐一个很酷的网站——usehooks.com。在那里,你可以学习更多关于如何开发 Hooks 的信息,并获取一些实用技巧,将其应用到你自己的项目中。

非常感谢大家参与本课程,祝愿你在 React 学习之路上取得更大进展,继续前行!