1、安装
1.1 前序
React 从一开始就被设计为可以被渐进地采用,你可以根据需要或多或少地使用 React。无论你只是想体验一下 React,并为 HTML 页面添加一些交互性,还是创建一个复杂的 React
1.1.1、In this chapter
如何将 React 添加到 HTML 页面中
如何新建一个独立的 React 项目
如何设置编辑器
如何安装 React 开发者工具
1.1.2、试用 React
无需安装任何东西就可以试用 React。请试着修改以下代码!
function Greeting({ name }) {
return <h1>Hello, {name}</h1>;
}
export default function App() {
return <Greeting name="world" />
}
我们在所有文档中使用代码沙盒(sandbox)作为教学的辅助工具。代码沙盒可以帮助你熟悉 React 的工作原理,并帮助你考察 React 是否适用。除了 React 文档外,还有许多支持 React 的第三方在线代码沙盒,例如:CodeSandbox 、Stackblitz 或 CodePen。
1.1.3、在本地试用 React,在页面中添加 React
要在本地计算机上试用 React 的话,打开一个html网页。然后在编辑中打开并编辑,在浏览器中查看效果。如果你手头已经有一个能运行的网站了,只想利用 React 添加一点儿功能的话,看后续介绍
1.1.4、创建一个 React 项目
如果你已经准备利用 React 创建一个独立的项目,那么你可以设置一个最小的工具链,让开发体验更愉快。你还可以使用现成的框架,直接开箱即用。
1.1.5、下一步
从哪里开始学习取决于你喜欢什么样的学习方式、需要完成的目标以及你接下来的打算!为什么不从阅读 (我们的入门教程)开始呢?或者你可以跳转到 描述 UI
章节拿更多示例练练手并一步步地学习每个知识点。记住,没有那种学习 React 的路径是错误的!
1.2、创建一个 React 新项目
如果你正在学习 React 或者考虑将其应用到现有的项目中,你可以利用script 标签将 React添加到任何 HTML 页面,来快速开启学习之旅。如果你的项目需要许多组件和许多文件,那就需要考虑以下方式了!
1.2.1、选择你自己的冒险方式
React 是一个工具库,帮你以组件的方式拆解并组织 UI 代码。React 不负责路由(routing)或数据管理。对于这些功能,你需要使用第三方工具库或实现你自己的解决方案。这意味着创建一个新的 React 项目有多种方式可以选择:使用 最小设置的工具链, 根据需要为项目添加功能。使用 功能完备的框架,常用功能都已内置。无论你是刚入门,想要构建一个大项目,还是想要建立自己的工具链,本指南都能为你指明道路。
1.3、React 工具链入门
如果你是刚刚开始接触 React,我们建议你使用 create React App ,这是尝试 React 功能的最流行的方式,也是构建新的单页客户端应用的最好方法。Create React App 是一个专为 React 配置的功能齐备的工具链。工具链有助于:
创建大量的文件和组件
使用来自 npm 的第三方工具库
及早检测到常见错误
开发时能实时编辑 CSS 和 JS
针对生产环境优化输出的文件
你仅需一条命令就可以在终端(命令行)中利用 Create React App 创建一个新项目!(前提是确保安装了Node.js !)
npx create-react-app my-app
现在就可以通过以下命令运行你的应用程序了:
cd my-app
npm start
Create React App 并不处理后端逻辑或数据库操作,它只是创建了一个针对前端的构建管道。这意味着你可以为其对接任何后端。但是,如果你寻找的是对类似路由(routing)以及服务器端业务逻辑功能的支持的话,请接着往下看!
1.4、同时使用 React 和框架
如果你希望创建一个更大的、可用于生产环境的项目的话,
Next.js 是一个非常好的起点。Next.js 是一个流行的、基于 React 构建的轻量级框架,用于构建静态和服务器端渲染的应用程序。该框架自带了路由(routing)、样式表( styling)和服务器端渲染(server-side rendering)等功能,可以让你的项目快速开始并运行起来。
请查看 Next.js 的官方指导:使用 Next.js 构建项目
1.5、其他选项
Gatsby 能帮你基于 React 和 GraphQL 生成静态网站。
Razzle是一个支持服务器端渲染(server-rendering)的框架,无需任何配置,但比 Next.js 提供了更多的灵活性。
1.6、自定义工具链
你可能更喜欢创建并配置自己的工具链。一个 JavaScript 构建工具链通常包含以下部分:
一个 软件包管理器—用于安装、更新和管理第三方软件包。
Yarn 和
npm 就是两个比较流行的软件包管理器。
一个 打包工具(bundler)—将您编写的模块化代码打成小包以优化加载时间。Webpack
、Snowpack、Parcel 就是几个比较流行的打包工具。
一个 编译器—将你使用新语法编写的 JavaScript 代码转换成能被老版本的浏览器执行的代码。Babel 就是这类工具中的一个代表。
在较大的项目中,你可能还需要一个工具来管理单一仓库中的多个软件包。Nx
就是此类工具中的一个代表。
如果你喜欢葱头开始创建自己的 JavaScript 工具链的话,请 查看这份指南
来了解如何自行实现 Create React App 中的功能。
2、为网站添加 React
React 从一开始就被设计为支持渐进式采用,你可以根据需要或多或少地使用 React。无论是微前端(micro-frontends)、现有系统,还是只是尝试一下 React,都可以通过添加几行代码就能为页面添加交互式的 React 组件,并且无需使用构建工具!
2.1、第一步:在页面中添加一个 HTML 元素
在要编辑的 HTML 页面中添加一个 HTML 元素,例如带有唯一 id
属性的空的 <div>
标签,该标签用于 React 定位内容显示的位置。
你可以在 <body>
标签内的任何位置放置一个类似 <div>
的“容器”元素。React 将把该 HTML 元素内的任何内容替换掉,因此一个空标签即可。你可以根据需要在页面上放置任意多个类似的 HTML 元素。
<!-- ... existing HTML ... -->
<div id="component-goes-here"></div>
<!-- ... existing HTML ... -->
2.2、第二部:添加 script 标签
在 HTML 页面中,将以下三个文件通过 <script>
标签添加到 </body>
标签前:react.development.js 加载 React 核心代码 react-dom.development.js 让 React 渲染 HTML 元素到 DOM
中。
like_button.js 这将是你在第三步中编写组件的地方!部署到生产环境时,将 “development.js” 文件替换为 “production.min.js” 文件。
<!-- end of the page -->
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="like_button.js"></script>
</body>
2.3、React without JSX
最初引入 JSX 是为了让使用 React 编写组件的感觉就像写 HTML 一样。从那时起,改语法变得流行起来。但是,在某些情况下,你可能不想使用或无法使用 JSX。那么你有两个选择:使用类似htm
之类的 JSX 替代方案,htm 不需要编译器,而是使用 JavaScript 内置的标记模板(Tagged Templates)语法。
使用 React.createElement()
函数,该函数具有特殊结构,后面有详解。
通过使用 JSX,你可以像这样编写组件:
function Hello(props) {
return <div>Hello {props.toWhat}</div>;
}
ReactDOM.render(<Hello toWhat="World" />, document.getElementById('root'));
而使用 React.createElement()
函数的化,你需要像这样编码:
function Hello(props) {
return React.createElement('div', null, `Hello ${props.toWhat}`);
}
ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);
该函数接受三个参数: React.createElement(component, props, children)
。以下解释器工作原理:
一个 component 参数,可以实一个代表 HTML 元素的字符串,也可以是一个函数形式的组件
一个对象,可以是任何 你想传入的 props
一个对象,可以是任何 子 组件,例如文本字符串
如果你厌倦了输入 React.createElement()
,一个常规的办法是为其赋予一个速记符:
const e = React.createElement;
ReactDOM.render(e('div', null, 'Hello World'), document.getElementById('root'));
如果将此速记符来代替 React.createElement()
,就能在不使用 JSX 的情况下达成同样的便利。
3、设置编辑器
一个正确配置的编辑器能够让读代码更清晰、写代码更快。它甚至可以帮你在写代码时捕获 bug!如果这是你第一次设置编辑器,或者你希望调整编辑器,以下是我们的一些建议。
3.1、选择你的编辑器
VS Code 是如今最流行的编辑器之一。它拥有一个庞大的插件市场,并集成了 GitHub 等流行的服务。下面列出的功能大部分可以作为插件添加到 VS Code 中,插件让 VS Code 高度可配置!
React 社区中其它常用的编辑器包括:
WebStorm
—专为 JavaScript 设计的集成开发环境。Sublime Text
—支持 JSX 和 TypeScript,内置了语法高亮和自动代码补全功能。Vim
—一个高度可配置的文本编辑器,能够高效地创建和修改任何类型的文本。它作为 “vi” 命令存在于大多数 UNIX 系统和 Apple OS X 中。
3.2、推荐功能
某些编辑器内置了这些功能,但其它编辑器可能需要安装插件。请查看你所选择的编辑器是支持以下功能!
3.3、代码过滤(Linting)
代码过滤的作用是在你书写代码时发现代码中的错误,并帮助你今早修复错误。
ESLint是一个流行的、开源的 JavaScript 过滤器。
安装 ESLint 并使用 React 的推荐配置 (请确保 Node 已安装! )
利用官方插件将 ESLint 集成到 VSCode 中
3.4、格式化
与其他贡献者共享代码时,最不想做的事情就是讨论用制表符(tabs)还是空格(spaces)!幸好,有Prettier来重新清理代码使其符合预定义的规则。运行 Prettier,所有的制表符(tabs)都将转换为空格(spaces),缩进、引号等也将全部根据配置被修改。理想的设置是当你保存文件时,Prettier 就会运行并帮你完成这些修改。
你可以按如下步骤安装 Prettier extension in VSCode:
启动 VS Code
按快捷键(
CTRL/CMD + P
)粘贴
ext install esbenp.prettier-vscode
按回车键
保存文件时执行格式化,理想情况下,应该是在你每次保存文件时格式化代码。VS Code 已支持此设置!
在 VS Code 中,按
CTRL/CMD + SHIFT + P
。输入 “settings”
按回车键
在搜索栏中,输入 “format on save”
确保勾选了 “format on save” 选项!
Prettier 有时会与其它代码过滤其产生冲突。但是通常都会有办法让它们很好地配合工作。例如,如果需要同时使用 Prettier 和 ESLint,则可以使用 eslint-prettier 插件并通过 ESLint 规则来运行 prettier。
4、React 开发者工具
通过 React 开发者工具(React Developer Tools)可以检查 React components 、编辑 props
和 state,以及定位性能问题。
4.1、浏览器扩展
对使用 React 构建的网站进行调试的最简单方法就是安装并使用 React 开发者工具的浏览器扩展插件。该插件支持几种常简的浏览器:
为 Chrome 浏览器安装扩展插件
为 Firefox 浏览器安装扩展插件
为 Edge 浏览器安装扩展插件、
现在,如果你访问 基于 React 构建的网站 时,你将看到 Components 和 Profiler 面板。
4.2、Safari 和其它浏览器
对于其它浏览器(例如 Safari),请安装react-devtools 这一 npm 软件包:
# Yarn
yarn global add react-devtools
# Npm
npm install -g react-devtools
然后从终端(命令行)中开启开发者工具:
react-devtools
然后,通过在网站的 <head>
标签内添加以下 <script>
标签来连接网站:
4.3、移动端(React Native)
React 开发者工具也可用于检查基于React Native
构建的应用程序。将 React 开发者工具安装到全局环境中是最简单的方式:
# Yarn
yarn global add react-devtools
# Npm
npm install -g react-devtools
接下来打开终端(命令行)就可以使用开发者工具了。
react-devtools
该工具就可以连接到任何运行在本地机器上的 React Native 应用程序了。如果开发者工具经过几秒后仍无法连接,请尝试重启应用程序。
5、快速入门
欢迎访问 React 文档!本页将向您介绍 80% 的 React 概念,这些概念将是您日常开发中经常用到的。
You will learn
如何创建并嵌套组件
How to add markup and styles
如何展示数据
How to render conditions and lists
如何响应事件并更新屏幕显示
如何在组件间共享数据
创建并嵌套组件
React 应用程序是由组件(component)组成的。组件是 UI(用户界面)的组成部分,拥有自己的逻辑和外观。一个组件可以小到一个按钮,大到整个页面。
React 组件就是 JavaScript 函数(function),此类函数返回由标签语言编写的用户界面:
function MyButton() {
return (
<button>Click me</button>
);
}
现在,你已经声明了 MyButton
组件,接下来就可以将其嵌入到其它组件中了:
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton />
</div>
);
}
请注意,<MyButton />
标签以大写字母开头,这样就能便于识别这个是一个 React 组件。React 组件的名称必须始终以大写字母开头,而 HTML 标签必须全部为小写字母。
看看成果:
function MyButton() {
return (
<button>
Click me
</button>
);
}
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton />
</div>
);
}
export default
关键字在文件中标明了主要组件。
编写 JSX 语法的标签
你在前面看到的标记语言(markup syntax)称为 JSX。JSX 不是必须要用的,但是因为使用方便,所以大多数 React 项目都使用 JSX。所有 我们推荐的用于本地开发的工具
都自带对 JSX 的支持。
JSX 的语法比 HTML 更严格。类似 <br />
这样的标签是必须要关闭的。并且,组件也不能返回多个并列最高层级的 JSX 标签,你必须为所有最高层级的标签添加一个共同的父标签,例如使用 <div>...</div>
或 <>...</>
作为父标签:
function AboutPage() {
return (
<>
<h1>About</h1>
<p>Hello there.<br />How do you do?</p>
</>
);
}
如果你需要将大量 HTML 代码移植到 JSX 语法,可以使用这个 在线转换器。添加样式,在 React 中,通过 className
这个属性来指定 CSS 类。它和 HTML 的 class 属性的功能是一样的:
<img className="avatar" />
然后在一个单独的 CSS 文件中为其编写 CSS 样式:
/* In your CSS */
.avatar {
border-radius: 50%;
}
React 没有规定如何添加 CSS 文件。最简单的方式是添加一个 <link> 标签到页面的 HTML 代码中。如果你使用了构建工具或框架,请查阅其相关文档,以便了解如何将 CSS 文件添加到你的项目中。
5.1、React 编程思想
React 可以改变你对所看到的设计以及所构建的应用程序的看法。以前你看到的是一片森林,使用 React 后,你将欣赏到每一棵树。React 简化了你对设计系统(design system)和 UI 状态的看法。在本教程中,我们将带领你使用 React 构建一个可搜索的数据表产品,并领略整个思考的过程。
从原型开始
假设你已经拥有了一个 JSON API 以及一个来自设计师的原型设计。JSON API 返回的数据如下所示:
[
{ category: "Fruits", price: "$1", stocked: true, name: "Apple" },
{ category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit" },
{ category: "Fruits", price: "$2", stocked: false, name: "Passionfruit" },
{ category: "Vegetables", price: "$2", stocked: true, name: "Spinach" },
{ category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin" },
{ category: "Vegetables", price: "$1", stocked: true, name: "Peas" }
]
原型设计如下:
用 React 实现 UI 的话,通常遵循相同的五个步骤。
5.2、第一步:将 UI 分解为组件结构
首先,在原型图中的每个组件及其子组件周围画一个框,并为他们命名。如果你有设计师搭档的话,他们可能已经在设计工具中为这些组件命名了。请与他们确认!
根据你的背景不同,你可以考虑采用不同的方式将原型设计拆分为不同的组件:
编程—使用相同的技术来决定是否需要创建新函数或对象。其中一个方法是 单一责任原则(single responsibility principle),也就是说,一个组件在理理想情况下只负责一件事情。如果变得臃肿了,则应将其分解为更小的子组件。
CSS—考虑类选择器(class selector)的用途。(但是,组件的粒度要小一些。)。
设计—考虑如何阻止设计中所用的图层(layer)。
如果 JSON 的结构良好,你通常会发现它能很自然地映射到 UI 的组件结构上。这是因为 UI 和数据模型通常具有相同的信息结构,即相同的模型(shape)。将 UI 分成多个组件,每个组件将与数据模型的一部分相匹配。
下图中可以看到有五个组件:
注意看 ProductTable
(即紫色部分),你会发现表头(包含 “Name” 和 “Price” 字样)并非是独立组件。这其实是一个偏好的问题,你可以选择将其独立成组件或作 ProductTable
组件的组成部分。在本例中,表头是 ProductTable
的组成部分,因为它出现在 ProductTable
的列表中。但是,如果此表头变得复杂了(例如需要添加排序功能),则更适合将其提取为 ProductTableHeader
组件。
既然你已经在原型图中标识了组件,那么就将它们排列成一个层级结构吧。在原型图中如果一个组件出现在另一个组件中,那么就应该作为子组件显示在层级结构中:
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
5.3、第二部:基于 React 构建静态版本
现在,你已经梳理了组件的层次结构,是时候开始实现这个应用程序了。最直接的方法是构建一个利用数据模型渲染 UI 的版本,此时无需添加任何交互功能。先构建静态版本然后再添加交互功能,通常是一条便捷的路径。构建静态版本需要大量的码字,但无需太多思考;而添加交互功能则需要大量的思考,码字反而不多。
要构建一个渲染数据模型的静态版本应用程序,你需要在重用其他组件的基础上构建新 组件(components)
并通过 props
传递数据。props 是将数据从父组件传递到子组件的一种方式。(如果你对于 state
的概念有了解的话,请不要使用 state(状态) 来构建这个静态版本的应用程序,state 仅用于交互,也就是随时间而变化的数据。由于这里要构建的是静态版本的应用程序,因此不要用 state。)
你可以按照“自上而下”(即,从较高层级的组件,例如 FilterableProductTable
,开始构建)或“自下而上”(即,从较低层级的组件,例如 ProductRow
,开始构建)的顺序构建所有组件。在比较简单的项目中,自上而下的方式通常共容易;而在较大的项目中,自下而上的方式更容易。
import { useState } from 'react';
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>
);
}
function ProductCategoryRow({ category }) {
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
function ProductRow({ product }) {
const name = product.stocked ? product.name :
<span style={{ color: 'red' }}>
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
function ProductTable({ products, filterText, inStockOnly }) {
const rows = [];
let lastCategory = null;
products.forEach((product) => {
if (
product.name.toLowerCase().indexOf(
filterText.toLowerCase()
) === -1
) {
return;
}
if (inStockOnly && !product.stocked) {
return;
}
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category} />
);
}
rows.push(
<ProductRow
product={product}
key={product.name} />
);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange
}) {
return (
<form>
<input
type="text"
value={filterText} placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)} />
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={(e) => onInStockOnlyChange(e.target.checked)} />
{' '}
Only show products in stock
</label>
</form>
);
}
const PRODUCTS = [
{category: "Fruits", price: "$1", stocked: true, name: "Apple"},
{category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
{category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
{category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
{category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
{category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];
export default function App() {
return <FilterableProductTable products={PRODUCTS} />;
}
3、绘制用户界面(UI)
React 是一个用于渲染用户界面(UI)的 JavaScript 工具库。UI 是由类似按钮(buttons)、文本框(text)以及图片(images)之类的小零件构成的。React 能够让你将这些“小零件”组合成可重复利用、可嵌套的 组件(components)。从网站到手机应用,屏幕上的所有内容都可以拆解成组件。在本章中,你将学习到如何创建、自定义以及根据条件显示 React 组件
In this chapter
如何编写你的第一个 React 组件
何时以及如何创建多组件文件
如何利用 JSX 在 JavaScript 中书写 markup
如何在 JSX 中利用花括号来访问组件外的 JavaScript 功能
如何通过 props 来配置组件
如何根据条件渲染组件
如何一次渲染多个组件
如何通过保持组件的功能单一来避免产生令人困惑的 bug
3.1、你的第一个组件
React 应用程序是由被称为“组件(components)”的独立的 UI 片段构成的。一个 React 组件就是一个 JavaScript 函数,并且函数中可以添加 markup。组件可以小刀一个按钮,也可以大到整个页面。下面展示的是一个 Gallery
组件,其中渲染了三个 Profile
组件:
function Profile() {
return (
<img
src="https://i.imgur.com/MK3eW3As.jpg"
alt="Katherine Johnson"
/>
);
}
export default function Gallery() {
return (
<section>
<h1>Amazing scientists</h1>
<Profile />
<Profile />
<Profile />
3.2、导入(Import)和导出(exporing)组件
你可以在一个文件中声明多个组件,但是文件变得太大的话就不方便查看了。要解决此问题,你可以将一个组件放到一个单独的文件中并(导出) export 组件,然后在另一个文件中(导入) import 该组件:
import Profile from './Profile.js';
export default function Gallery() {
return (
<section>
<h1>Amazing scientists</h1>
<Profile />
<Profile />
<Profile />
</section>
);
}
3.3、用 JSX 书写 markup
每个 React 组件就是 JavaScript 函数,函数中可以书写 markup,这些 markup 将由 React 渲染到浏览器中。React 组件使用名为 JSX 的语法扩展来支持 markup。JSX 看上去就像 HTML 一样,但它的语法比较严格,并且可以显示动态信息。
如果我们将现有的 HTML markup 粘贴到 React 组件中,可能会报错:
export default function TodoList() {
return (
// This doesn't quite work!
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
>
<ul>
<li>Invent new traffic lights
<li>Rehearse a movie scene
<li>Improve spectrum technology
</ul>
);
}
如果你已有上述类似的 HTML 了,可以使用 converter 工具来修复语法错误:
export default function TodoList() {
return (
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo"
/>
<ul>
<li>Invent new traffic lights</li>
<li>Rehearse a movie scene</li>
<li>Improve spectrum technology</li>
</ul>
</>
);
}
3.4、JavaScript in JSX with curly braces
JSX 允许你在 JavaScript 文件中书写类似 HTML 的标记,使渲染逻辑和内容处于同一位置。有时,你需要添加一点 JavaScript 逻辑或者在 markup 中引用一个动态属性。在这种情况下,你可以在 JSX 中使用大花括号为 JavaScript “开一扇窗”:
const person = {
name: 'Gregorio Y. Zara',
theme: {
backgroundColor: 'black',
color: 'pink'
}
};
export default function TodoList() {
return (
<div style={person.theme}>
<h1>{person.name}'s Todos</h1>
<img
className="avatar"
src="https://i.imgur.com/7vQD0fPs.jpg"
alt="Gregorio Y. Zara"
/>
<ul>
<li>Improve the videophone</li>
<li>Prepare aeronautics lectures</li>
<li>Work on the alcohol-fuelled engine</li>
</ul>
</div>
);
}
3.5、将属性(props)传递给组件
React 组件使用 props 参数来互相通信。每个父组件都可以通过 props 将信息传递给子组件。props 可能会让你联想到 HTML 中的属性(attributes),但是你可以通过 props 传递 JavaScript 所支持的不同类型的值,包括对象(objects)、数组(array)、函数(functions)甚至 JSX!
import { getImageUrl } from './utils.js'
export default function Profile() {
return (
<Card>
<Avatar
size={100}
person={{
name: 'Katsuko Saruhashi',
imageId: 'YfeOqp2'
}}
/>
</Card>
);
}
function Avatar({ person, size }) {
return (
<img
className="avatar"
src={getImageUrl(person)}
alt={person.name}
width={size}
height={size}
/>
);
}
function Card({ children }) {
return (
<div className="card">
{children}
</div>
);
}
3.6、按条件渲染
组件通常需要根据不同的条件显示不同的内容。在 React 中,你可以使用诸如 if
语句、&&
以及 ? :
操作符等控制渲染 JSX 的条件。
在本示例中,JavaScript 的 &&
操作符被用于控制在什么条件下渲染复选框(checkmark):
function Item({ name, isPacked }) {
return (
<li className="item">
{name} {isPacked && '✔'}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
3.7、渲染列表
你通常需要按照数据集合来显示多个相似的组件。这是,你可以将 React 和 JavaScript 的 filter()
和 map()
函数一同使用,以便将数据中的数组转换为组件数组。
对于每一个数组项,你需要指定一个 key(键)
。通常,你会使用数据库中的 ID 作为 key
使用。key
的作用是让 React 保持对列表中每个条目的追踪,即便列表发生了变化,也依然能够追踪每个条目在列表中的位置。
import { people } from './data.js';
import { getImageUrl } from './utils.js';
export default function List() {
const listItems = people.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
);
return (
<article>
<h1>Scientists</h1>
<ul>{listItems}</ul>
</article>
);
}
3.8、保持组件的功能单一
某些 JavaScript 函数是很 “pure(纯粹的)”。一个“纯粹的”函数有如下特点:
只管好自己的事。 在被调用时,不会更改已经存在的任何对象(objects)或变量。
相同的输入,相同的输出。 给定相同的输入,“纯粹的”函数是能够始终返回相同结果的。
通过严格的将组件编写为“纯粹的”函数,你就可以避免在代码库膨胀时出现一系列令人费解的错误和不可预测的行为。以下示例展示的是一个“不纯粹的”组件:
let guest = 0;
function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
)
}
你可以通过传递 prop 而不是修改已存在的变量,让此组件变为“纯粹的”组件:
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup guest={1} />
<Cup guest={2} />
<Cup guest={3} />
</>
);
}
4、添加交互
4.1、前序
屏幕上的一些东西会根据用户输入而更新。例如,单击图片库可切换活动图像。在React中,随时间变化的数据称为状态。您可以向任何组件添加状态,并根据需要进行更新。在本章中,你将学习如何编写组件来处理交互,更新它们的状态,并随时间显示不同的输出。
这是一个使用React构建的测验表单。请注意它是如何使用status状态变量来确定是启用还是禁用提交按钮,以及是否显示成功消息。
在本章中
如何处理用户发起的事件
如何使组件“记住”有状态的信息
React如何分两个阶段更新UI
为什么状态在你更改后没有立即更新
如何对多个状态更新进行排队
如何更新状态中的对象
如何更新状态数组
4.2、响应事件
React允许您向JSX添加事件处理程序。事件处理程序是您自己的函数,将在响应用户交互(如单击、悬停、关注表单输入等)时触发。
像<button>这样的内置组件只支持像onClick这样的内置浏览器事件。但是,您也可以创建自己的组件,并为它们的事件处理程序道具赋予您喜欢的任何特定于应用程序的名称。
export default function App() {
return (
<Toolbar
onPlayMovie={() => alert('Playing!')}
onUploadImage={() => alert('Uploading!')}
/>
);
}
function Toolbar({ onPlayMovie, onUploadImage }) {
return (
<div>
<Button onClick={onPlayMovie}>
Play Movie
</Button>
<Button onClick={onUploadImage}>
Upload Image
</Button>
</div>
);
}
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
4.3、状态:组件的内存
组件经常需要在交互过程中改变屏幕上的内容。在表单中输入应该会更新输入字段,在图像转盘上单击“next”应该会更改显示的图像,单击“buy”将产品放入购物车。组件需要“记住”一些东西:当前输入值、当前图像、购物车。在React中,这种特定于组件的内存称为状态。
你可以用useState钩子给组件添加状态。Hooks是一种特殊的函数,它可以让你的组件使用React的特性(状态就是其中之一)。useState钩子允许你声明一个状态变量。它接受初始状态并返回一对值:当前状态和一个允许您更新它的状态设置函数。
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
Here is how an image gallery uses and updates state on click:
import { useState } from 'react';
import { sculptureList } from './data.js';
export default function Gallery() {
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleNextClick() {
setIndex(index + 1);
}
function handleMoreClick() {
setShowMore(!showMore);
}
let sculpture = sculptureList[index];
return (
<>
<button onClick={handleNextClick}>
Next
</button>
<h2>
<i>{sculpture.name} </i>
by {sculpture.artist}
</h2>
<h3>
({index + 1} of {sculptureList.length})
</h3>
<button onClick={handleMoreClick}>
{showMore ? 'Hide' : 'Show'} details
</button>
{showMore && <p>{sculpture.description}</p>}
<img
src={sculpture.url}
alt={sculpture.alt}
/>
</>
);
}
4.4、呈现和提交
在你的组件显示在屏幕上之前,它们必须由React渲染。理解这个过程中的步骤将帮助您思考代码是如何执行并解释其行为的。
想象一下,你的组件是厨房里的厨师,用食材组装美味的菜肴。在这个场景中,React是服务员,负责输入客户的请求并为他们带来订单。这个请求和服务UI的过程有三个步骤:
触发渲染(将用餐者的订单发送到厨房)
呈现组件(从厨房获取订单)
提交到DOM(将订单放在表上)
4.5、状态为快照
与常规的JavaScript变量不同,React状态的行为更像是一个快照。设置它不会改变已经拥有的状态变量,而是会触发重新呈现。这一开始可能会令人惊讶!
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
React以这种方式工作是为了帮助你避免细微的bug。这是一个小聊天应用程序。试着猜一下,如果你先按“发送”,然后把收件人改为鲍勃,会发生什么。谁的名字会在五秒钟后出现在警报中?
import { useState } from 'react';
export default function Form() {
const [to, setTo] = useState('Alice');
const [message, setMessage] = useState('Hello');
function handleSubmit(e) {
e.preventDefault();
setTimeout(() => {
alert(`You said ${message} to ${to}`);
}, 5000);
}
return (
<form onSubmit={handleSubmit}>
<label>
To:{' '}
<select
value={to}
onChange={e => setTo(e.target.value)}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
</select>
</label>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
4.6、对一系列状态更改进行排队
这个组件有bug:点击“+3”只会增加一次分数。
import { useState } from 'react';
export default function Counter() {
const [score, setScore] = useState(0);
function increment() {
setScore(score + 1);
}
return (
<>
<button onClick={() => increment()}>+1</button>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
<h1>Score: {score}</h1>
</>
)
}
状态为快照解释了发生这种情况的原因。设置状态请求新的重新呈现,但不会在已经运行的代码中更改它。所以在你调用setScore(score + 1)之后,score仍然是0。
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
您可以通过在设置状态时传递更新函数来解决这个问题。注意如何用setScore(s => s + 1)替换setScore(score + 1)来修复“+3”按钮。如果需要对多个状态更新进行排队,这很方便。
import { useState } from 'react';
export default function Counter() {
const [score, setScore] = useState(0);
function increment() {
setScore(s => s + 1);
}
return (
<>
<button onClick={() => increment()}>+1</button>
<button onClick={() => {
increment();
increment();
increment();
}}>+3</button>
<h1>Score: {score}</h1>
</>
)
}
4.7、更新处于状态的对象
State可以保存任何类型的JavaScript值,包括对象。但是你不应该直接改变你在React状态下持有的对象和数组。相反,当您想要更新对象和数组时,您需要创建一个新的对象和数组(或创建一个现有对象和数组的副本),然后更新状态以使用该副本。
通常,你会用…扩展语法以复制要更改的对象和数组。例如,更新一个嵌套对象可能像这样:
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
setPerson({
...person,
name: e.target.value
});
}
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value
}
});
}
function handleCityChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
city: e.target.value
}
});
}
function handleImageChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
image: e.target.value
}
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<label>
Image:
<input
value={person.artwork.image}
onChange={handleImageChange}
/>
</label>
<p>
<i>{person.artwork.title}</i>
{' by '}
{person.name}
<br />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}
If copying objects in code gets tedious, you can use a library like Immer to reduce repetitive code:
import { useImmer } from 'use-immer';
export default function Form() {
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
function handleNameChange(e) {
updatePerson(draft => {
draft.name = e.target.value;
});
}
function handleTitleChange(e) {
updatePerson(draft => {
draft.artwork.title = e.target.value;
});
}
function handleCityChange(e) {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}
function handleImageChange(e) {
updatePerson(draft => {
draft.artwork.image = e.target.value;
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<label>
Image:
<input
value={person.artwork.image}
onChange={handleImageChange}
/>
</label>
<p>
<i>{person.artwork.title}</i>
{' by '}
{person.name}
<br />
(located in {person.artwork.city})
</p>
<img
src={person.artwork.image}
alt={person.artwork.title}
/>
</>
);
}
4.8、更新状态中的数组
数组是另一种可以存储在状态中的可变JavaScript对象,应该将其视为只读对象。就像对象一样,当你想要更新一个存储在state中的数组时,你需要创建一个新的数组(或者复制一个现有的数组),然后设置state来使用新的数组:
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [list, setList] = useState(
initialList
);
function handleToggle(artworkId, nextSeen) {
setList(list.map(artwork => {
if (artwork.id === artworkId) {
return { ...artwork, seen: nextSeen };
} else {
return artwork;
}
}));
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={list}
onToggle={handleToggle} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
如果在代码中复制数组很繁琐,你可以使用像Immer这样的库来减少重复的代码:
import { useState } from 'react';
import { useImmer } from 'use-immer';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [list, updateList] = useImmer(initialList);
function handleToggle(artworkId, nextSeen) {
updateList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={list}
onToggle={handleToggle} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
5、管理状态
5.1、前序
随着应用程序的增长,更关注状态的组织方式以及组件之间数据的流动方式会有所帮助。冗余或重复状态是bug的常见来源。在本章中,你将学习如何很好地构建状态,如何保持状态更新逻辑的可维护性,以及如何在远程组件之间共享状态。
在本章中
如何将UI更改视为状态更改
如何组织好状态
如何“提升状态”以在组件之间共享它
如何控制状态是否被保留或重置
如何在一个函数中巩固复杂的状态逻辑
如何不“钻道具”传递信息
如何随着应用程序的增长而扩展状态管理
5.2、用状态对输入作出反应
使用React,你不需要直接从代码中修改UI。例如,您不会编写诸如“禁用按钮”、“启用按钮”、“显示成功消息”等命令。相反,您将描述组件的不同可视状态(“初始状态”、“输入状态”、“成功状态”)所希望看到的UI,然后触发状态更改以响应用户输入。这与设计师思考UI的方式类似。
这是一个使用React构建的测验表单。请注意它是如何使用status状态变量来确定是启用还是禁用提交按钮,以及是否显示成功消息。
import { useState } from 'react';
export default function Form() {
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing');
if (status === 'success') {
return <h1>That's right!</h1>
}
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
function handleTextareaChange(e) {
setAnswer(e.target.value);
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={handleTextareaChange}
disabled={status === 'submitting'}
/>
<br />
<button disabled={
answer.length === 0 ||
status === 'submitting'
}>
Submit
</button>
{error !== null &&
<p className="Error">
{error.message}
</p>
}
</form>
</>
);
}
function submitForm(answer) {
// Pretend it's hitting the network.
return new Promise((resolve, reject) => {
setTimeout(() => {
let shouldError = answer.toLowerCase() !== 'lima'
if (shouldError) {
reject(new Error('Good guess but a wrong answer. Try again!'));
} else {
resolve();
}
}, 1500);
});
}
6、逃生舱口
6.1、用ref引用值
当你想让组件“记住”一些信息,但又不希望这些信息触发新的渲染时,你可以使用ref——它就像一个秘密的“口袋”,用于在组件中存储信息!
6.1.1、用ref引用值
如何添加一个ref到你的组件
如何更新ref的值
refs和state有什么不同
如何安全地使用ref
6.1.2、向组件添加一个ref
你可以通过从React中导入useRef钩子来为你的组件添加一个ref:
import { useRef } from 'react';
在你的组件内部,调用useRef钩子并传递你想要引用的初始值作为唯一的参数。例如,下面是一个指向值0的ref:
const ref = useRef(0);
useRef返回如下对象:
{
current: 0 // The value you passed to useRef
}
你可以通过ref.current属性访问ref的当前值。这个值是可变的,这意味着您既可以读也可以写它。(这就是为什么它是React的单向数据流的“逃生舱口”——下面有更多!)
这里,按钮将在每次点击时增加ref.current:
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
ref指向一个数字,但是,像state一样,您可以指向任何东西:字符串、对象甚至函数。与state不同,ref是一个普通的JavaScript对象,具有当前属性,可以读取和修改。
请注意,组件不会随着每个增量而重新呈现。像状态一样,refs在重新渲染被React保留。但是,设置state会重新呈现组件。改变ref就不行!
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
当用户按下“Start”时,你将使用setInterval来每100毫秒更新一次时间:
import { useState } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
function handleStart() {
// Start counting.
setStartTime(Date.now());
setNow(Date.now());
setInterval(() => {
// Update the current time every 10ms.
setNow(Date.now());
}, 10);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
</>
);
}
当按下“停止”按钮时,您需要取消现有的间隔,以便它停止更新now状态变量。你可以通过调用clearInterval来做到这一点,但是你需要给它一个之前由setInterval调用在用户按下Start时返回的interval ID。您需要将interval ID保存在某个地方。因为interval ID不用于渲染,你可以把它保存在ref中:
import { useState, useRef } 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>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
当一段信息用于呈现时,请保持它的状态。当事件处理程序只需要一条信息,并且更改它不需要重新呈现时,使用ref可能更有效。
6.1.3、refs和state之间的差异
也许您认为refs似乎没有状态那么“严格”,例如,您可以改变它们,而不必总是使用状态设置函数。但在大多数情况下,您需要使用state。ref是一个“逃生口”,你不会经常需要。
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
You clicked {count} times
</button>
);
}
因为要显示计数值,所以对它使用状态值是有意义的。当使用setCount()设置计数器的值时,React会重新呈现组件,并更新屏幕以反映新的计数。
如果你试图用ref来实现这个,React永远不会重新渲染组件,所以你永远不会看到计数的变化!看看为什么点击这个按钮不会更新它的文本:
import { useRef } from 'react';
export default function Counter() {
let countRef = useRef(0);
function handleClick() {
// This doesn't re-render the component!
countRef.current = countRef.current + 1;
}
return (
<button onClick={handleClick}>
You clicked {countRef.current} times
</button>
);
}
这就是为什么在渲染期间读取ref.current会导致代码不可靠的原因。如果需要,请使用state。
6.1.4、useRef内部是如何工作的?
虽然useState和useRef都是由React提供的,但原则上useRef可以在useState之上实现。你可以想象在React内部,useRef是这样实现的:
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
在第一次渲染时,useRef返回{current: initialValue}。这个对象是由React存储的,所以在下一次渲染时将返回相同的对象。注意在这个例子中状态设置器是如何未被使用的。这是不必要的,因为useRef总是需要返回相同的对象!
React提供了内置版本的useRef,因为它在实践中足够常见。但是你可以把它看作一个没有setter的常规状态变量。如果您熟悉面向对象编程,refs可能会让您想起实例字段——但不是这样。你写的东西,参考,当前。
6.1.5、何时使用refs
通常,当组件需要“走出”React并与外部API(通常是不会影响组件外观的浏览器API)通信时,您将使用ref。以下是一些罕见的情况:
存储超时id
存储和操作DOM元素,这将在下一页讨论
存储计算JSX不需要的其他对象。
如果组件需要存储一些值,但不影响呈现逻辑,则选择refs。
6.1.6、裁判的最佳做法 、
遵循这些原则将使您的组件更具可预测性:
把refs当作逃生舱口。当您使用外部系统或浏览器api时,Refs非常有用。如果您的大部分应用程序逻辑和数据流依赖于refs,那么您可能需要重新考虑您的方法。
渲染期间不要读写ref.current。如果在呈现过程中需要某些信息,则使用state代替。由于React不知道ref.current何时发生变化,因此即使在渲染时读取它也会使组件的行为难以预测。(唯一的例外是像if (!ref.current) ref.current = new Thing()这样的代码,它只在第一次渲染时设置一次ref。)
React状态的限制不适用于refs。例如,状态就像每次呈现的快照一样,不会同步更新。但是当你改变ref的当前值时,它会立即改变:
ref.current = 5;
console.log(ref.current); // 5
这是因为ref本身是一个普通的JavaScript对象,所以它的行为就像一个。
当你使用ref时,你也不需要担心避免变异。只要你正在变异的对象不用于渲染,React就不会关心你对ref或它的内容做了什么。
6.1.7、Refs和DOM
你可以把一个ref指向任何值。然而,ref最常见的用例是访问DOM元素。例如,如果您想以编程方式refs,这就很方便。当你将一个ref传递给JSX中的ref属性时,比如<div ref={myRef}>, React会将相应的DOM元素放入myRef.current中。您可以在使用Refs操纵DOM中了解更多相关内容。
6.1.8、回顾
Refs是一个逃生舱口,用于保存不用于渲染的值。你不会经常用到它们。
ref是一个普通的JavaScript对象,只有一个名为current的属性,可以读取或设置。
你可以通过调用useRef钩子让React给你一个ref。
像状态一样,refs让您在组件的重新呈现之间保留信息。
与state不同,设置ref的当前值不会触发重新呈现。
渲染期间不要读写ref.current。这使得您的组件难以预测
6.1.9、尝试一些挑战
修复了一个破碎的聊天输入
输入一条消息,然后点击“发送”。您会注意到,在看到“发送!”“警报。在此延迟期间,您可以看到“撤消”按钮。点击它。这个“撤销”按钮是用来停止“发送!”的信息不会出现。它通过为handleSend期间保存的超时ID调用clearartimeout来实现这一点。然而,即使点击了“撤销”,“发送!”消息仍然出现。找出它不起作用的原因,然后修复它。
import { useState } from 'react';
export default function Chat() {
const [text, setText] = useState('');
const [isSending, setIsSending] = useState(false);
let timeoutID = null;
function handleSend() {
setIsSending(true);
timeoutID = setTimeout(() => {
alert('Sent!');
setIsSending(false);
}, 3000);
}
function handleUndo() {
setIsSending(false);
clearTimeout(timeoutID);
}
return (
<>
<input
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<button
disabled={isSending}
onClick={handleSend}>
{isSending ? 'Sending...' : 'Send'}
</button>
{isSending &&
<button onClick={handleUndo}>
Undo
</button>
}
</>
);
}
6.1.10、修复了组件无法重新渲染的问题
这个按钮应该在显示“开”和“关”之间切换。然而,它总是显示“Off”。这段代码有什么问题?解决它。
import { useRef } from 'react';
export default function Toggle() {
const isOnRef = useRef(false);
return (
<button onClick={() => {
isOnRef.current = !isOnRef.current;
}}>
{isOnRef.current ? 'On' : 'Off'}
</button>
);
}
6.1.11、解决消除抖动
在这个例子中,所有的按钮点击处理程序都是“公开的”。要了解这是什么意思,请按下其中一个按钮。请注意消息是如何在一秒钟后出现的。如果您在等待消息时按下按钮,计时器将重置。因此,如果您快速点击同一个按钮多次,消息将不会出现,直到一秒钟后,你停止点击。贬低可以让你延迟一些动作,直到用户“停止做事情”。
这个例子可以工作,但并不完全像预期的那样。这些按钮不是独立的。要查看问题,请单击其中一个按钮,然后立即单击另一个按钮。您期望在延迟之后,您将看到两个按钮的消息。但是只有最后一个按钮的信息显示出来。第一个按钮的信息丢失了。
import { useState } from 'react';
let timeoutID;
function DebouncedButton({ onClick, children }) {
return (
<button onClick={() => {
clearTimeout(timeoutID);
timeoutID = setTimeout(() => {
onClick();
}, 1000);
}}>
{children}
</button>
);
}
export default function Dashboard() {
return (
<>
<DebouncedButton
onClick={() => alert('Spaceship launched!')}
>
Launch the spaceship
</DebouncedButton>
<DebouncedButton
onClick={() => alert('Soup boiled!')}
>
Boil the soup
</DebouncedButton>
<DebouncedButton
onClick={() => alert('Lullaby sung!')}
>
Sing a lullaby
</DebouncedButton>
</>
)
}
6.1.12、 阅读最新状态
在本例中,按下“发送”后,在显示消息之前会有一小段延迟。输入“hello”,按发送,然后再次快速编辑输入。尽管您进行了编辑,警报仍然会显示“hello”(这是单击按钮时state的值)。
通常,这种行为是你在应用程序中想要的。然而,可能偶尔会有一些情况,你想要一些异步代码读取一些状态的最新版本。您能想到一种方法使警报显示当前输入文本,而不是单击时的文本吗?
import { useState, useRef } from 'react';
export default function Chat() {
const [text, setText] = useState('');
function handleSend() {
setTimeout(() => {
alert('Sending: ' + text);
}, 3000);
}
return (
<>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<button
onClick={handleSend}>
Send
</button>
</>
);
}
6.2 用ref操纵DOM
用ref操纵DOM
因为React处理更新DOM以匹配呈现输出,所以组件不需要经常操作DOM。然而,有时您可能需要访问由react管理的DOM元素——例如,聚焦一个节点、滚动到它,或者测量它的大小和位置。在React中没有内置的方法来做这些事情,所以你需要一个引用到DOM节点。
6.2.1、你会学到
如何访问由React管理的DOM节点与ref属性
ref JSX属性如何与useRef钩子相关
如何访问另一个组件的DOM节点
在这种情况下,修改由React管理的DOM是安全的
6.2.2、获取对节点的引用
要访问由React管理的DOM节点,首先,导入useRef钩子:
import { useRef } from 'react';
然后,用它在你的组件中声明一个ref:
const myRef = useRef(null);
最后,将其作为ref属性传递给DOM节点:
<div ref={myRef}>
useRef钩子返回一个只有一个属性current的对象。最初,myRef。Current将为空。当React为这个<div>创建一个DOM节点时,React会把对这个节点的引用放到myRef.current中。然后,您可以从事件处理程序访问该DOM节点,并使用在其上定义的内置浏览器api。
myRef.current.scrollIntoView();
6.2.3、示例:聚焦文本输入
在本例中,单击按钮将聚焦输入
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
要实现这一点:
用useRef钩子声明inputRef。
将其作为<input ref={inputRef}>传递。这告诉React把这个<input>的DOM节点放到inputRef.current中。
在handleClick函数中,从inputRef中读取输入DOM节点。使用inputRef.current.focus()对其调用focus()。
通过onClick将handleClick事件处理程序传递给<button>。
虽然DOM操作是refs最常见的用例,但useRef Hook可以用于存储React之外的其他东西,比如计时器id。与状态类似,渲染之间的refes仍然存在。你甚至可以将refs视为状态变量,当你设置它们时不会触发重新渲染!你可以在引用参考值中了解更多关于参考值的信息。
6.2.4、示例:滚动到一个元素
一个组件中可以有多个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>
</>
);
}
6.2.5、如何使用refcallback管理refs列表
在上面的例子中,有一个预定义的引用数。然而,有时你可能需要参考列表中的每个项目,而你不知道你将有多少。像这样的事情是行不通的:
<ul>
{items.map((item) => {
// Doesn't work!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
这是因为Hooks只能在组件的顶层被调用。不能在循环、条件或map()调用中调用useRef。
相反,解决方案是将一个函数传递给ref属性。这被称为“ref回调”。当需要设置ref时,React将使用DOM节点调用ref回调,而当需要清除它时,React将使用null。这允许您维护自己的数组或Map,并通过索引或某种ID访问任何ref。
这个例子展示了如何使用这种方法滚动到长列表中的任意节点:
import { useRef } from 'react';
export default function CatFriends() {
const itemsRef = useRef(null);
function scrollToId(itemId) {
const map = getMap();
const node = map.get(itemId);
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function getMap() {
if (!itemsRef.current) {
// Initialize the Map on first usage.
itemsRef.current = new Map();
}
return itemsRef.current;
}
return (
<>
<nav>
<button onClick={() => scrollToId(0)}>
Tom
</button>
<button onClick={() => scrollToId(5)}>
Maru
</button>
<button onClick={() => scrollToId(9)}>
Jellylorum
</button>
</nav>
<div>
<ul>
{catList.map(cat => (
<li
key={cat.id}
ref={(node) => {
const map = getMap();
if (node) {
map.set(cat.id, node);
} else {
map.delete(cat.id);
}
}}
>
<img
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
}
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({
id: i,
imageUrl: 'https://placekitten.com/250/200?image=' + i
});
}
在这个例子中,itemsRef没有保存一个DOM节点。相反,它保存一个从项目ID到DOM节点的Map。(ref可以保存任何值!)每个列表项上的ref回调都负责更新Map:
<li
key={cat.id}
ref={node => {
const map = getMap();
if (node) {
// Add to the Map
map.set(cat.id, node);
} else {
// Remove from the Map
map.delete(cat.id);
}
}}
>
这允许稍后从Map中读取单个DOM节点。
6.2.6、访问另一个组件的DOM节点
当你在一个输出浏览器元素(如<input />)的内置组件上放置ref时,React会将该ref的当前属性设置为相应的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}>
Focus the input
</button>
</>
);
}
6.2.7、单击该按钮将向控制台打印一条错误消息:
警告:函数组件不能给出refs。尝试访问此ref将失败。你的意思是使用React.forwardRef()吗?
这是因为在默认情况下,React不允许组件访问其他组件的DOM节点。甚至连它自己的孩子也没有!这是有意为之。Refs是一个逃生舱口,应该谨慎使用。手动操作另一个组件的DOM节点会使您的代码更加脆弱。
相反,希望公开其DOM节点的组件必须选择加入该行为。组件可以指定它将其ref“转发”给它的一个子组件。下面是MyInput如何使用forwardRef API:
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
它是这样工作的:
<MyInput ref={inputRef} />告诉React将相应的DOM节点放入inputRef.current中。然而,这取决于MyInput组件是否选择这样做——默认情况下,它不会这样做。
MyInput组件是使用forwardRef声明的。它选择接收上面的inputRef作为第二个ref参数,在props之后声明。
MyInput本身将接收到的ref传递给它内部的<input>。
现在点击按钮聚焦输入工作:
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
在设计系统中,低级组件(如按钮、输入等)将其引用转发到其DOM节点是一种常见模式。另一方面,表单、列表或页面部分等高级组件通常不会公开其DOM节点,以避免对DOM结构的意外依赖。
6.2.8、使用命令式句柄公开API的子集
在上面的示例中,MyInput公开了原始DOM输入元素。这允许父组件调用focus()。但是,这也允许父组件做其他事情—例如,更改其CSS样式。在不常见的情况下,您可能希望限制公开的功能。你可以用useImperativeHandle:
import {
forwardRef,
useRef,
useImperativeHandle
} from 'react';
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose focus and nothing else
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
这里,MyInput中的realInputRef保存了实际的输入DOM节点。然而,useImperativeHandle指示React提供你自己的特殊对象作为父组件的ref值。所以inputRef。当前的Form组件将只有focus方法。在这种情况下,ref " handle "不是DOM节点,而是在useImperativeHandle调用中创建的自定义对象。
6.2.9、当React附加refs时
在React中,每次更新都分为两个阶段:
在渲染过程中,React调用组件来确定屏幕上应该显示什么。
在提交期间,React将更改应用到DOM。
一般来说,你不希望在渲染期间访问refs。这也适用于持有DOM节点的refs。在第一次呈现期间,DOM节点尚未创建,因此ref.current将为空。在呈现更新时,DOM节点还没有更新。所以现在读还为时过早。
React在提交时设置ref.current。在更新DOM之前,React会将受影响的ref.current值设置为null。更新DOM后,React立即将它们设置为相应的DOM节点。
通常,您将从事件处理程序访问refs。如果你想用ref做一些事情,但是没有特定的事件来做,你可能需要一个effect。我们将在下一页讨论效果。
6.2.10、刷新状态与flushSync同步更新
考虑这样的代码,它添加了一个新的todo,并将屏幕向下滚动到列表的最后一个子元素。注意,由于某种原因,它总是滚动到上一个添加的todo之前的todo:
import { useState, useRef } from 'react';
export default function TodoList() {
const listRef = useRef(null);
const [text, setText] = useState('');
const [todos, setTodos] = useState(
initialTodos
);
function handleAdd() {
const newTodo = { id: nextId++, text: text };
setText('');
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
return (
<>
<button onClick={handleAdd}>
Add
</button>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<ul ref={listRef}>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}
let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
initialTodos.push({
id: nextId++,
text: 'Todo #' + (i + 1)
});
}
问题在于这两行:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
在React中,状态更新是排队的。通常,这就是你想要的。但是,这里它会导致一个问题,因为setTodos不会立即更新DOM。因此,当您滚动到列表的最后一个元素时,待办事项还没有添加。这就是为什么滚动总是“滞后”一个项目。
为了解决这个问题,你可以强制React同步更新(“刷新”)DOM。要做到这一点,需要从react-dom中导入flushSync,并将状态更新封装到flushSync调用中:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
这将指示React在执行flushSync封装的代码后同步更新DOM。因此,当您尝试滚动到它时,最后一个待办事项将已经在DOM中:
import { useState, useRef } from 'react';
import { flushSync } from 'react-dom';
export default function TodoList() {
const listRef = useRef(null);
const [text, setText] = useState('');
const [todos, setTodos] = useState(
initialTodos
);
function handleAdd() {
const newTodo = { id: nextId++, text: text };
flushSync(() => {
setText('');
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
return (
<>
<button onClick={handleAdd}>
Add
</button>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<ul ref={listRef}>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}
let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
initialTodos.push({
id: nextId++,
text: 'Todo #' + (i + 1)
});
}
6.2.11、带refs的DOM操作的最佳实践
refs是一个逃生舱口。你应该只在必须“走出React”的时候使用它们。这方面的常见示例包括管理焦点、滚动位置或调用React不公开的浏览器api。
如果你坚持使用非破坏性的操作,比如聚焦和滚动,你应该不会遇到任何问题。但是,如果您尝试手动修改DOM,就有可能与React所做的更改发生冲突。
为了说明这个问题,这个示例包括一个欢迎消息和两个按钮。第一个按钮使用条件呈现和状态切换它的存在,就像你通常在React中做的那样。第二个按钮使用remove() DOM API强制将其从React控制之外的DOM中移除。
import {useState, useRef} from 'react';
export default function Counter() {
const [show, setShow] = useState(true);
const ref = useRef(null);
return (
<div>
<button
onClick={() => {
setShow(!show);
}}>
Toggle with setState
</button>
<button
onClick={() => {
ref.current.remove();
}}>
Remove from the DOM
</button>
{show && <p ref={ref}>Hello world</p>}
</div>
);
}
在手动删除DOM元素之后,试图使用setState再次显示它将导致崩溃。这是因为您已经更改了DOM,而React不知道如何继续正确地管理它。
避免更改由React管理的DOM节点。修改、添加或删除由React管理的元素中的子元素可能会导致不一致的视觉结果或如上所述的崩溃。
然而,这并不意味着你根本做不到。这需要谨慎。你可以安全地修改DOM中React没有必要更新的部分。例如,如果一些<div>在JSX中总是空的,React就没有理由去碰它的子列表。因此,手动添加或删除元素是安全的。
6.2.12 回顾
ref是一个通用概念,但大多数情况下,您将使用它们来保存DOM元素。
你指示React把一个DOM节点放到myRef中。当前通过传递<div ref={myRef}>。
通常,您将在聚焦、滚动或测量DOM元素等非破坏性操作中使用refs。
默认情况下,组件不公开其DOM节点。您可以通过使用forwardRef并将第二个ref参数传递给特定节点来选择公开DOM节点。
避免更改由React管理的DOM节点。
如果要修改由React管理的DOM节点,请修改React没有理由更新的部分。
6.2.13 尝试一些挑战
挑战1 / 4:
播放和暂停视频
在这个例子中,按钮切换一个状态变量,在播放状态和暂停状态之间切换。然而,为了真正播放或暂停视频,切换状态是不够的。还需要在<video>的DOM元素上调用play()和pause()。向它添加一个ref,并使按钮工作。
import { useState, useRef } from 'react';
export default function VideoPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
function handleClick() {
const nextIsPlaying = !isPlaying;
setIsPlaying(nextIsPlaying);
}
return (
<>
<video width="250">
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
type="video/mp4"
/>
</video>
<button onClick={handleClick}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</>
)
}
挑战2 / 4:聚焦搜索领域
这样,点击“搜索”按钮就可以将焦点放在字段中。
export default function Page() {
return (
<>
<nav>
<button>Search</button>
</nav>
<input
placeholder="Looking for something?"
/>
</>
);
}
挑战3 / 4:滚动图像旋转木马
这个图像旋转木马有一个“下一步”按钮,用于切换活动图像。使图库水平滚动到活动图像单击。你需要在活动图像的DOM节点上调用scrollIntoView():
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
import { useState } from 'react';
export default function CatFriends() {
const [index, setIndex] = useState(0);
return (
<>
<nav>
<button onClick={() => {
if (index < catList.length - 1) {
setIndex(index + 1);
} else {
setIndex(0);
}
}}>
Next
</button>
</nav>
<div>
<ul>
{catList.map((cat, i) => (
<li key={cat.id}>
<img
className={
index === i ?
'active' :
''
}
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
}
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({
id: i,
imageUrl: 'https://placekitten.com/250/200?image=' + i
});
}
Challenge 4 of 4:
Focus the search field with separate components
Make it so that clicking the “Search” button puts focus into the field. Note that each component is defined in a separate file and shouldn’t be moved out of it. How do you connect them together?
import SearchButton from './SearchButton.js';
import SearchInput from './SearchInput.js';
export default function Page() {
return (
<>
<nav>
<SearchButton />
</nav>
<SearchInput />
</>
);
}
export default function SearchButton() {
return (
<button>
Search
</button>
);
}
export default function SearchInput() {
return (
<input
placeholder="Looking for something?"
/>
);
}