Open WangShuXian6 opened 1 month ago
要在 HTML 中实现一个 "Hello World",其实并不复杂。你只需要创建一个简单的 HTML 文档,使用 <body>
标签和一个包含 “Hello World” 的 <div>
或者 <p>
标签。
但如果你想通过 JavaScript 实现 "Hello World",也不会特别复杂。这是你第一次接触如何使用 JavaScript 与页面互动,并创建动态和交互式用户体验的入口。
要在页面上添加 JavaScript,可以使用 <script>
标签。你可以直接在这个标签内编写 JavaScript 代码,或者使用 src
属性链接到一个外部的 JavaScript 文件。虽然我们通常会将 JavaScript 代码放在单独的文件中,但为了简单起见,在初步学习中,我们会将所有内容放在一个文件里。
在这个练习中,我们的重点是学习以下内容:
可以通过访问 MDN DOM 文档 来了解更多关于 DOM 的信息。DOM 是从 JavaScript 在 Web 诞生之初就存在的一部分,通过 DOM,我们能够与页面进行交互。
尽管直接操作 DOM 可能有点混乱,但现代 JavaScript 和 DOM 的操作已经变得非常简洁高效。你可以使用这些技术构建一些不错的用户体验。当然,像 React 这样的框架提供了更多的功能,帮助我们更方便地进行复杂应用的开发,但在这个练习中,我们的重点是了解 DOM 的基础知识。
通过本次练习,你将学会使用 JavaScript 直接在 DOM 上实现 "Hello World",为之后深入学习 React 和其他高级工具打下坚实的基础。
在本次练习的第一步中,我们将使用 JavaScript 在 HTML 中创建一个 "Hello World"。当你完成后,页面应简单显示 "Hello World",如同示例中那样。
你可以按照以下步骤进行操作:
index.html
。<script>
标签,编写或引用 JavaScript 代码,将 "Hello World" 添加到页面。这是一项非常基础的练习,指引会帮助你完成,祝你玩得开心!
这个视频讲解了如何使用JavaScript和DOM API在HTML中创建一个“Hello World”的示例。以下是关键步骤的总结:
基本HTML结构:
div
元素的HTML文件。<script>
标签,并将type
设为module
,用来嵌入JavaScript代码。使用JavaScript操作DOM:
<script>
标签中,使用document.getElementById('root')
选择页面上的root元素。document.createElement('div')
创建一个新的div
元素。div
元素添加一个className
(例如:container
),并设置其textContent
为“Hello World”。appendChild()
或append()
将新创建的div
元素追加到root元素中。DOM操作:
通过这些步骤,你可以使用JavaScript和DOM操作技术,动态地在网页上创建并追加元素,实现“Hello World”的显示。
这段视频介绍了一个新的练习,目的是让你更深入地理解如何使用JavaScript操作DOM。与之前的练习不同,这次我们将完全用JavaScript生成根元素,而不是依赖于HTML预先定义的元素。以下是视频的主要内容:
挑战的目标:
<div>
),而不是在HTML中预先定义。任务内容:
document.createElement
动态生成一个根元素,并将其插入到文档中。动手实验:
练习的核心是完全通过JavaScript操控页面结构,摆脱对HTML的依赖,增强对动态DOM操作的理解。
在这段视频中,主要讲解了如何使用JavaScript动态创建并添加HTML元素到DOM中,以下是视频的核心内容总结:
删除现有元素:
cannot read properties of null reading append
”,因为在DOM中找不到该元素。使用JavaScript创建元素:
document.createElement
创建一个新的div
元素,并设置其id
为"root"。将新元素添加到DOM中:
document.body.append
方法,将动态创建的div
元素(rootElement
)添加到body
中,使其真正显示在页面上。document.body.prepend
,而不是append
。DOM的动态操作:
这段视频的目的是展示如何通过JavaScript操控DOM,创建并修改页面元素,帮助理解如何在动态网页中使用JavaScript进行基本的DOM操作。
在这段视频中,介绍了一个轻松的休息时间。讲者强调了在学习过程中定期休息的重要性,帮助大脑吸收和巩固所学内容。视频还分享了一个轻松的笑话:
之后,讲者提醒观众保持充足的水分,并鼓励大家利用这段时间稍作休息,为接下来的学习做好准备。
这段视频介绍了如何在引入React的基础上,通过不使用JSX的方式,使用原生React API创建一个简单的"Hello World"应用。讲者首先解释了React是基于传统的document.createElement
构建的,目的是让开发者理解React背后的工作原理。以下是视频中的一些关键点:
React与React DOM:讲者介绍了React不仅仅是一个单一的包,它实际上分为两个主要的包:React
和React DOM
。React
负责管理组件、hooks和API,而React DOM
则负责将React组件渲染到网页的DOM上。
React的跨平台能力:除了网页上的React渲染器(React DOM),React还可以用于虚拟现实、原生桌面应用和命令行界面等场景。
从原生DOM到React的转换:通过引入React和React DOM,开发者能够将React元素转换为可以在网页上显示的DOM元素。
视频的目的是引导开发者通过逐步学习React的底层API,最终理解React如何简化开发工作,并逐步深入React的使用。
在这个练习中,我们将React和React DOM引入页面,并使用它们的createElement
和createRoot
API来替代原生的document.createElement
等操作。讲者提醒,这并不是通常引入React的方式,通常你会使用构建工具(如Webpack、Parcel)来处理React的构建和优化工作,包括类型检查、打包和性能优化等。
在这个简单的练习中,我们的目标是通过React的声明式API(如React.createElement
)来实现之前使用原生DOM API的效果。尽管最后页面的展示效果与之前完全相同,但我们将会使用React来构建整个页面结构。这是从传统DOM操作向React世界过渡的第一步。
你可以通过查看项目仓库中的公共目录来了解如何加载React和React DOM库,并使用它们来创建页面元素。希望你在这次练习中有所收获!
在这个示例中,我们展示了如何使用React和React DOM将元素渲染到页面上。首先,我们通过createElement
从React创建一个新的元素,并设置className
和children
属性来替代传统的class
和textContent
。
React元素只是一个UI描述符,它不是直接的DOM元素。因此,我们还需要通过React DOM中的createRoot
API来将这个React元素渲染为实际的DOM元素。以下是我们所做的关键步骤:
createElement
:我们用React的createElement
来创建一个元素。在这个例子中,我们创建了一个div
,并给它设置了className
和children
属性。createRoot
渲染元素:通过createRoot
API,我们将这个元素附加到页面上已经存在的根节点rootElement
。然后通过render
方法将React元素实际渲染到DOM中。最后的效果是在页面上显示一个带有"Hello World"文本的div
元素。
这是React基本工作原理的一个很好的演示,展示了从UI描述符(React元素)到实际DOM渲染的流程。
在这个练习中,我们将处理多个子元素并确保它们之间的空格正确显示。具体来说,我们会在一个React元素中创建两个span
标签,一个用于显示"Hello",另一个用于显示"World",并确保它们之间有一个空格。
span
元素,一个显示"Hello",另一个显示"World"。React允许我们将多个子元素传递给一个父元素,而不仅限于单个子元素。要实现这个需求,我们可以通过创建两个span
元素并在它们之间添加一个空格或字符串来实现。
示例代码如下:
import React from 'react';
import ReactDOM from 'react-dom/client';
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
const element = React.createElement(
'div',
null,
React.createElement('span', null, 'Hello'),
' ', // 这是我们用来添加空格的部分
React.createElement('span', null, 'World')
);
root.render(element);
React.createElement
: 使用该方法分别创建两个span
元素,分别包含"Hello"和"World"。span
元素之间直接添加一个字符串' '
(一个空格字符),确保在它们之间有空格。最终效果是页面上显示"Hello World",并且"Hello"和"World"之间有一个空格。
这个练习展示了如何在React中使用多个子元素,并处理它们之间的布局问题。
在这个练习中,我们使用 React 的 createElement
API 来构建嵌套的 UI,并处理多个子元素。这次的目标是创建两个 span
元素,一个显示“Hello”,另一个显示“World”,并确保它们之间有空格。
使用 React.createElement
创建 span
元素:
React.createElement('span', null, 'Hello')
创建第一个 span
,内容为“Hello”。span
,内容为“World”。在 span
元素之间添加空格:
' '
,React 将确保这个空格被渲染为文本节点,从而在两个 span
元素之间显示空格。将所有元素作为子元素传递:
React.createElement
的第三个参数开始表示子元素,可以是文本、元素或数组形式。在这个例子中,我们通过传递多个子元素来构建 div
标签的内容。以下是完整代码:
import React from 'react';
import ReactDOM from 'react-dom/client';
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
const element = React.createElement(
'div',
null,
React.createElement('span', null, 'Hello'),
' ', // 这是用来添加空格的字符串
React.createElement('span', null, 'World')
);
root.render(element);
React.createElement('span', null, 'Hello')
: 创建一个 span
元素,内容为 "Hello"。' '
: 这是一个空格字符串,它被作为子元素传递,使两个 span
元素之间有一个空格。React.createElement('span', null, 'World')
: 创建第二个 span
元素,内容为 "World"。' '
来表示空格。React.createElement
接受多个子元素作为第三个及后续参数。它们会一起渲染到父元素中。通过这种方式,我们可以确保在页面上正确显示“Hello World”,并且“Hello”和“World”之间有一个空格。
在这个练习中,我们将使用 React.createElement
来构建一个更加复杂的嵌套结构。最终的目标是创建一个包含 p
标签、一个 ul
列表及其多个 li
项的结构。当我们完成时,DOM 树应看起来如下:
<div class="container">
<p>Some text here</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
createElement
构建整个嵌套结构。import React from 'react';
import ReactDOM from 'react-dom/client';
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
// 创建包含嵌套元素的结构
const element = React.createElement(
'div',
{ className: 'container' }, // 父 div 容器
React.createElement('p', null, 'Some text here'), // p 标签
React.createElement(
'ul',
null,
React.createElement('li', null, 'Item 1'), // li 项目1
React.createElement('li', null, 'Item 2'), // li 项目2
React.createElement('li', null, 'Item 3') // li 项目3
)
);
// 渲染到页面
root.render(element);
顶层 div
元素:
React.createElement('div', { className: 'container' }, ...)
创建一个 div
,并赋予 className
为 "container"。p
标签:
React.createElement('p', null, 'Some text here')
创建一个 p
标签,包含文本内容 "Some text here"。ul
列表和 li
项目:
React.createElement('ul', null, ...)
创建一个 ul
列表,并在其中嵌套多个 li
项目。嵌套结构:
ul
列表的每个 li
项是通过调用 React.createElement('li', null, 'Item X')
创建的,X
代表不同的项目编号。虽然这个结构不算特别复杂,但通过这种方式嵌套多个 createElement
调用,可以让我们深刻体会到 JSX 的优势。直接使用 createElement
来构建复杂的 UI 结构时,代码的可读性会变差,层次感也不明显。而在 JSX 中,嵌套结构可以以更接近 HTML 的方式书写,既直观又高效。
这个练习让我们理解 React.createElement
的灵活性和强大之处,同时也预示了 JSX 如何简化开发体验。
在这个练习中,我们使用了 React.createElement
来创建更复杂的嵌套 UI 结构。与之前的 Hello World
相比,我们现在正在构建一个包含 <p>
标签、<ul>
列表和多个 <li>
项的结构。这个结构展示了 React 元素嵌套的工作方式,但同时也让我们理解到 JSX 在处理大量嵌套时的优势。
import React from 'react';
import ReactDOM from 'react-dom/client';
// 获取 root 元素
const rootElement = document.getElementById('root');
const root = ReactDOM.createRoot(rootElement);
// 使用 createElement 创建嵌套的 UI 结构
const element = React.createElement(
'div',
{ className: 'container' },
React.createElement('p', null, "Sam's favorite food"), // p 标签
React.createElement(
'ul',
{ className: 'sams-food' }, // ul 列表,带有 className 属性
React.createElement('li', null, 'Green eggs'), // li 项目1
React.createElement('li', null, 'Ham') // li 项目2
)
);
// 渲染到页面
root.render(element);
p
标签:
React.createElement('p', null, "Sam's favorite food")
创建一个 p
标签,显示文本内容为 "Sam's favorite food"。ul
列表:
React.createElement('ul', { className: 'sams-food' }, ...)
创建一个带有 className
为 sams-food
的 ul
列表,并在其中嵌套多个 li
项。li
项目:
React.createElement('li', null, 'Green eggs')
和 React.createElement('li', null, 'Ham')
分别创建两个列表项,显示 "Green eggs" 和 "Ham"。虽然 React.createElement
提供了创建复杂 UI 的能力,但随着嵌套层级增加,代码的可读性和维护性可能会变差。这就是 JSX 存在的主要原因:它让我们用类似 HTML 的语法来编写 React 元素,大大提高了代码的可读性和简洁性。
通过这个练习,你已经学会了如何使用 React.createElement
创建嵌套的 UI 结构,同时也体验到当 UI 复杂度增加时,JSX 的优势会变得非常明显。
哈哈,那个笑话真是让人忍俊不禁!抓住一个放了一个,真是个有趣的双关。希望这个小休息能让你放松一下,记得保持水分和适当休息哦!当你准备好了,我们会继续学习更多的 React 知识,深入探索这个强大的框架,掌握更多有趣的技巧!
JSX 确实为 React 编写用户界面带来了很大的方便,它使得我们可以以接近 HTML 的语法编写代码,而不必通过复杂的 createElement
API 一层层嵌套调用。
React 团队创建了 JSX 语法,它本质上是一种让你在 JavaScript 中写 XML 类似的语法,JSX 然后会通过编译器(如 Babel)转换为 React 的 createElement
调用。虽然浏览器本身不理解 JSX 语法,但是通过像 Babel 这样的工具,JSX 可以被编译成常规的 JavaScript,从而在浏览器中运行。
为了保持事情简单,React 的 JSX 编译器 Babel 可以在浏览器中直接运行,因此我们可以避免使用复杂的构建工具。在本次练习中,你将学习如何在浏览器中通过引入 Babel 来编写 JSX 语法,并了解 JSX 是如何转换为 React.createElement
调用的。
在这项练习中,你将体验到 JSX 的强大和简洁性,让编写 UI 变得更加高效流畅。准备好了吗?让我们一起开始学习 JSX 吧!
在这个练习中,我们将把 Babel 添加到页面中,并将 createElement
调用转换为 JSX 语法。
首先,我们需要在页面中引入 Babel,这次我们会使用 Babel 的单独版本(Babel standalone),它是一个打包好的单个脚本,可以直接在浏览器中加载并执行。通过这个脚本,Babel 会在页面中查找具有特定 type
类型的 <script>
标签,并对其内容进行编译,最终生成新的脚本标签让浏览器评估执行。
具体步骤如下:
加载 Babel standalone:
<script>
标签将其引入页面。更新 <script>
标签:
<script>
标签的 type
属性设置为 "text/babel"
,这会告诉 Babel,它应该编译这个脚本中的 JSX 内容。转换 JSX:
createElement
API 替换为 JSX 语法。Babel 会自动将 JSX 转换为对应的 React.createElement
调用。这个过程让你能够直观地了解 JSX 如何转换为常规的 JavaScript 调用,虽然这种方式不常用于生产环境,但它能帮助你在没有构建工具的情况下运行 React 项目。
祝你练习愉快!
在这个练习中,我们已经成功地将 Babel 添加到页面中,并且将 createElement
调用转换为了 JSX 语法。通过这个过程,我们学到了几个关键点:
添加 Babel: Babel 是一个编译器,可以将 JSX 代码转换为 JavaScript 代码,让浏览器能够理解和执行。虽然我们在练习中加载了一个较大的 Babel standalone 文件,这种方法不适合生产环境,但它适合我们进行开发和学习。
使用 JSX 替代 createElement
调用:
我们将 JSX 替换了原本的 React.createElement
调用。JSX 语法更接近 HTML,简洁直观,可以更容易地编写 React 组件。
Babel 编译:
Babel 会自动查找类型为 text/babel
的 <script>
标签,编译其中的内容,并将其转化为浏览器可以执行的 JavaScript 代码。在生产环境中,你通常会使用编译工具(如 Webpack 等)来实现这一过程。
模块导入:
Babel 编译后的代码依赖于 React.createElement
,所以我们需要确保在代码中引入 React。通过 import * as React from 'react';
,我们可以确保 React 被正确导入和引用。
JSX 的好处:
使用 JSX 语法,让我们可以轻松地嵌套和构建复杂的 UI 元素,而不必手动编写繁琐的 createElement
调用。这样可以大大提高开发效率和代码的可读性。
接下来,我们可以继续探索 JSX 的更多特性和强大之处,并且通过这些练习加深对 React 的理解。
在这个练习中,我们讨论了插值(Interpolation)的概念,特别是在 JSX 中的应用。在 React 和 JSX 中,插值是将 JavaScript 代码嵌入到 JSX 表达式中的一种方式,这与在模板字符串(Template Literal)中的插值非常类似。
插值的概念:
插值的核心思想是允许在字符串或 JSX 中嵌入 JavaScript 表达式。通过这种方式,我们可以动态地生成内容。例如,使用反引号 (`
) 和 ${}
在模板字符串中嵌入变量或表达式,这与在 JSX 中使用花括号 {}
插入 JavaScript 表达式类似。
JSX 中的插值:
在 JSX 中,你可以通过花括号 {}
在 HTML 标签的属性和内容中嵌入 JavaScript 表达式。例如:
const name = "React";
return <div className={`container ${name}`}>Hello, {name}!</div>;
在这个例子中,className
和 div
的内容都使用了插值,将 JavaScript 表达式插入 JSX。
插值位置:
className={classNameVariable}
。<div>{message}</div>
,其中 message
是一个变量。JSX 和 JavaScript 模式的切换:
在 JSX 中使用 {}
插入 JavaScript 表达式相当于在普通 JavaScript 中使用 ${}
插入变量或表达式。花括号 {}
是一种告诉 JSX 解析器 "进入 JavaScript 模式" 的方式,解析器会解析 {}
中的表达式,然后返回其结果并插入到 DOM 中。
插值是 React 和 JSX 的重要特性,它使得我们可以轻松地将动态数据插入到组件中,使得我们的 UI 更加灵活和可复用。在接下来的练习中,你将能够实践这些插值的技巧,从而进一步掌握它们的应用。
在这个练习中,我们讨论了如何在 JSX 中进行插值(Interpolation),特别是在 JSX 和 JavaScript 之间切换的方式。
JSX 与 JavaScript 的切换:
在 JSX 中,通过使用 {}
可以将 JavaScript 表达式嵌入到 JSX 中。当 JSX 编译时,任何在 {}
中的内容都会被当作 JavaScript 表达式进行评估,并插入到对应的位置。例如:
const className = "container";
const children = "Hello World";
return <div className={className}>{children}</div>;
这里的 className
和 children
都是通过插值的方式传递给 JSX 元素。
插值的语法:
className={className}
。<div>{children}</div>
。切换状态:
<div></div>
等 JSX 语法时,React 正在解析类似于 HTML 的代码。{}
包裹表达式时,解析器会将其转换为 JavaScript 代码,执行后插入结果。自闭合标签:
在 JSX 中可以使用自闭合标签,例如 <img />
或 <input />
,这是 JSX 的一项简化功能,它允许你在没有子元素时不必书写结束标签。
表达式与逻辑:
if
或 for
语句,但可以使用三元运算符来控制逻辑:
{isTrue ? <p>True</p> : <p>False</p>}
通过这些概念的掌握,你将能够更好地在 JSX 中处理动态内容和属性,使得你的 React 应用更加灵活和强大。
在这个练习中,我们介绍了如何将一个包含多个属性的对象应用到 JSX 元素上,而无需单独为每个属性进行手动设置。你可能会经常遇到这种情况,尤其是在属性动态生成或者属性数量较多的场景下。
假设我们有一个 props
对象,其中包含了 className
和 children
等属性。常规的做法是这样写:
const props = {
className: "container",
children: "Hello World",
};
return (
<div className={props.className}>
{props.children}
</div>
);
这需要你逐个指定对象中的属性,例如 className={props.className}
,children={props.children}
。
为了避免手动传递每个属性,JSX 提供了一种更简洁的方法,即 属性扩展语法,类似于 JavaScript 中的 扩展操作符。你可以通过 ...props
将对象中的所有属性应用到 JSX 元素上:
const props = {
className: "container",
children: "Hello World",
};
return (
<div {...props} />
);
...props
会将 props
对象中的每个键值对分别扩展为该元素的属性。上面的代码与以下手动传递属性的代码是等效的:
<div className="container">Hello World</div>
这种写法特别适合以下场景:
通过使用这种方式,你可以更轻松地处理具有多个属性的 JSX 元素。
在这个练习中,我们探讨了如何在 JSX 中使用扩展语法将一个对象中的所有属性应用到某个元素上。这种方式对于处理动态属性或者复杂的属性集合时非常有用。
我们可以使用 ...props
将一个对象中的所有属性“展开”并应用到 JSX 元素上。例如:
const props = {
className: "container",
children: "Hello World",
};
return <div {...props} />;
这段代码会将 props
对象中的 className
和 children
属性直接应用到 div
元素上,等效于手动指定这些属性:
<div className="container">Hello World</div>
扩展操作符的一个关键点是属性覆盖。如果你在展开属性对象后又指定了同名的属性,那么 JSX 会采用后面定义的属性值。例如:
const props = {
className: "container",
children: "Hello World",
};
return <div {...props} className="my-container" />;
在这个例子中,className="my-container"
会覆盖 props
中的 className="container"
,因为它出现在 ...props
之后。
输出的结果是:
<div class="my-container">Hello World</div>
children
属性children
属性有点特殊。如果你直接在元素标签之间定义了内容,例如:
return <div {...props}>Goodbye World</div>;
此时,"Goodbye World"
会覆盖 props
对象中的 children
属性。换句话说,标签内部的内容优先级更高。
你可以将多个属性对象进行扩展,并且扩展顺序决定了最终应用的属性。例如:
const props1 = { className: "container" };
const props2 = { className: "my-container" };
return <div {...props1} {...props2} />;
在这个例子中,props2
中的 className="my-container"
会覆盖 props1
中的 className="container"
,因为 props2
位于 props1
之后。
输出的结果是:
<div class="my-container"></div>
通过这种方式,你可以灵活地管理和覆盖 JSX 元素的属性,同时减少手动编写重复代码的麻烦。
在这个练习中,我们将把之前使用 createElement
创建的 "Sam's Favorite Food" 列表(包含绿色鸡蛋和火腿)转化为使用 JSX 编写的版本。这将展示 JSX 在处理嵌套结构时的简洁性和可读性。
在 createElement
中,我们需要通过多层函数调用来构建嵌套的 HTML 结构。而使用 JSX 时,编写这样的嵌套结构会更加直观和接近原生的 HTML。以下是我们如何使用 JSX 来重现 "Sam's Favorite Food" 列表的代码:
function FavoriteFood() {
return (
<div className="container">
<p>Sam's favorite food:</p>
<ul className="sam-food">
<li>Green Eggs</li>
<li>Ham</li>
</ul>
</div>
);
}
createElement
的区别与 createElement
的多层函数调用不同,JSX 更加贴近 HTML 的语法,并且在可读性和直观性上有显著提高:
createElement
,让代码变得更加简洁易懂。class
属性被改为 className
,因为 class
是 JavaScript 中的保留字。<img />
、<br />
这样的单标签元素需要自闭合。通过这种方式,JSX 让我们更轻松地处理复杂的嵌套结构,尤其是在大型应用程序中,它可以极大地简化代码的编写与维护。
在这个片段中,讲解了如何通过将 HTML 代码直接复制粘贴到 JSX 中,并且展示了在 JSX 中处理嵌套结构是多么简单和高效。然而,JSX 与 HTML 之间有一些重要的区别,尤其是关于属性的命名。例如:
class
与 className
的区别:在 JSX 中,我们不能像在 HTML 中那样使用 class
属性,因为 class
在 JavaScript 中是一个保留字。在 JSX 中,你需要使用 className
来指定 CSS 类名。这是因为 JSX 更关注的是 DOM 属性,而不是 HTML 属性。
HTML 属性 vs. DOM 属性:JSX 使用的是 DOM 属性,而不是 HTML 属性。例如,表单中的 for
属性在 JSX 中应该写作 htmlFor
。这种转换有助于避免与 JavaScript 保留字的冲突,并保持一致性。
改进后的编程体验:通过 JSX,我们不再需要使用 React.createElement
来手动创建和嵌套元素。JSX 更加直观和简洁,类似于 HTML,这使得编写复杂的用户界面变得更加容易。
使用 JSX 明显提高了代码的可读性和开发效率,特别是在处理复杂的嵌套结构时。虽然 JSX 和 HTML 之间存在一些细微的差异(如属性命名),但这都是为了与 JavaScript 保持一致,并优化开发体验。
在这一段中,讲解了如何使用 React Fragments 来避免不必要的包裹元素,尤其是在某些布局需求(如 CSS Grid 或 Flexbox)中可能非常有用。
通常情况下,React 元素必须被一个单一的父元素包裹。这意味着如果你想返回多个兄弟元素(而不希望它们被一个 div
包裹),你可以使用 React Fragments 来实现。Fragments 允许你返回多个元素,而不在 DOM 中添加额外的包裹元素。
标准写法:
<React.Fragment>
<Element1 />
<Element2 />
</React.Fragment>
或者你可以先导入 Fragment
:
import { Fragment } from 'react';
然后:
<Fragment>
<Element1 />
<Element2 />
</Fragment>
简洁写法:
你可以用简写的形式,省略 Fragment
的名字,只需使用空标签包裹内容:
<>
<Element1 />
<Element2 />
</>
div
或其他容器标签中。通过使用 Fragment,你可以确保只输出你需要的元素,而不会在 DOM 中添加额外的节点,这对于保持 DOM 结构简洁和避免不必要的布局影响非常有帮助。
通过使用 React Fragments,你可以避免多余的容器元素,同时保持 JSX 代码的简洁和清晰。这种技巧特别适合处理复杂的布局结构,或者在组件返回多个元素时更好地控制 DOM 结构。
这一段解释了 React Fragments 的使用,并展示了为什么需要使用它来避免不必要的 div
包裹,同时还深入讲解了为何在 JavaScript 中无法直接返回多个顶层元素。
问题的来源:
div
和一个 ul
,会导致编译错误,因为 JavaScript 无法让一个变量同时指向两个不同的值。为什么不能直接移除父元素:
div
),React 会抛出错误,因为它无法理解如何将多个兄弟元素直接渲染在 DOM 中。JavaScript 不支持给一个变量赋值多个值,这就是为什么必须有一个父元素包裹它们。React Fragments 的引入:
Fragment
,它是一个 "虚拟" 的容器,允许你返回多个元素,但不会在 DOM 中生成任何实际的包裹元素。div
或者 span
,但仍然需要满足 React 的语法要求时。Fragment 的两种写法:
<React.Fragment>
<Element1 />
<Element2 />
</React.Fragment>
<>
<Element1 />
<Element2 />
</>
通过使用 React Fragments,你可以在不生成多余 DOM 元素的情况下返回多个兄弟元素,这使得代码更简洁,同时也避免了多余的嵌套层级。它非常适合在 CSS 布局或组件返回多个元素时使用。
哈哈!这个笑话很轻松有趣:“什么样的贝果会飞?答案是——普通贝果(plane bagel)!” 这也是个非常好的提示,让我们放松一下、站起来活动活动。
现在正是时候伸展一下身体,去喝点水或者吃个小零食。如果有机会,还可以去对别人说点鼓励的话,帮助别人也会让自己感觉很好。之后回来继续学习更多有趣的 React 内容!
在这个练习中,我们开始了解自定义组件的概念。在 React 中,自定义组件其实很简单,它就是一个函数,该函数接受一个对象(通常是 props
),并返回一些可以渲染的内容。那就是自定义组件的全部定义。
通常,返回的内容是 React 元素,但实际上它也可以是一个字符串、数字等可渲染的值。从本质上讲,自定义组件是你可以传递给 createElement
API 的东西。在 JSX 中,自定义组件有专门的语法,可以让你轻松地在 JSX 中使用这些自定义组件。
这里有一个简单的例子:一个名为 greeting
的函数组件,它接收 props
对象,然后这些 props
就是你渲染该组件时传递的值。
在接下来的练习中,你会逐步通过这些步骤来熟悉自定义组件的创建和使用,学习 JSX 是如何将这些组件编译成实际的函数调用,并了解 React 是如何处理这些调用的。
这个过程很有趣,也非常有用。希望你在接下来的练习中玩得开心,祝你好运!
在这个练习的第一部分,我们将逐步靠近创建一个可以生成可重用 JSX 的通用函数。现在我们有一个容器,它里面有两个消息,分别是“hello world”和“goodbye world”。显然,这里有一些重复的代码,我们可以优化它。
为了使代码更加通用,你可以编写一个函数,这个函数可以接收动态的子元素 children
,从而减少重复。例如,你可以创建一个名为 message
的函数,它允许你传递不同的 children
内容。
这个练习的目标是让你实现这样一个通用的函数接口。虽然这还不是一个完整的 React 组件,但我们正在逐渐靠近 React 组件的形式。通过插值(interpolation),我们可以将函数调用的结果作为表达式插入 JSX 中,而函数返回的 React 元素可以作为 div
的子元素渲染。
任务:你需要编写这个通用的 message
函数,并通过插值将它的输出作为 JSX 的一部分。
祝你在这个过程中玩得愉快,完成之后我们会继续!
在这个步骤中,我们通过创建一个 message
函数来减少代码重复。这个函数接收一个对象作为参数,并将其子元素 children
渲染到指定位置。最终,我们可以调用这个函数,传递需要显示的内容,从而避免代码的重复。
通过使用这个 message
函数,如果你想要在多个地方更新样式或结构,你只需要修改函数内部,而不必手动修改每个使用的地方。这就是减少代码重复的优势。
这个练习让我们更接近于 React 组件的结构,虽然当前的写法还不是完全的 React 组件形式,但它展示了如何通过参数化来灵活地处理 JSX 的渲染。在下一步中,我们将进一步探索真正的 React 组件并逐步优化这一过程。
祝你在这个过程愉快!
在这个步骤中,我们将进一步优化代码,使其更加符合 React 组件的语义,特别是通过使用 CreateElement
API。
我们需要将自定义的 message
函数转化为真正的 React 组件,并通过 CreateElement
API 来处理它,而不是直接调用函数。通过这种方式,React 将能够更好地管理组件的生命周期,比如何时渲染或更新组件。
组件的定义:
你仍然会保留 message
函数,但是不再直接调用它。相反,你会将这个函数作为一个特殊的元素传递给 React.createElement
。这样 React 就会负责调用这个函数,而不是你手动调用它。
传递 props:
在 React 中,当你通过 CreateElement
API 创建一个元素时,所有的 props 都会被组合成一个对象并传递给组件。我们会把 children
作为这个 props 的一部分。
Render 流程:
通过 React.createElement
API 传递 message
函数,然后由 React 来调用这个函数,并将 props 传递给它。
function Message({ children }) {
console.log('Rendering Message component');
return <div className="message">{children}</div>;
}
// 使用 CreateElement API
const element = React.createElement(Message, null, 'Hello World');
// Render 到 DOM
ReactDOM.createRoot(document.getElementById('root')).render(element);
在这个例子中,我们定义了一个 Message
组件,它接受一个 children
prop 并渲染它。然后,我们使用 React.createElement
来创建这个组件的实例,而不是手动调用它。React 会自动处理组件的调用并传递 props。
通过这种方法,当你引入更多复杂的功能,比如 hooks 时,你会发现 React 组件的这种形式非常重要和方便。
你可以通过在 Message
函数和 React.createElement
的各个位置添加 console.log
,来观察 React 是如何管理组件的渲染的。
React.createElement
是生成 React 元素的 API。CreateElement
API,React 将自动管理组件的渲染、更新等操作。希望你在这个过程中愉快探索!
在这个步骤中,我们通过使用 React.createElement
进一步加深了对 React 组件的理解,并学习了如何让 React 自己决定何时调用我们的自定义组件。通过这种方式,我们的组件更符合 React 的生命周期和渲染机制。
React.createElement
调用:
我们将 message
函数作为自定义组件传递给 React.createElement
,并让 React 负责在渲染时调用该组件。React 会传递 props,并在必要时调用组件函数。
延迟调用:
使用 React.createElement
后,React 将延迟调用组件,直到真正需要渲染时才调用组件。这与我们直接调用组件函数的方式不同。之前的方式是立即调用函数,而现在是由 React 自己来控制。
React 特有的元素类型:
当我们使用 React.createElement
时,React 创建了一个特殊的 React 元素,其类型不是普通的 HTML 标签,而是我们的自定义组件。这为组件的扩展和复用提供了可能性。
生命周期:
在我们继续深入理解 React 的状态管理和生命周期方法时,使用 React.createElement
这种方式的优势会变得更加明显。React 可以更高效地管理组件的渲染和更新,同时保持应用的状态隔离和一致性。
通过这一步,我们的组件已经在语义上符合了 React 的工作方式,即 React 自己负责组件的调用和管理。而接下来我们将进一步完善 JSX 语法,使自定义组件的使用更加简洁和直观。
希望这个过程帮助你更好地理解了 React.createElement
的机制和自定义组件的工作原理。
在这个练习中,我们的目标是从使用 React.createElement
过渡到更简洁、易读的 JSX 语法。不过需要注意的是:自定义组件的名称大小写非常重要。
在使用 JSX 时,React 会将小写字母开头的名称(例如 div
、span
)视为 HTML 标签。如果你使用了小写的自定义组件名称(例如 message
),React 会将它当作一个字符串,认为它是一个 HTML 标签。然而,当你将名称首字母大写(例如 Message
)时,React 就知道这是一个函数引用(自定义组件)。
通过这个过程,你将理解 React 如何编译 JSX 以及它如何区分 HTML 元素和自定义组件。
不大写组件名称: 首先,将你的自定义组件名称写成小写形式,并查看 React 如何将其编译为字符串:
const element = (
<div>
<message>Hello World</message>
</div>
);
将名称首字母大写: 现在,将组件名称首字母大写,查看编译后的不同:
const element = (
<div>
<Message>Hello World</Message>
</div>
);
当你将名称大写时,React 会识别 Message
是一个自定义组件,而不是一个 HTML 标签,并会调用函数,而不是将其当作字符串标签名处理。
这是以后你在 JSX 中编写 React 组件的标准方式,它比 React.createElement
提供了更清晰、可读性更高的语法。
一开始,我们会将 React.createElement
替换为 JSX 语法。让我们把 message
标签应用到 JSX 中:
<Message>Hello World</Message>
<Message>Goodbye World</Message>
表面上看,这似乎是有效的,但我们会得到一个警告:“浏览器无法识别标签 message
”。这意味着 JSX 正在尝试将 message
作为一个 HTML 标签,而不是 React 组件。React 提示我们:如果要渲染一个 React 组件,请将名称的首字母大写。
为了让 React 识别这是一个自定义组件,而不是原生 DOM 元素,我们需要将自定义组件的名称首字母大写:
<Message>Hello World</Message>
<Message>Goodbye World</Message>
当我们查看编译后的输出时,原来使用小写 message
时,React 编译器会将其解释为字符串 'message'
,即浏览器尝试渲染一个不存在的原生 DOM 元素。而当我们将其大写为 Message
后,React 就能识别这是一个函数引用,并调用对应的 React 组件。
通过这种方式,你可以轻松地创建并使用自定义组件,并遵循 React 的约定使代码更加规范和清晰。
React 使用组件名称的大小写来区分 HTML 元素和自定义组件。因此,当定义自定义组件时,始终记得将其名称首字母大写,以确保 JSX 能够正确编译并渲染组件。
这个练习是为了加深你对自定义组件和 props
的理解。我们将创建一个简单的计算器组件,这个组件将接收 left
、operator
和 right
作为属性,并渲染一个包含计算结果的 div
。
你需要实现一个 Calculator
组件,它会:
left
(左操作数)、operator
(运算符)和 right
(右操作数)作为 props
。props
生成一个计算表达式,比如:3 + 5
。下面是一个简单的实现思路:
function Calculator({ left, operator, right }) {
let result;
switch (operator) {
case '+':
result = left + right;
break;
case '-':
result = left - right;
break;
case '*':
result = left * right;
break;
case '/':
result = right !== 0 ? left / right : 'Error';
break;
default:
result = 'Invalid operator';
}
return (
<div>
{left} {operator} {right} = {result}
</div>
);
}
// 使用示例
<Calculator left={3} operator="+" right={5} />
<Calculator left={10} operator="*" right={2} />
Calculator
组件使用 props
来接收 left
、operator
和 right
。operator
的值,组件会执行相应的数学运算。left
、operator
、right
和结果渲染到页面上。尝试使用不同的 props
调用 Calculator
组件,并查看其工作效果。这个练习旨在帮助你理解如何通过 props
在 React 中传递数据并创建动态组件。
在这个练习中,我们创建了一个 Calculator
组件,并通过解构 props
来获取 left
、operator
和 right
三个参数。然后根据这些参数执行运算,并展示结果。
解构 props
:
我们从传入的 props
对象中解构出 left
、operator
和 right
。这种方式可以让代码更加简洁和可读。
执行运算:
使用 operator
来决定执行哪种运算(加、减、乘、除),并将结果存储在 result
变量中。这个步骤模拟了简单的数学运算。
渲染结果:
我们使用 JSX 来将 left
、operator
和 right
的值显示出来,并将计算结果显示在 output
标签中,以便于提高无障碍性。
function Calculator({ left, operator, right }) {
let result;
switch (operator) {
case '+':
result = left + right;
break;
case '-':
result = left - right;
break;
case '*':
result = left * right;
break;
case '/':
result = right !== 0 ? left / right : 'Error';
break;
default:
result = 'Invalid operator';
}
return (
<div>
<code>{left} {operator} {right}</code> = <output>{result}</output>
</div>
);
}
// 使用示例
<Calculator left={1} operator="+" right={2} />
<Calculator left={1} operator="/" right={2} />
灵活的 props
:
组件的 props
可以是任意类型,不仅限于原始类型(如数字、字符串),还可以是对象或其他 React 元素。
JSX 语法:
在 JSX 中,通过 {}
可以插入 JavaScript 表达式来显示变量或计算结果。
组件本质:
React 组件本质上是一个函数,它接受 props
作为参数,并返回需要渲染的 JSX。
通过这个练习,你可以进一步熟悉如何使用 props
传递数据,并在 React 组件中灵活渲染动态内容。
哈哈,Michael Jackson 的笑话和 React Router 的双关真是有趣!看来你学到很多 React 的知识,确实是时候让这些内容在大脑中“炖一炖”了!趁着这个休息时间,站起来活动一下,让身体和大脑都焕然一新。
在你休息之后,我们还会继续深入学习 React 的精彩部分。我很期待你回来,咱们继续一起学习 React 的其他酷炫内容!
从现在开始,我们将使用 TypeScript 来完成所有的开发。TypeScript 是一种建立在 JavaScript 之上的强类型语言,它可以为你带来类型安全性。
类型安全的最大好处就是,当别人调用你的函数时,如果传递的参数类型不对,TypeScript 会在编译时给你报错。比如,如果一个函数需要传入字符串(因为可能会用到 .toUpperCase()
这样的操作),但你传入了一个数字,TypeScript 就会提前告知你。在普通的 JavaScript 中,这是不会被捕获的。
整个行业目前基本都在使用 TypeScript,你也肯定想跟上这个趋势。接下来我们将开始在这个练习中探索 TypeScript。
有些人可能会觉得 TypeScript 会给他们的代码带来额外的“红色波浪线”,但其实这些都是帮助你避免潜在错误的警告。可以把 TypeScript 想象成一个“无情但诚实的朋友”,虽然它会指出代码中的问题,但它的目的是为了避免你犯下严重的错误。
React 组件本质上是接受对象(props)的函数,并返回可以渲染的东西。从 TypeScript 的角度来看,你只需要为这些函数定义类型。没有什么特别的规则,就是为函数加上类型定义。
这是一个普通的 JavaScript 函数:
function getUserDisplayName(user) {
return user.name || 'Unknown';
}
如果我们用 TypeScript,可以这样定义类型:
function getUserDisplayName(user: { name?: string }): string {
return user.name || 'Unknown';
}
上面的 user
参数必须是一个对象,并且可能有一个可选的 name
属性,类型为 string
。TypeScript 会帮你确保返回的结果始终是一个 string
。
下面是一个没有类型定义的 React 组件:
const Message = ({ children }) => <div>{children}</div>;
我们可以使用 TypeScript 为 children
添加类型:
const Message: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <div>{children}</div>;
};
这样,children
可以是任何 React 可渲染的内容,比如字符串、元素或其它组件。
如果你刚开始使用 TypeScript,可能会因为某些类型错误感到困惑。这时候,不妨使用 // @ts-expect-error
来临时忽略这些错误。等你对 TypeScript 更加熟悉后,可以回过头来修复这些问题。
我们将在接下来的练习中更深入地使用 TypeScript,帮助你写出更加健壮的 React 代码。
在这个练习中,我们将为计算器组件添加类型安全,以确保左操作数、运算符和右操作数都具有正确的类型。这将帮助我们避免潜在的错误,比如传入一个字符串而不是数字。
首先,我们需要定义一个类型来表示计算器的属性(props)。这可以通过创建一个接口来完成:
interface CalculatorProps {
left: number; // 左操作数
operator: '+' | '-' | '*' | '/'; // 运算符
right: number; // 右操作数
}
接下来,我们将使用这个类型来更新计算器组件的 props:
const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
let result: number;
switch (operator) {
case '+':
result = left + right;
break;
case '-':
result = left - right;
break;
case '*':
result = left * right;
break;
case '/':
result = right !== 0 ? left / right : 0; // 防止除以零
break;
default:
throw new Error(`Unknown operator: ${operator}`);
}
return (
<div>
<code>
{left} {operator} {right} = {result}
</code>
</div>
);
};
最后,我们可以在应用中使用这个组件,并确保传递正确的 props:
<Calculator left={1} operator="+" right={2} />
<Calculator left={5} operator="-" right={3} />
<Calculator left={4} operator="*" right={2} />
<Calculator left={10} operator="/" right={2} />
left
设置为一个字符串,TypeScript 将会抛出错误,提醒你这个 prop 的类型不匹配。operator
,我们使用了字符串字面量类型来限制允许的运算符,以便获得自动完成功能。通过这样的设置,我们的计算器组件现在就具备了类型安全,这不仅提高了代码的健壮性,也提高了开发过程中的便利性。您可以继续在应用中添加更多的计算器实例,看看 TypeScript 如何为您提供更好的开发体验。
在这个练习中,我们将通过添加类型安全来改进计算器组件的使用体验。这样,开发者在使用组件时可以获得更好的错误提示和自动补全功能。
CalculatorProps
类型首先,我们需要定义一个类型 CalculatorProps
,以确保传递给计算器组件的参数符合预期:
interface CalculatorProps {
left: number; // 左操作数
operator: '+' | '-' | '*' | '/'; // 运算符,可以限制为特定的字符串
right: number; // 右操作数
}
接下来,我们将使用这个类型更新计算器组件的 props:
const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
let result: number;
switch (operator) {
case '+':
result = left + right;
break;
case '-':
result = left - right;
break;
case '*':
result = left * right;
break;
case '/':
result = right !== 0 ? left / right : 0; // 防止除以零
break;
default:
throw new Error(`Unknown operator: ${operator}`);
}
return (
<div>
<code>
{left} {operator} {right} = {result}
</code>
</div>
);
};
使用组件时,我们确保传递正确的类型。例如:
<Calculator left={1} operator="+" right={2} />
<Calculator left={5} operator="-" right={3} />
<Calculator left={4} operator="*" right={2} />
<Calculator left={10} operator="/" right={2} />
为了确保类型安全,我们可以故意传递错误的类型,比如将 left
设置为字符串,TypeScript 将会抛出错误,提醒你这个 prop 的类型不匹配:
<Calculator left={"one"} operator="+" right={2} /> // 会产生错误
通过这种方式,我们可以确保组件的 props 类型安全,同时在开发过程中获得更好的错误提示和自动补全功能。这样做不仅提高了代码的可维护性,也提升了开发者的体验。继续在应用中添加更多的计算器实例,测试 TypeScript 的类型检查功能,确保你熟悉它的用法。
要将 operator
的类型限制为特定的字符串(如“+”、“-”、“*”和“/”),我们可以在 CalculatorProps
接口中使用字符串字面量类型。这将使 TypeScript 在编译时检查 operator
的值是否符合这些特定的运算符。
CalculatorProps
类型首先,我们需要更新 CalculatorProps
接口,具体如下:
interface CalculatorProps {
left: number; // 左操作数
operator: '+' | '-' | '*' | '/'; // 限制运算符为特定的字符串
right: number; // 右操作数
}
在更新了类型定义后,我们的计算器组件会自动获得这些类型的安全性。代码如下:
const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
let result: number;
switch (operator) {
case '+':
result = left + right;
break;
case '-':
result = left - right;
break;
case '*':
result = left * right;
break;
case '/':
result = right !== 0 ? left / right : 0; // 防止除以零
break;
default:
throw new Error(`Unknown operator: ${operator}`);
}
return (
<div>
<code>
{left} {operator} {right} = {result}
</code>
</div>
);
};
现在,当你尝试将不支持的运算符传递给 Calculator
组件时,TypeScript 会在编译时发出错误:
<Calculator left={1} operator="+" right={2} /> // 正确
<Calculator left={5} operator="-" right={3} /> // 正确
<Calculator left={4} operator="*" right={2} /> // 正确
<Calculator left={10} operator="/" right={2} /> // 正确
<Calculator left={10} operator="%" right={2} /> // 错误:类型“%”不可赋值给类型“'+' | '-' | '*' | '/'”。
通过将运算符类型限制为字符串字面量类型,开发者可以在编译时捕获潜在的错误,而不必在运行时检查。这提高了代码的安全性和可维护性。继续使用这些特性,在你的应用程序中进一步应用 TypeScript,确保在构建更复杂的功能时保持类型安全。
要限制运算符为特定字符串(如加法、减法、乘法和除法),并获得更好的类型安全和自动完成功能,可以使用 TypeScript 的字符串字面量类型。你已经成功实现了这个过程,并且得到了 TypeScript 的类型检查和代码编辑器的自动完成功能。这是 TypeScript 的强大之处,能够在开发过程中提供实时的反馈。
以下是你所做的关键步骤的总结:
定义 CalculatorProps
接口:
interface CalculatorProps {
left: number;
operator: '+' | '-' | '*' | '/'; // 使用字符串字面量类型限制运算符
right: number;
}
在组件中使用这些类型:
在 Calculator
组件中,你使用这些定义的类型来确保正确的参数传递。
const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
// 计算逻辑...
};
错误处理:
通过 TypeScript 的类型检查,当你尝试传递不允许的运算符(如 **
或 ^
)时,编辑器将给出错误提示,而不必运行应用程序。
虽然当前实现已经很好地解决了问题,但你提到可以进一步优化。以下是一些潜在的改进方向:
使用枚举: 如果运算符数量较多或可能变化,考虑使用枚举来管理运算符。
enum Operator {
Add = '+',
Subtract = '-',
Multiply = '*',
Divide = '/',
}
interface CalculatorProps {
left: number;
operator: Operator; // 使用枚举类型
right: number;
}
自动化运算符提示: 结合 TypeScript 的类型提示功能,增强用户体验。例如,可以在函数内部提供详细的错误信息或提示,指导用户如何正确使用组件。
运行时检查: 尽管 TypeScript 提供了静态检查,但在组件运行时也可以加入检查,以确保运算符的有效性(例如,抛出错误或警告)。
通过这些方法,你不仅可以提高代码的可读性和可维护性,还能让使用组件的开发者获得更好的体验。继续探索 TypeScript 的特性,将其应用于你的 React 项目中,以构建更安全、更高效的应用程序!
为了使 TypeScript 的运算符更具可扩展性并避免手动添加每个运算符,你可以使用类似于之前所述的typeof
和keyof
关键字来创建派生类型。这样,我们就可以从现有的运算符对象中提取出所有的运算符,而不是手动维护一个字符串字面量类型。
以下是你可以遵循的步骤,以便在 TypeScript 中实现更灵活的运算符类型:
定义一个运算符对象: 首先,定义一个包含所有有效运算符的对象。这将允许你从这个对象中提取出运算符。
const operations = {
add: '+',
subtract: '-',
multiply: '*',
divide: '/',
} as const; // 使用 `as const` 来确保这是一个只读对象
提取运算符类型:
然后,使用typeof
和keyof
来提取运算符的类型。
type Operator = typeof operations[keyof typeof operations]; // 这将是 '+' | '-' | '*' | '/'
更新 CalculatorProps
接口:
修改 CalculatorProps
接口,使其使用新的运算符类型。
interface CalculatorProps {
left: number;
operator: Operator; // 使用派生的 Operator 类型
right: number;
}
以下是一个完整的示例代码,展示了如何实现上述步骤:
const operations = {
add: '+',
subtract: '-',
multiply: '*',
divide: '/',
} as const;
type Operator = typeof operations[keyof typeof operations]; // '+' | '-' | '*' | '/'
interface CalculatorProps {
left: number;
operator: Operator;
right: number;
}
const Calculator: React.FC<CalculatorProps> = ({ left, operator, right }) => {
let result: number;
switch (operator) {
case '+':
result = left + right;
break;
case '-':
result = left - right;
break;
case '*':
result = left * right;
break;
case '/':
result = left / right;
break;
default:
throw new Error(`Unsupported operator: ${operator}`);
}
return (
<div>
<code>
{left} {operator} {right} = {result}
</code>
</div>
);
};
通过这种方式,你可以方便地添加或删除运算符,而不必手动维护类型定义。当你更新运算符对象时,TypeScript 会自动反映这些更改,保持类型的一致性。这种方法在开发过程中可以减少错误并提高代码的可维护性。
很高兴你已经成功实现了运算符的类型派生!下面是对这个解决方案的进一步分析,帮助你理解这一过程的细节和优势。
你所使用的解决方案利用了 TypeScript 的类型系统,通过从一个包含有效运算符的对象中提取出运算符类型。这不仅简化了代码,还使得类型定义更加灵活和可扩展。
定义运算符对象:
你创建了一个对象 operations
,它包含了所有有效的运算符,并且使用 as const
确保它是一个只读的常量。这使得 TypeScript 能够推断出对象的字面量类型。
const operations = {
add: '+',
subtract: '-',
multiply: '*',
divide: '/',
// 可以在这里添加其他运算符
} as const;
提取运算符类型:
你通过使用 typeof
和 keyof
创建了一个运算符类型,这个类型自动从 operations
对象中派生而来。
type Operator = typeof operations[keyof typeof operations]; // 结果是 '+' | '-' | '*' | '/'
更新 CalculatorProps
接口:
最后,你更新了 CalculatorProps
接口,将 operator
的类型改为新的 Operator
类型。
interface CalculatorProps {
left: number;
operator: Operator; // 使用派生的 Operator 类型
right: number;
}
自动推断:通过这种方式,任何时候你更新 operations
对象,都能自动反映在 Operator
类型中,避免了手动同步的麻烦。
类型安全:TypeScript 会确保在使用组件时,只有有效的运算符可以被传递,这大大减少了运行时错误的风险。
开发体验:使用 TypeScript 提供的自动完成功能,使得编写代码时更加高效和愉快。
这种类型派生的方式非常有效,能够让你的代码更易于维护和扩展。继续保持这种方式,不断优化你的代码!如果有其他问题或需要进一步的帮助,请随时问我。
好的,我们来讨论如何为计算器组件设置默认值并使某些属性变为可选属性。在 TypeScript 中,我们可以通过定义接口来实现这一点,同时结合提供默认值的方式。下面是如何完成这一任务的步骤。
CalculatorProps
接口:使 left
、right
和 operator
属性变为可选。CalculatorProps
接口首先,我们需要更新 CalculatorProps
接口,以便将 left
、right
和 operator
属性标记为可选的。使用问号 ?
表示属性是可选的。
interface CalculatorProps {
left?: number; // 可选属性
operator?: Operator; // 可选属性
right?: number; // 可选属性
}
在你的计算器组件内部,你可以使用逻辑运算符来设置默认值。这样,如果传入的 props 中没有这些属性,组件就会使用默认值。
const Calculator: React.FC<CalculatorProps> = ({ left = 0, operator = '+', right = 0 }) => {
// 现在 left、operator 和 right 都有默认值
const result = operations[operator](left, right); // 计算结果
return (
<div>
<code>{`${left} ${operator} ${right}`}</code>
= <output>{result}</output>
</div>
);
};
下面是完整的示例代码,展示如何将所有这些组合在一起:
import React from 'react';
// 定义运算符
const operations = {
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
multiply: (a: number, b: number) => a * b,
divide: (a: number, b: number) => a / b,
} as const;
// 定义可选的运算符类型
type Operator = typeof operations[keyof typeof operations];
// 定义 CalculatorProps 接口
interface CalculatorProps {
left?: number;
operator?: Operator;
right?: number;
}
// Calculator 组件
const Calculator: React.FC<CalculatorProps> = ({ left = 0, operator = 'add', right = 0 }) => {
const result = operations[operator](left, right);
return (
<div>
<code>{`${left} ${operator} ${right}`}</code>
= <output>{result}</output>
</div>
);
};
// 使用 Calculator 组件
const App = () => (
<div>
<Calculator left={1} right={2} operator="add" />
<Calculator right={5} operator="multiply" />
<Calculator left={4} />
<Calculator />
</div>
);
export default App;
通过这些步骤,你已经成功地为计算器组件实现了可选属性和默认值。这不仅提高了组件的灵活性,也使得使用组件时的开发体验更加友好。如果用户不传入某些属性,组件会自动使用预设的默认值。这样,你可以确保组件始终能正常工作。继续加油!如果你还有其他问题,欢迎随时问我。
好的,我们来总结一下如何在 TypeScript 中为 React 组件设置可选属性和默认值。在这段代码中,我们的目标是为计算器组件的 left
、right
和 operator
属性提供默认值,并确保这些属性是可选的。
left
、right
和 operator
属性标记为可选属性。以下是一个简单的实现示例:
import React from 'react';
// 定义运算符类型
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
// 定义计算器属性接口
interface CalculatorProps {
left?: number; // 可选属性
operator?: Operator; // 可选属性
right?: number; // 可选属性
}
// 定义运算操作
const operations: Record<Operator, (a: number, b: number) => number> = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
// Calculator 组件
const Calculator: React.FC<CalculatorProps> = ({ left = 0, operator = 'add', right = 0 }) => {
// 计算结果
const result = operations[operator](left, right);
return (
<div>
<code>{`${left} ${operator} ${right}`}</code>
= <output>{result}</output>
</div>
);
};
// 使用 Calculator 组件
const App = () => (
<div>
<Calculator left={1} right={2} operator="add" />
<Calculator right={5} operator="multiply" />
<Calculator left={4} />
<Calculator />
</div>
);
export default App;
接口定义:我们定义了 CalculatorProps
接口,其中 left
、right
和 operator
都是可选属性。
interface CalculatorProps {
left?: number;
operator?: Operator;
right?: number;
}
默认值设置:在组件定义中,我们为 left
、operator
和 right
属性设置了默认值。这意味着如果没有传入这些属性,组件会使用默认值:
const Calculator: React.FC<CalculatorProps> = ({ left = 0, operator = 'add', right = 0 }) => {
const result = operations[operator](left, right);
// ... 组件的返回部分
};
运算操作:使用一个记录类型 Record<Operator, (a: number, b: number) => number>
来定义运算操作,这样我们就可以安全地引用运算符,确保不会传入错误的运算符。
Calculator
组件而不传入某个属性时,比如不传入 left
,则会使用默认值 0
。通过这次的实现,你已经成功为计算器组件设置了可选属性和默认值。这不仅提高了组件的灵活性,也使得使用组件时的开发体验更加友好。继续加油,如果你有其他问题,随时可以问我!
在这一步中,我们将专注于优化 TypeScript 的类型定义,以便更好地处理数学运算,而不需要在每个操作中重复定义函数的类型。这将使我们能够轻松添加新的操作而不必担心类型声明。
我们希望创建一个操作类型,以便我们可以从中导出所需的参数类型和返回类型,而无需手动指定每个操作的详细类型。
Operation
,它是一个接受两个数字参数并返回数字的函数类型。下面是一个简化的实现示例:
import React from 'react';
// 定义运算符类型
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
// 定义操作类型
type Operation = (left: number, right: number) => number;
// 创建操作对象
const operations: Record<Operator, Operation> = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
// 定义计算器属性接口
interface CalculatorProps {
left?: number;
operator?: Operator;
right?: number;
}
// 计算器组件
const Calculator: React.FC<CalculatorProps> = ({
left = 0,
operator = 'add',
right = 0,
}) => {
const result = operations[operator](left, right);
return (
<div>
<code>{`${left} ${operator} ${right}`}</code>
= <output>{result}</output>
</div>
);
};
// 使用 Calculator 组件
const App = () => (
<div>
<Calculator left={1} right={2} operator="add" />
<Calculator right={5} operator="multiply" />
<Calculator left={4} />
<Calculator />
</div>
);
export default App;
定义 Operation
类型:我们定义了一个 Operation
类型,表示接受两个 number
类型参数并返回 number
的函数。
type Operation = (left: number, right: number) => number;
创建 operations
对象:使用 Record<Operator, Operation>
来定义一个运算对象,允许我们将每个运算符与其对应的实现关联起来。
const operations: Record<Operator, Operation> = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
计算器组件:在 Calculator
组件中,我们使用 operations
对象来执行所需的运算,而不需要为每个操作手动定义类型。
Operation
类型,我们减少了冗余的类型声明。operations
对象中添加新项即可,无需更改类型定义。通过以上步骤,我们成功地优化了 TypeScript 的类型定义,使得添加新操作变得更加容易。这样的结构不仅提高了代码的可读性和可维护性,也使得开发过程更加顺畅。如果你有其他问题或需要进一步的帮助,请随时告诉我!
在这个阶段,我们要进一步改进我们的 TypeScript 类型定义,以确保我们的计算器操作具备更好的类型安全和自动完成功能。我们需要处理的主要任务是定义一个 Operation
函数类型,并利用这个类型来确保我们能够限制操作符的可用性,同时又不会牺牲类型安全性。
Operation
函数类型,该类型接受两个数字参数并返回一个数字。以下是一个改进的实现示例:
import React from 'react';
// 定义运算符类型
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
// 定义运算类型
type Operation = (left: number, right: number) => number;
// 创建操作对象,使用 Record 来限定操作符
const operations: Record<Operator, Operation> = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
// 定义计算器属性接口
interface CalculatorProps {
left?: number;
operator?: Operator;
right?: number;
}
// 计算器组件
const Calculator: React.FC<CalculatorProps> = ({
left = 0,
operator = 'add',
right = 0,
}) => {
const result = operations[operator](left, right);
return (
<div>
<code>{`${left} ${operator} ${right}`}</code>
= <output>{result}</output>
</div>
);
};
// 使用 Calculator 组件
const App = () => (
<div>
<Calculator left={1} right={2} operator="add" />
<Calculator right={5} operator="multiply" />
<Calculator left={4} />
<Calculator />
</div>
);
export default App;
定义操作符类型:我们定义了一个 Operator
类型,它只允许 'add'
, 'subtract'
, 'multiply'
, 和 'divide'
这四个字符串。
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
定义运算类型:定义了一个 Operation
类型,用于表示运算函数。
type Operation = (left: number, right: number) => number;
创建操作对象:使用 Record<Operator, Operation>
来确保 operations
对象中的每个运算符都与相应的操作函数匹配。
const operations: Record<Operator, Operation> = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
};
定义计算器组件:在 Calculator
组件中,定义了 CalculatorProps
接口,以确保左侧和右侧参数为可选的数字,操作符为可选的 Operator
类型。
自动完成与类型检查:通过将操作符类型限制为一个小的字符串集,我们能够利用 TypeScript 的自动完成特性,确保用户在传递参数时仅能选择有效的操作符。
可选参数与默认值:我们为左侧和右侧数字以及操作符提供了默认值,使得使用计算器组件时更加灵活和友好。
通过以上步骤,我们成功实现了对计算器组件的类型安全检查和操作符限制,使得代码更加健壮。这样的结构不仅提高了代码的可读性和可维护性,也使得开发过程更加顺畅。如果你还有其他问题或者需要进一步的帮助,请随时告诉我!
在这个步骤中,我们将通过使用 TypeScript 的 satisfies
关键字来改进我们的 operations
对象的类型定义。这样做可以简化类型声明,同时保持类型安全和良好的开发体验。
satisfies
语法:我们将使 operations
对象满足更广泛的类型定义。operations
符合更广泛的类型,而不是强制性地将其定义为一个特定的类型。以下是如何实现的示例代码:
import React from 'react';
// 定义运算符类型
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
// 定义运算类型
type Operation = (left: number, right: number) => number;
// 创建操作对象
const operations = {
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
multiply: (a: number, b: number) => a * b,
divide: (a: number, b: number) => a / b,
} satisfies Record<Operator, Operation>; // 使用 satisfies 语法
// 定义计算器属性接口
interface CalculatorProps {
left?: number;
operator?: Operator;
right?: number;
}
// 计算器组件
const Calculator: React.FC<CalculatorProps> = ({
left = 0,
operator = 'add',
right = 0,
}) => {
const result = operations[operator](left, right);
return (
<div>
<code>{`${left} ${operator} ${right}`}</code>
= <output>{result}</output>
</div>
);
};
// 使用 Calculator 组件
const App = () => (
<div>
<Calculator left={1} right={2} operator="add" />
<Calculator right={5} operator="multiply" />
<Calculator left={4} />
<Calculator />
</div>
);
export default App;
运算符类型:我们定义了一个 Operator
类型,它限制了可用的运算符。
type Operator = 'add' | 'subtract' | 'multiply' | 'divide';
运算类型:定义了一个 Operation
类型,表示接收两个数字并返回一个数字的函数。
type Operation = (left: number, right: number) => number;
使用 satisfies
语法:在 operations
对象中,使用 satisfies
关键字来指定 operations
应符合 Record<Operator, Operation>
类型,而不强制为特定的类型。这使得我们可以更灵活地扩展 operations
对象而不会破坏类型检查。
const operations = {
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
multiply: (a: number, b: number) => a * b,
divide: (a: number, b: number) => a / b,
} satisfies Record<Operator, Operation>;
satisfies
关键字,我们能够减少冗余的类型定义,使代码更加简洁易读。operations
的类型检查,并确保我们只使用有效的操作符。通过这个步骤,我们不仅优化了类型定义,同时也提升了开发体验。你可以根据需要继续扩展 operations
对象,而不必担心手动维护类型。如果你有其他问题或需要进一步的帮助,请随时告诉我!
在这个步骤中,我们将优化我们对运算符类型的管理,使其更具灵活性,同时保持类型安全和良好的开发体验。我们将通过使用 TypeScript 的 satisfies
语法来实现这一点。
satisfies
语法:使 operations
对象能够满足更广泛的类型,而无需重复类型定义。operations
时仍能获得类型安全和 IDE 的自动补全功能。以下是更新后的代码示例:
import React from 'react';
// 定义运算类型
type Operation = (left: number, right: number) => number;
// 创建操作对象
const operations = {
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
multiply: (a: number, b: number) => a * b,
divide: (a: number, b: number) => a / b,
} satisfies Record<string, Operation>; // 使用 satisfies 语法
// 定义计算器属性接口
interface CalculatorProps {
left?: number;
operator?: keyof typeof operations; // 限制为 operations 对象的键
right?: number;
}
// 计算器组件
const Calculator: React.FC<CalculatorProps> = ({
left = 0,
operator = 'add',
right = 0,
}) => {
const result = operations[operator](left, right);
return (
<div>
<code>{`${left} ${operator} ${right}`}</code>
= <output>{result}</output>
</div>
);
};
// 使用 Calculator 组件
const App = () => (
<div>
<Calculator left={1} right={2} operator="add" />
<Calculator right={5} operator="multiply" />
<Calculator left={4} />
<Calculator />
</div>
);
export default App;
运算类型:定义了一个 Operation
类型,表示接收两个数字并返回一个数字的函数。
type Operation = (left: number, right: number) => number;
使用 satisfies
语法:在 operations
对象中,使用 satisfies
关键字来指定 operations
应符合 Record<string, Operation>
类型。这样,operations
可以是任意字符串映射到 Operation
类型的对象,同时保持对各个操作的类型检查和自动补全功能。
const operations = {
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
multiply: (a: number, b: number) => a * b,
divide: (a: number, b: number) => a / b,
} satisfies Record<string, Operation>;
限制运算符:通过 keyof typeof operations
,我们确保 operator
属性只能是 operations
对象中的有效键,从而增强了类型安全。
operator?: keyof typeof operations;
satisfies
,我们避免了单独定义运算符类型,从而减少了冗余代码。operations
中保持了类型检查和 IDE 的自动补全功能,确保开发过程顺利。通过这个步骤,我们不仅优化了运算符的类型管理,还提升了代码的可维护性和可扩展性。你可以根据需要继续扩展 operations
对象,而不必担心手动维护类型。如果你有其他问题或需要进一步的帮助,请随时告诉我!
这是一个很好的时刻来休息一下!给自己一些时间来补充水分,放松心情,帮助你更好地消化和巩固所学的知识。
在学习新内容时,特别是编程和技术方面,休息和自我照顾是非常重要的。就像你提到的,写下你所学到的东西,即使不再查看,写的过程本身也会帮助你记住这些信息。
当你准备好继续时,欢迎回来,我们将继续学习更多有趣的内容!如果你有任何问题或者需要进一步的帮助,请随时告诉我。
好了,在这个练习中,我们将进行样式的学习。
我现在应该戴上太阳镜,但没关系,Cody 是我的风格。
我们将讨论两种主要的样式应用方式:你可以使用内联样式或者外部 CSS 样式表(级联样式表)。通常,你会通过类名来定位元素。虽然你也可以通过属性或标签名来定位元素,这也是一个合理的做法,但我们将重点关注类名。
在 HTML 中,我们有一个类名为 my class
。而在 JavaScript 中,你实际获取的是 className
。需要注意,属性与特性不同,在 TypeScript 或 TSX(TypeScript 和 JSX 的混合)中,你使用的是属性名而不是特性名,所以是 className
。
关于样式,内联样式虽然不常用,但在某些情况下确实很有用。在 HTML 中,你会使用一长串 CSS 字符串,这种方式的功能有限,但这就是它的用法。而在 JavaScript 中,你会使用 CSS 样式对象。元素上有一个 style
属性,CSS 属性会以驼峰命名法来代替 CSS 中的连接符命名法。在 JSX 中,我们同样会使用 style
,并传递一个对象,其中的属性是驼峰命名的。
另一个有趣的点是,你可以省略像 px
这样的单位。如果你为某个需要像素单位的属性只提供一个数值,那么 JSX 会自动为你添加 px
单位。当然,也可以是 rem
或其他单位。
还有一个点,初学 React 时经常会让人困惑,那就是双层大括号。其实这并没有什么特别之处,你可以通过定义一个对象并传递它作为插值的值来实现相同的效果。这是因为 JSX 允许插入 JavaScript 代码,而大括号中的内容正是一个 JavaScript 对象,因此双层大括号本质上只是对象的插值形式。
关于类名和内联样式,内联样式是直接应用在你渲染的元素上,而类名、属性和标签名则需要在页面中添加一个样式表。我们会在 HTML 中使用 href
来引入一个外部样式表,并在该样式表中定义 my class
类。另一种方式是将样式直接写在 HTML 的 style
标签中,不过这种方式使用较少。
虽然在 TSX 中有一些不同的处理方式,但我们很少这么做,因此不需要过多担心。
这应该足够让你开始这个练习了,我相信你会享受为应用添加样式的过程。
在这个练习中,另一个很酷的地方是我们将学习如何组合样式。早期内联样式曾一度流行,因为它们很容易组合在一起。然而,现在我们有了像 Tailwind 这样更好的工具,所以我们不会深入探讨 Tailwind,但我们会讨论如何组合 CSS 类名和内联样式属性。
当我们完成后,你将得到一个非常通用的盒子组件,它的 API 非常简洁,能让用户轻松创建出像这样的漂亮盒子。我相信你会喜欢这个练习。
我们现在有一个 index.css
文件,其中包含了很多很酷的样式,这些样式我们希望应用到对应的元素上。
如果我们查看页面源代码,可以看到它作为样式表被引入。这里是指向 index.css
的 href
。
你的任务是应用其中的一些类名,同时也要应用一些内联样式。
完成之后,页面应该看起来像这样(展示的最终效果)。希望你能在这个过程中享受乐趣!
我们将进行一些非常酷的多光标操作魔法。
首先,我会选择所有这些元素,然后为它们添加 className="box"
,接着将其设置为 box--these
。其中有一个元素是无尺寸的(sizeless),我们将其去掉。
接着,我们会添加 style
属性,设置 backgroundColor
。背景颜色应该是这样的:backgroundColor
。Boom!
然后我们将字体样式设置为斜体(italic)。Boom!看这个效果。
接下来更新这个元素,它并不是没有颜色的。这个类没有尺寸,但看这个效果,简直是神奇的——它有效!
每个元素都需要有一个 className
,并且每个元素都需要有一个 style
。即使是无尺寸、无颜色的 box
,它依然应该有 className="box"
。虽然它不需要尺寸或颜色,但它应该仍然是斜体的。
这样,我们就为不同的元素应用了所有这些很棒的 CSS 属性。如果你成功实现了这一点,干得漂亮!
### 提取公共部分到 Box 组件
我们现在有一些重复的代码存在于不同的 box 组件中。我认为将这些公共部分提取到一个单独的 `Box` 组件中会非常有帮助。
目前,我们在 `div` 元素中指定了 `className="box"`,所有的 box 都有相同的 `className`。我们还设置了 `style` 属性,将字体样式定义为斜体(italic),所有的 box 都使用了相同的斜体样式。唯一的区别是类名和背景颜色,当然,子元素(children)也是一个 `prop`。
### 创建 Box 组件
我们可以创建一个 `Box` 组件,它接受这些 `props`,然后将它们组合在一起。这样,我们就可以获得正确的最终结果,而不需要每次重复定义所有公共的样式。
```tsx
interface BoxProps {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}
const Box: React.FC<BoxProps> = ({ className = '', style = {}, children }) => {
return (
<div
className={`box ${className}`}
style={{ fontStyle: 'italic', ...style }}
>
{children}
</div>
);
};
子元素(children
)会通过 props
传递给 Box
组件中的 div
元素。我们可以像这样使用 Box
组件:
<Box className="custom-box" style={{ backgroundColor: 'lightblue' }}>
这是一个自定义的 box 内容。
</Box>
这个 Box
组件会自动应用字体样式为斜体,同时允许我们自定义 className
和 style
属性。
我们也可以通过解构 props
来处理额外的属性,比如 size
:
interface BoxProps {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
size?: 'small' | 'medium' | 'large'; // 新增的 size 属性
}
const Box: React.FC<BoxProps> = ({ className = '', style = {}, children, size }) => {
const sizeStyle = size === 'small' ? { padding: '5px' } :
size === 'medium' ? { padding: '10px' } :
{ padding: '20px' };
return (
<div
className={`box ${className}`}
style={{ fontStyle: 'italic', ...sizeStyle, ...style }}
>
{children}
</div>
);
};
通过这个 Box
组件,你可以轻松复用公共样式,同时可以自定义类名、样式和额外的属性。接下来,你可以尝试为 Box
组件添加更多功能并进行扩展。
使用示例:
<Box className="large-box" style={{ backgroundColor: 'lightgreen' }} size="large">
这是一个大号的 box 内容。
</Box>
这样就可以在应用中灵活使用 Box
组件,简化代码并提高可复用性。
这段代码展示了如何创建一个通用的 `Box` 组件,允许你复用公共样式并支持自定义类名和样式,同时还支持额外的 `size` 属性。
## 055 自定义组件 (1)
### 创建 Box 组件
我们将创建一个函数组件 `Box`,并传递所有的 `props`。
```tsx
const Box: React.FC<React.ComponentProps<'div'>> = (props) => {
return <div {...props} />;
};
在这个基础上,我们可以将所有的 div
元素更新为 Box
组件,这样我们就不必担心其他问题,所有东西仍然能正常工作。
我们可以做一些特殊的处理,尤其是在 className
和 style
属性上。首先,我们从 props
中提取 className
:
const Box: React.FC<React.ComponentProps<'div'>> = ({ className = '', ...props }) => {
return <div className={`box ${className}`} {...props} />;
};
你会注意到,虽然我们传递了 className="box small"
,但实际渲染时只显示了 box
,这是因为我们在提取 className
后没有将它合并。因此,我们需要使用字符串插值来合并 className
。
现在 className
已经合并完成,不过我们发现有时会重复出现 box
。我们可以通过删除不必要的 box
来解决这个问题:
const Box: React.FC<React.ComponentProps<'div'>> = ({ className = '', ...props }) => {
const combinedClassName = className ? `box ${className}` : 'box';
return <div className={combinedClassName} {...props} />;
};
这样可以避免出现 undefined
或重复的 box
,并且我们还可以移除所有多余的 box
类名。
另一个我们想要处理的是 style
属性。我们可以从 props
中提取 style
,然后进行合并:
const Box: React.FC<React.ComponentProps<'div'>> = ({ className = '', style = {}, ...props }) => {
const combinedClassName = className ? `box ${className}` : 'box';
const combinedStyle = { fontStyle: 'italic', ...style };
return <div className={combinedClassName} style={combinedStyle} {...props} />;
};
在这个例子中,fontStyle: 'italic'
是默认样式,用户传递的 style
会覆盖默认样式。我们还可以将 style
属性中的其他属性合并在一起。
现在你可以根据需求定制 Box
组件。可以使用这个组件创建不同的盒子元素:
<Box className="small-box" style={{ backgroundColor: 'lightblue' }}>
这是一个小盒子。
</Box>
<Box className="medium-box" style={{ fontWeight: 900 }}>
这是一个加粗字体的盒子。
</Box>
通过这种方式,我们能够创建一个通用的 Box
组件,该组件能够合并 className
和 style
,并允许定制和扩展。这个模式非常常见,可以让你更轻松地管理和复用组件的样式和行为。
如果你对这些操作感到困惑,可以通过 console.log
打印 props
进行调试。这不仅有助于你理解 React 中的组件逻辑,还可以提升你的 JavaScript 技能。
希望你享受这个过程并觉得有趣!
在这一步,我们将进一步提升用户在使用 Box
组件时的体验。之前,用户需要手动传递 className
,比如 box--small
或者 box--medium
。现在,为了让用户的开发体验更好,我们将添加一个 size
属性,并为其定义具体的类型。
这样,开发者可以通过传递一个简单的 size
属性来控制盒子的大小,而不需要知道具体的类名。
我们依然保留 style
和 className
的组合方式,这样如果用户想要进一步覆盖或组合样式,他们依然可以这么做。
我们将为 Box
组件添加一个新的 size
属性,并通过类型定义限制可选的尺寸。
interface BoxProps extends React.ComponentProps<'div'> {
size?: 'small' | 'medium' | 'large'; // 定义 size 属性
}
const Box: React.FC<BoxProps> = ({ className = '', size, style = {}, ...props }) => {
// 根据 size 属性来设置 className
const sizeClass = size ? `box--${size}` : '';
const combinedClassName = `box ${sizeClass} ${className}`.trim(); // 合并 className
// 合并 style
const combinedStyle = { fontStyle: 'italic', ...style };
return (
<div className={combinedClassName} style={combinedStyle} {...props} />
);
};
通过传递 size
属性,用户可以很轻松地控制盒子的大小,而无需了解底层的 CSS 类名。
<Box size="small" style={{ backgroundColor: 'lightblue' }}>
这是一个小盒子。
</Box>
<Box size="large" className="custom-box" style={{ backgroundColor: 'lightgreen' }}>
这是一个大盒子。
</Box>
在这个示例中,size="small"
会自动为 Box
组件应用 box--small
类名,而 size="large"
则会应用 box--large
类名。
通过添加 size
属性,我们不仅简化了开发者的操作,还提高了 Box
组件的可扩展性和易用性。开发者现在只需传递一个 size
属性,就可以轻松控制盒子的大小,而无需担心具体的 CSS 类名。这种优化提供了更好的开发体验。
通过这种方式,用户可以轻松使用 size
属性来控制盒子的大小,同时保留了对 className
和 style
的灵活性。
在这一步,我们希望通过 size
属性来控制 Box
组件的大小,并为用户提供更好的开发体验。例如,用户可以通过简单的 size
属性来设置盒子的大小,比如 small
、medium
或 large
,而不需要手动传递类名。
我们首先为 size
添加一个类型,并处理它的默认值和应用逻辑。
interface BoxProps extends React.ComponentProps<'div'> {
size?: 'small' | 'medium' | 'large'; // 定义 size 属性
}
const Box: React.FC<BoxProps> = ({ className = '', size, style = {}, ...props }) => {
// 根据 size 属性生成相应的类名
const sizeClass = size ? `box--${size}` : '';
// 使用数组存储类名并过滤掉空值
const combinedClassName = [ 'box', sizeClass, className ]
.filter(Boolean) // 过滤掉 falsy 值,如空字符串
.join(' '); // 用空格拼接类名
// 合并样式
const combinedStyle = { fontStyle: 'italic', ...style };
return <div className={combinedClassName} style={combinedStyle} {...props} />;
};
为确保类名中没有多余的空格,我们将类名存储在一个数组中,并使用 filter(Boolean)
来过滤掉空字符串或 undefined
。最后通过 join(' ')
将它们用空格拼接在一起。
通过设置 size
属性,用户可以轻松控制 Box
的大小,而无需了解底层的类名。
<Box size="small" style={{ backgroundColor: 'lightblue' }}>
这是一个小盒子。
</Box>
<Box size="medium" className="custom-box" style={{ backgroundColor: 'lightgreen' }}>
这是一个中号盒子。
</Box>
<Box size="large" style={{ backgroundColor: 'pink' }}>
这是一个大盒子。
</Box>
通过为 size
属性定义明确的类型,开发者可以在使用时享受更好的自动补全支持。如果开发者错误地拼写了 size
的值,TypeScript 也会进行相应的类型检查,帮助开发者更快地发现问题。
<Box size="large">
这是一个大盒子。
</Box>
通过这种方式,Box
组件的样式逻辑完全自包含,用户只需要提供 size
属性即可控制组件的外观。我们还可以为开发者提供明确的 API,而不需要他们了解内部的类名或样式细节。这种模式提供了更好的开发体验,并确保了代码的可维护性。
而且,组件开发者可以完全控制哪些样式可以被覆盖,比如,如果你不希望用户覆盖 fontStyle
,你可以固定该样式,使其不可被修改。
这种方法不仅简化了使用,还让组件变得更加灵活和可扩展。
通过这种实现,Box
组件的大小可以通过 size
属性轻松控制,且用户不需要关心底层的 CSS 类名,从而提供了更好的开发体验。
用坏了的铅笔写字是毫无意义的,哈哈。
而如果你不能记住所学的内容,学习也是毫无意义的。所以,也许你可以拿出一支铅笔,把所学的东西记下来,这样你就能记住它们了。
然后,出去走动一下,让血液流动起来,这样你可以回来继续学习更多有趣的内容。
你的大脑需要休息,你也需要。希望你有一个愉快的休息时间,我们等你回来继续学习。
现在是时候深入了解表单了!表单让你能够与网页互动,真的很神奇。
自从 HTML 在 90 年代中期标准化以来,表单就一直存在,并且可以做很多很酷的事情。我们将学习表单的一些基础知识,甚至还会添加一些事件处理程序,尽管我们还没有深入学习 React 的状态管理,但我们仍然会在表单和表单数据上进行一些很酷的操作。
你可以在 MDN 上学习到有关表单的所有知识,那里有很多有趣的内容。表单最常见的元素是 input
元素,它可以有多种类型,从颜色选择器到日期选择器等等,我们将探索其中的许多内容。
在网页平台上,当用户输入数据时,浏览器会触发事件,这样你的 JavaScript 就可以监听这些事件并执行相应的操作。在 React 中,我们使用 onChange
事件,而不是原生的 onInput
。虽然 onChange
事件在 React 中处理得很像 onInput
,但它实际上是等到输入框失去焦点时才会触发,这是一个小小的区别。
在默认情况下,当你提交表单时,浏览器会触发完整的页面刷新。如果你不希望页面刷新,比如在构建像 X(Twitter)这样的应用时,每次点赞都刷新页面显然不理想。为避免这种情况,你可以在 onSubmit
事件中调用 preventDefault()
来阻止页面刷新。
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
// 处理表单数据
};
关联输入框与其标签是非常重要的。通常,标签可以包裹输入框,也可以使用 htmlFor
属性将标签与具有相同 id
的输入框关联起来。这有助于提高表单的可访问性。
<label htmlFor="username">用户名:</label>
<input id="username" type="text" />
表单是一个庞大的主题,涉及的内容非常多。在本次练习中,我们将探索表单的许多基本功能,并学习如何通过 React 处理事件和表单数据。相信你会在这个过程中学到很多,并且乐在其中!
这段内容引导用户了解表单的基础知识,涵盖了事件处理、表单提交和标签关联等概念,同时使用了合适的代码示例。
我们现在从零开始,你的任务是将当前的待办事项(to-do)渲染成一个完整的表单。这张表单应该包含以下部分:
当你在输入框中输入内容并点击提交时,页面会执行一次完整的刷新。你会注意到,表单数据会显示在 URL 中(我们会在下一步中讨论这一点)。
相信你现在已经拥有足够的信息来开始这个任务了,动手试试吧!
这段内容引导你从头构建一个简单的表单,同时解释了页面刷新和 URL 传递表单数据的行为,并提供了一些额外的挑战,如正确关联标签和输入框。
我们将从渲染表单开始,首先创建一个基本的表单结构。
return (
<form>
<label htmlFor="username-input">用户名:</label>
<input id="username-input" type="text" />
<button type="submit">提交</button>
</form>
);
尽管不是强制要求的,但我通常会明确地为按钮设置 type
属性,尤其是在复杂应用中。如果没有明确设置按钮类型,可能会在不知情的情况下提交父级表单。所以在这例中,我们将按钮的 type
设置为 submit
。
如果我们没有设置 htmlFor
和 id
,当你点击标签时,输入框并不会获得焦点,而且屏幕阅读器也不会为输入框提供有意义的提示。因此,添加 id
和 htmlFor
属性是确保无障碍访问的重要步骤。
通过设置 htmlFor="username-input"
,并在输入框上添加 id="username-input"
,我们可以确保点击标签时,输入框会自动聚焦,同时屏幕阅读器能够正确读取标签的内容。
<label htmlFor="username-input">用户名:</label>
<input id="username-input" type="text" />
当你输入用户名并点击提交按钮时,页面会执行完整的刷新,提交的数据会显示在 URL 中。我们稍后会详细讨论这一点。
<form>
<label htmlFor="username-input">用户名:</label>
<input id="username-input" type="text" />
<button type="submit">提交</button>
</form>
通过本次练习,我们实现了一个基本的表单,确保了无障碍访问,并能够提交数据。你可以进一步优化样式,确保表单更符合实际需求。当提交表单时,数据会在 URL 中显示,我们会在后续的步骤中探讨如何处理这些数据。希望你已经完成了这个目标!
这个示例引导用户创建一个基本的表单,并介绍了设置 htmlFor
和 id
以确保无障碍访问,同时展示了如何处理表单提交。
当我们提交表单时,发生了一个完整的页面刷新。这是因为我们向服务器发送了请求,表单数据被序列化并附加到当前 URL 中。例如,当我输入用户名为 Kent C. Dodds 并点击提交时,浏览器会在 URL 中生成查询参数 ?username=Kent+C.+Dodds
,并触发页面刷新。
默认情况下,表单会向当前的 URL 发起请求。如果你想控制请求发送到的具体路径,可以使用表单的 action
属性。action
属性允许你指定请求发送到的服务器端点。
<form action="/submit-url">
<!-- 表单内容 -->
</form>
在本次练习中,我们有一个简单的后端 api.server
来处理表单提交。它已经为你设置好了 loader
,负责处理 GET
请求,并将表单数据传递给处理函数。在本次练习中,你的任务是为表单添加 action
,并查看提交后的效果。
<form action="/submit-form">
<label htmlFor="username-input">用户名:</label>
<input id="username-input" type="text" name="username" />
<button type="submit">提交</button>
</form>
通过设置 action
属性,你可以控制表单提交请求的路径。在这个练习中,设置 action
后,表单数据会发送到指定的路径,你可以通过后端处理这个请求并响应数据。试着为你的表单添加 action
属性并查看提交后的效果,提交应该非常快速完成。
这段内容解释了表单的默认提交行为,并引导你通过设置 action
属性来控制请求发送的路径。通过简单的示例代码,帮助你理解如何在实际开发中实现该功能。
现在,我们将为表单添加一个 action
属性,该属性设置为 /api/onboarding
,用于指定表单提交时的请求路径。
<form action="/api/onboarding">
<label htmlFor="username-input">用户名:</label>
<input id="username-input" type="text" name="username" />
<button type="submit">提交</button>
</form>
当你输入用户名并点击提交时,表单数据会被序列化为 URL 参数。例如,如果你输入了用户名 "JohnDoe",那么提交后的 URL 会变为:
/api/onboarding?username=JohnDoe
当表单数据被提交到 /api/onboarding
,我们的后端会处理请求,并返回一个包含所有序列化值的 HTML 文档,方便我们查看。
你可以将 action
设置为绝对路径或相对路径。绝对路径可能类似于 /app-playground/api/onboarding
,而相对路径则是 api/onboarding
。两者都会工作,只是取决于你想将请求发送到哪个路径。
<form action="/app-playground/api/onboarding">
<!-- 表单内容 -->
</form>
action
属性决定了表单数据提交的路径。你可以根据需要配置相对或绝对路径。当表单提交时,数据将被序列化并发送到指定的 URL。通过这种方式,你可以轻松控制表单的提交行为。
这个示例解释了如何通过 action
属性设置表单的提交路径,并展示了如何在 URL 中序列化表单数据并将其提交到服务器。
目前我们的表单只有用户名(username),但我们现在要进一步增强它,添加以下字段:
这些输入类型的详细信息可以在 MDN(Mozilla Developer Network)文档中找到。MDN 提供了各种输入类型的指南,帮助你理解它们如何在表单中工作。
<form action="/submit">
<label htmlFor="username">用户名:</label>
<input id="username" type="text" name="username" />
<label htmlFor="password">密码:</label>
<input id="password" type="password" name="password" />
<label htmlFor="age">年龄:</label>
<input id="age" type="number" name="age" />
<label htmlFor="photo">照片:</label>
<input id="photo" type="file" name="photo" />
<label htmlFor="favorite-color">最喜欢的颜色:</label>
<input id="favorite-color" type="color" name="favoriteColor" />
<label htmlFor="start-date">开始日期:</label>
<input id="start-date" type="date" name="startDate" />
<button type="submit">提交</button>
</form>
我们添加了各种类型的输入,包括文本框、密码框、数字输入、文件上传、颜色选择器和日期选择器。每种输入类型会影响表单提交时的数据格式。例如:
type="password"
会隐藏用户输入的内容。type="file"
会允许用户上传文件。type="color"
会显示颜色选择器。type="date"
会让用户选择日期。通过添加多种输入类型,我们增强了表单的功能,并且不同的输入类型在表单提交时会影响表单数据的格式。你可以参考 MDN 文档来深入了解每种输入类型的用法,并尝试不同的组合来提升表单的交互体验。
这个增强版表单添加了多种不同类型的输入字段,并解释了每种输入类型的作用及其对表单提交的影响。通过这个示例,你可以了解如何在表单中应用多种输入类型。
我们继续改进我们的表单,现在已经有用户名输入框了。接下来我们要添加以下字段:
我们将通过添加这些不同类型的输入,来了解它们如何影响表单提交。
密码输入框
type="password"
确保输入的内容不会明文显示。年龄输入框
type="number"
以确保用户只能输入数字。照片上传
type="file"
,并通过 accept="image/*"
限制用户只能上传图片文件。颜色选择器
type="color"
,允许用户选择自己喜欢的颜色。开始日期
type="date"
,让用户选择日期,虽然可能不是设计师理想的样式,但非常适合 MVP。<form action="/submit-form" method="get">
<label htmlFor="username">用户名:</label>
<input id="username" type="text" name="username" />
<label htmlFor="password">密码:</label>
<input id="password" type="password" name="password" />
<label htmlFor="age">年龄:</label>
<input id="age" type="number" name="age" />
<label htmlFor="photo">照片:</label>
<input id="photo" type="file" name="photo" accept="image/*" />
<label htmlFor="favorite-color">最喜欢的颜色:</label>
<input id="favorite-color" type="color" name="favoriteColor" />
<label htmlFor="start-date">开始日期:</label>
<input id="start-date" type="date" name="startDate" />
<button type="submit">提交</button>
</form>
当你输入所有信息并点击提交按钮后,页面会进行完整的刷新,并将表单数据以查询参数的形式添加到 URL 中。上传的图片文件只会传递文件名,而非实际的文件内容,这需要进一步的处理。
注意,当你点击浏览器的“返回”按钮时,所有的字段内容都会保留,除了密码。这是因为密码字段由于安全原因在浏览器中具有特殊的行为,它不会在返回后保留内容。
通过增强表单,我们添加了多种不同类型的输入字段,并理解了它们对用户体验的影响。未来我们将进一步处理照片上传的细节问题,敬请期待。
通过这次改进,我们将多个不同类型的输入字段添加到了表单中,并解释了每个字段的作用及其对表单提交的影响。这些字段有助于增强用户体验,同时为后续处理照片上传等高级功能奠定了基础。
我们在使用表单时发现了两个主要问题:
此外,表单每次提交都会导致页面刷新,这是我们希望避免的。
表单默认通过 GET
请求将数据序列化并附加到 URL 中,这对密码字段来说非常不安全。我们可以通过将表单的 method
属性设置为 POST
来解决此问题。POST
请求不会将数据附加到 URL 中,而是通过请求体发送。
<form action="/submit-form" method="post">
<label htmlFor="username">用户名:</label>
<input id="username" type="text" name="username" />
<label htmlFor="password">密码:</label>
<input id="password" type="password" name="password" />
<!-- 其他输入字段 -->
<button type="submit">提交</button>
</form>
对于文件上传,我们需要确保表单使用 enctype="multipart/form-data"
来正确传输二进制数据(如图片)。默认的 enctype
只支持发送简单的文本数据。
<form action="/submit-form" method="post" enctype="multipart/form-data">
<label htmlFor="photo">照片:</label>
<input id="photo" type="file" name="photo" accept="image/*" />
<!-- 其他输入字段 -->
<button type="submit">提交</button>
</form>
我们还希望在提交表单时避免页面刷新。为此,可以通过在 onSubmit
事件处理程序中调用 event.preventDefault()
来阻止默认的表单提交行为。
<form id="my-form" action="/submit-form" method="post" enctype="multipart/form-data">
<label htmlFor="username">用户名:</label>
<input id="username" type="text" name="username" />
<label htmlFor="password">密码:</label>
<input id="password" type="password" name="password" />
<label htmlFor="photo">照片:</label>
<input id="photo" type="file" name="photo" accept="image/*" />
<button type="submit">提交</button>
</form>
<script>
document.getElementById('my-form').addEventListener('submit', function(event) {
event.preventDefault();
// 处理自定义表单提交逻辑
});
</script>
method="post"
以防止密码出现在 URL 中。enctype="multipart/form-data"
以正确处理文件上传。event.preventDefault()
防止表单提交导致页面刷新。在每一步之后,可以测试表单的行为,并观察提交时数据的变化。最后一步,我们会将这些逻辑与后端 API 结合,查看它们如何协同工作。
通过这次练习,我们解决了表单提交时密码暴露和图片上传的问题,并防止了页面刷新。随着你添加每个属性和事件处理程序,你可以看到表单行为的变化。希望你在这次练习中有所收获!
这段内容详细介绍了如何解决表单中的常见问题,特别是防止密码暴露在 URL 中、正确处理图片上传以及防止页面刷新。通过逐步实现,用户可以观察每个更改的影响。
在默认情况下,表单通过 GET
请求提交,并将所有数据序列化到 URL 中。这在某些场景下是有用的,比如在电商网站上,你可能想将搜索条件保存在书签中,或与他人分享。但是,对于包含敏感信息的表单(如密码或上传的文件),这种方式显然不合适。
在我们的表单中,包含了密码和照片上传等敏感数据。如果使用 GET
请求,这些数据将出现在 URL 中,导致安全隐患。GET
请求的默认行为是将所有表单数据附加到 URL 中,因此我们需要使用 POST
请求来避免这一问题。
<form action="/submit-form" method="post">
<label for="username">用户名:</label>
<input id="username" type="text" name="username" />
<label for="password">密码:</label>
<input id="password" type="password" name="password" />
<button type="submit">提交</button>
</form>
通过将表单的 method
设置为 post
,表单数据将不会附加到 URL 中,而是通过请求体发送。这不仅保护了用户的隐私,还符合标准的安全实践。
当我们使用 POST
请求时,后端需要通过请求体来获取数据,而不是从 URL 中获取。在后端,我们使用 request.formData()
来处理请求体中的表单数据。
async function action({ request }) {
const formData = await request.formData();
const username = formData.get('username');
const password = formData.get('password');
// 处理表单数据...
}
通过 formData.get()
可以获取表单中的各个字段,并在后端进行处理。
对于上传文件,我们需要将表单的 enctype
设置为 multipart/form-data
,以便正确传输二进制数据(如图像文件)。默认的 application/x-www-form-urlencoded
只适用于简单的文本数据,而不能处理文件上传。
<form action="/submit-form" method="post" enctype="multipart/form-data">
<label for="photo">上传照片:</label>
<input id="photo" type="file" name="photo" accept="image/*" />
<button type="submit">提交</button>
</form>
在设置了 multipart/form-data
后,表单可以处理文件上传,并将文件以二进制的形式传输到服务器。
preventDefault()
为了避免提交表单时导致页面刷新,我们可以在 onSubmit
事件处理程序中调用 event.preventDefault()
来阻止默认的表单提交行为。
<form id="my-form" action="/submit-form" method="post" enctype="multipart/form-data">
<label for="username">用户名:</label>
<input id="username" type="text" name="username" />
<label for="password">密码:</label>
<input id="password" type="password" name="password" />
<label for="photo">照片:</label>
<input id="photo" type="file" name="photo" accept="image/*" />
<button type="submit">提交</button>
</form>
<script>
document.getElementById('my-form').addEventListener('submit', function(event) {
event.preventDefault();
// 处理自定义表单提交逻辑
});
</script>
通过 preventDefault()
,我们可以阻止页面刷新,并在客户端自行处理表单数据的逻辑。
我们可以在客户端使用 FormData
API 来解析表单数据并执行相关处理。例如,我们可以通过以下代码获取表单的所有数据,并以对象的形式输出:
document.getElementById('my-form').addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(event.target);
const entries = Object.fromEntries(formData.entries());
console.log(entries);
});
这段代码会将表单中的所有字段数据转换为对象格式,并输出到控制台。对于文件上传,FormData
API 还支持处理文件对象,因此你可以进一步操作上传的文件。
通过这次练习,我们了解了表单提交的几种方法以及如何通过 POST
请求和 multipart/form-data
来安全地处理表单数据。我们还探讨了如何在前端处理表单数据,并避免页面刷新。这些都是开发表单时非常重要的知识点。
通过这一段总结,我们解决了表单提交中密码暴露和文件上传的问题,同时还学习了如何在前端通过 preventDefault()
避免页面刷新并处理表单数据。
在处理表单的提交、方法和编码时,常常需要进行许多配置工作,包括设置 action
、encodingType
以及 onSubmit
事件。这些操作虽然可以手动完成,但在 React 中,有一种更简便的方式来处理表单。
React 提供了一些内置的功能,使得你不必重复实现这些常见的功能。本次练习中,我们将删除之前的额外配置,简化表单处理的方式,并且利用更简单的代码来完成表单的提交逻辑。
首先,我们可以移除手动设置的 action
、encodingType
和 onSubmit
事件处理逻辑。接下来,我们只需传递一个函数给 onSubmit
,这样我们就可以轻松管理表单的提交和处理。
以下是一个使用 React 内置功能简化表单处理的示例代码:
import React, { useState } from 'react';
function SimpleForm() {
const [formData, setFormData] = useState({
username: '',
password: '',
photo: null,
});
const handleSubmit = (event) => {
event.preventDefault();
console.log('Submitted data:', formData);
// 在这里处理提交逻辑,或者发送 API 请求
};
const handleChange = (event) => {
const { name, value, files } = event.target;
setFormData((prevData) => ({
...prevData,
[name]: files ? files[0] : value,
}));
};
return (
<form onSubmit={handleSubmit}>
<label>
用户名:
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
</label>
<br />
<label>
密码:
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
</label>
<br />
<label>
上传照片:
<input
type="file"
name="photo"
accept="image/*"
onChange={handleChange}
/>
</label>
<br />
<button type="submit">提交</button>
</form>
);
}
export default SimpleForm;
handleSubmit
函数:这个函数阻止了表单的默认提交行为,并通过 event.preventDefault()
来防止页面刷新。随后,我们可以根据需求处理表单数据,譬如发送请求到服务器。
handleChange
函数:这个函数会根据输入字段的变化动态更新表单的状态。对于 file
类型的输入,它会保存上传的文件对象,而不是其路径。
自动管理表单状态:我们通过 useState
钩子来管理表单的数据,使得每次输入都能够自动更新。
通过这种方式,你可以轻松管理复杂的表单逻辑,无需担心手动处理编码类型或防止页面刷新。React 的组件化模型和状态管理极大简化了表单处理的流程。
这样,你就可以专注于实际的业务逻辑,而不用再为表单的基础设施烦恼。希望你能通过这一简化体验到 React 的便利性。
在这部分内容中,我们了解到如何利用 React 内置功能来简化表单处理。通过传递一个函数给 action
,React 能够处理很多繁琐的表单提交逻辑,并自动处理一些常见的操作,比如表单重置和防止默认行为。
我们将通过重构代码,删除不必要的手动逻辑,利用 React 自动处理表单数据的能力。
import React from 'react';
function SimpleForm() {
const logFormData = (formData) => {
const entries = Object.fromEntries(formData.entries());
console.log('Submitted form data:', entries);
};
return (
<form action={logFormData}>
<label>
用户名:
<input type="text" name="username" defaultValue="Kent" />
</label>
<br />
<label>
密码:
<input type="password" name="password" />
</label>
<br />
<label>
年龄:
<input type="number" name="age" defaultValue={30} />
</label>
<br />
<button type="submit">提交</button>
</form>
);
}
export default SimpleForm;
logFormData
函数:我们创建了一个 logFormData
函数来接收并处理表单数据。表单数据以 formData
对象的形式传递给 action
,并通过 Object.fromEntries
方法将其转换为普通对象,方便输出。
action
属性:我们将 action
设置为 logFormData
函数。React 将自动处理表单提交时的各种操作,如阻止默认提交行为,获取表单数据,并传递给 logFormData
进行处理。
表单重置:React 自动处理表单提交后的重置行为。提交表单后,表单将会重置为默认值。
减少手动操作:我们不再需要手动处理 onSubmit
、preventDefault()
或其他繁琐的提交逻辑,React 内置的功能已经帮我们做了这些工作。
自动重置表单:表单提交后,React 会自动重置表单为初始状态,避免用户重复输入相同信息。
在表单重置时,如果你没有为某些字段设置默认值(defaultValue
),你可能会看到浏览器的警告。此时,React 尝试将表单重置为其默认状态,但可能遇到无法重置为空值的问题。
通过这种方式,React 让表单处理变得更加简洁和高效。你可以专注于表单数据的业务逻辑,而无需处理表单提交的底层细节。这种方式不仅提升了开发体验,还使代码更加简洁易维护。
在本次练习中,我们将探讨各种输入类型及其在 React 中的处理细节。虽然很多信息可以在 MDN Web 文档中找到,但 React 引入了一些特定的行为,尤其是在处理 defaultValue 和 value 属性时,这些是你需要掌握的关键。
我们将处理以下输入类型:
defaultValue
:当你希望设置一个初始值供用户修改时使用。value
:用于受控组件(controlled components),其输入值与组件的状态直接关联。value
属性,但在 React 中,通常会使用 defaultValue
来处理非受控组件,或者使用 value
来处理受控组件。接下来,我们将结合 Web 标准和 React 特有的行为,逐步理解这些输入类型在 React 中的工作原理。
在本次练习中,我们将为复选框创建一个标签并讨论如何根据设计需求摆放复选框及其对应的文本。
复选框通常会被放置在文本的旁边,例如复选框在前,文本在后。虽然在某些设计中,你可能希望以不同的方式来排布这些元素,但这种设置在很多情况下是常见的,特别是当你希望标签直接与复选框并排显示时。
<label>
):将文本放置在标签内,复选框也嵌套其中。这样,当你点击文本时,也可以选择复选框。checkbox
:这是复选框的主要属性,不同于文本输入框(text
),这里我们会使用 checkbox
。以下是代码示例:
<label>
<input type="checkbox" name="visibility" />
显示内容
</label>
通过这种方式,我们可以快速创建一个符合用户体验的复选框和标签布局。在实际应用中,你可以根据需要调整样式或布局来优化视觉效果和用户交互。
在本次练习中,我们继续探讨如何在 React 中创建复选框,并深入理解复选框在表单提交时的数据处理方式。
首先,我们会在表单中创建一个复选框,并将标签置于复选框的右侧,这是一种常见的布局方式。复选框的 type
属性应设置为 "checkbox"
。
<div>
<label>
<input type="checkbox" name="waiver" />
我已阅读并同意条款
</label>
</div>
当我们提交表单时,复选框的行为有两个关键点:
name
属性及其值为 "on"
。也就是说,当复选框被选中时,复选框的 name
对应的值会是 "on"
,这与我们通常期望的 true/false
不同。name
。这意味着复选框未被选中时,不会在提交的数据中体现出来。<form>
<input type="text" name="username" placeholder="用户名" />
<input type="checkbox" name="waiver" />
<button type="submit">提交</button>
</form>
当你提交表单后,如果复选框被选中,表单数据中的 waiver
字段会显示为 "on"
。如果没有选中,表单数据将不会包含 waiver
字段。这种行为是浏览器原生表单处理的一部分,与 React 无关。
当复选框被选中并提交时,表单数据可能如下:
{
"username": "Kent C. Dodds",
"waiver": "on"
}
如果复选框未选中,数据可能如下:
{
"username": "Kent C. Dodds"
}
复选框在表单数据中的表示形式比较特殊,它在选中时会传递 "on"
作为值,而在未选中时则不会传递任何信息。这是 Web 平台的默认行为,了解这一点对你处理表单数据非常有帮助。在实际开发中,你可能会使用某些库来处理这类行为,但理解基础仍然很重要。
在本次练习中,我们将创建一个下拉选择框,用于表单中选择不同的用户类型。下拉框包含四种用户类型:管理员、老师、家长和学生。默认选项将是 "请选择一个选项",并且在提交时如果没有选择具体的用户类型,表单将返回一个空字符串。
我们将使用 select
和 option
元素来实现下拉框选择功能。每个选项代表一个用户类型,包括管理员、老师、家长和学生。
<form>
<label htmlFor="accountType">用户类型:</label>
<select name="accountType" id="accountType">
<option value="">请选择一个选项</option>
<option value="admin">管理员</option>
<option value="teacher">老师</option>
<option value="parent">家长</option>
<option value="student">学生</option>
</select>
<button type="submit">提交</button>
</form>
当表单提交时,如果用户没有选择任何有效选项(选择的是 "请选择一个选项"),表单数据中的 accountType
字段将为空字符串。如果用户选择了其他选项,比如 "管理员"、"老师" 等,提交的数据将会包含对应的值。
<form onSubmit={handleSubmit}>
<label htmlFor="accountType">用户类型:</label>
<select name="accountType" id="accountType">
<option value="">请选择一个选项</option>
<option value="admin">管理员</option>
<option value="teacher">老师</option>
<option value="parent">家长</option>
<option value="student">学生</option>
</select>
<button type="submit">提交</button>
</form>
在 handleSubmit
函数中,我们可以处理选择框的数据:
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const accountType = formData.get('accountType') || '';
console.log('选择的用户类型:', accountType);
};
"admin"
。accountType
将为空字符串。{
"accountType": "admin"
}
{
"accountType": ""
}
通过这次练习,你可以理解如何在 React 中使用 select
和 option
元素创建下拉选择框,并处理其表单提交数据。掌握这些基础知识对构建动态表单应用非常重要。
select
元素创建下拉菜单在这次练习中,我们将使用 select
元素和 option
元素来创建一个下拉菜单,允许用户选择角色类型,比如管理员、教师、家长或学生。我们还会添加一个默认选项 "请选择一个选项",并确保当该选项被选择时,提交的表单数据将为空字符串。
使用 select
元素包裹多个 option
元素,每个 option
代表一个可供选择的角色类型。
<form>
<label htmlFor="accountType">用户类型:</label>
<select name="accountType" id="accountType">
<option value="">请选择一个选项</option>
<option value="admin">管理员</option>
<option value="teacher">教师</option>
<option value="parent">家长</option>
<option value="student">学生</option>
</select>
<button type="submit">提交</button>
</form>
通过 handleSubmit
函数获取表单数据,并处理下拉菜单的值。确保在用户没有选择有效选项时,accountType
返回空字符串。
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const accountType = formData.get('accountType') || '';
console.log('选择的用户类型:', accountType);
};
value
,比如 "admin"
或 "teacher"
。根据选择不同的用户类型,提交的数据如下所示:
{
"accountType": "admin"
}
{
"accountType": ""
}
select
和 option
元素提供了一种简单的方式来创建下拉菜单,用户可以通过选择不同的选项进行表单提交。根据顺序排列的 option
元素决定默认的显示值,开发者可以通过 value
属性自定义每个选项的提交值。
在这次练习中,我们将使用单选按钮(radio buttons)来创建一个只能选择一个选项的表单组。单选按钮与 select
类似,因为它们都只允许用户选择一个值;同时与复选框也有相似之处,因为你需要使用 checked
属性来判断某个选项是否被选中。
我们还将使用 fieldset
元素来包裹一组单选按钮,并用 legend
为整个选项组添加描述性标签。
fieldset
和 legend
fieldset
元素用于包裹一组相关的表单元素,而 legend
用来为这组元素添加描述性标签。
<form>
<fieldset>
<legend>请选择您的角色</legend>
<label>
<input type="radio" name="role" value="admin" /> 管理员
</label>
<label>
<input type="radio" name="role" value="teacher" /> 教师
</label>
<label>
<input type="radio" name="role" value="parent" /> 家长
</label>
<label>
<input type="radio" name="role" value="student" /> 学生
</label>
<button type="submit">提交</button>
</fieldset>
</form>
通过 handleSubmit
函数获取表单数据,处理单选按钮的值。确保在用户选择不同选项时,能够正确获取选中的角色。
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const role = formData.get('role');
console.log('选择的角色:', role);
};
name
属性必须相同,这样才能确保用户在提交表单时只能选择一个选项。checked
属性可以用来判断某个选项是否被选中。根据选择不同的角色类型,提交的数据如下所示:
{
"role": "admin"
}
{
"role": "student"
}
通过使用 fieldset
和 legend
包裹一组单选按钮,用户可以在多个选项中选择一个,并且这些选项会有一个描述性标签。
fieldset
实现选择功能在这个练习中,我们使用单选按钮和 fieldset
来创建一个可供用户选择的分组。fieldset
和 legend
的作用是为这组单选按钮提供一个整体的标签,确保屏幕阅读器和其他辅助技术可以正确理解这些选项的含义。
fieldset
和 legend
fieldset
元素包裹一组相关的表单元素,而 legend
用来为这组元素添加描述性标签:
<form>
<fieldset>
<legend>选择可见性</legend>
<label>
<input type="radio" name="visibility" value="public" /> 公开
</label>
<label>
<input type="radio" name="visibility" value="private" /> 私密
</label>
<label>
<input type="radio" name="visibility" value="unlisted" /> 未列出
</label>
<button type="submit">提交</button>
</fieldset>
</form>
通过 handleSubmit
函数处理表单数据,判断用户选择了哪个可见性选项:
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const visibility = formData.get('visibility');
console.log('选择的可见性:', visibility);
};
name
,这使得用户在提交表单时只能选择一个选项。value
属性决定了提交的表单数据中与该单选按钮相关联的值。提交表单时,如果选择了 "公开":
{
"visibility": "public"
}
选择 "未列出":
{
"visibility": "unlisted"
}
如果未选择任何选项,表单数据中将不会包含 visibility
键。
通过使用 fieldset
和 legend
,我们可以为一组单选按钮提供一个整体的描述性标签。这样,用户和辅助技术都可以更清楚地知道这些选项是为哪个设置服务的。
在某些情况下,您需要将一些额外的数据提交给服务器,但不希望用户手动输入。这时可以使用隐藏的输入字段 (hidden input
)。这些字段不会显示在用户界面上,但会随表单数据一起提交。
隐藏输入字段通过 type="hidden"
实现。它的作用是存储一些信息,这些信息不会显示给用户,但在表单提交时会被发送到服务器。
<form>
<label>
任务名称:
<input type="text" name="taskName" />
</label>
<input type="hidden" name="taskId" value="12345" />
<button type="submit">提交</button>
</form>
当用户提交表单时,taskId
作为隐藏输入字段将随表单一起发送到服务器:
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const taskId = formData.get('taskId');
const taskName = formData.get('taskName');
console.log('任务 ID:', taskId);
console.log('任务名称:', taskName);
};
隐藏输入字段可以在以下情况下使用:
在表单提交时,隐藏的 taskId
将与其他可见字段(如任务名称 taskName
)一起提交。提交的数据将如下所示:
{
"taskName": "写作任务",
"taskId": "12345"
}
隐藏输入字段是一种有效的方式,可以在不影响用户界面的情况下将额外的数据发送到服务器。这些字段通常用于提交与特定记录相关的 ID 或其他无需用户输入的参数。
在构建动态应用时,隐藏输入字段可以用于传递诸如组织 ID 之类的值,而无需用户交互或看到这些值。下面是一个使用隐藏输入字段的示例。
我们在表单中添加一个 org_id
的隐藏字段,值为 123
。在实际应用中,这个值可能是动态的,基于应用程序的状态。
<form>
<label>
用户名:
<input type="text" name="username" />
</label>
<input type="hidden" name="org_id" value="123" />
<button type="submit">提交</button>
</form>
当表单提交时,org_id
隐藏字段的数据将与其他表单字段一起发送到服务器,尽管用户看不到它。在提交请求时,数据将如下所示:
{
"username": "example_user",
"org_id": "123"
}
在浏览器的开发者工具中,虽然用户看不到隐藏输入,但你仍然可以在 DOM 中看到它:
<input type="hidden" name="org_id" value="123">
如果应用需要动态设置 org_id
,你可以在 JavaScript 中更改这个隐藏字段的值。例如:
document.querySelector('input[name="org_id"]').value = '456';
隐藏输入字段的常见使用场景:
总结:隐藏输入字段是一种强大且灵活的方式,用于提交用户看不到但需要传递到服务器的额外数据。
在 React 中处理表单时,了解如何使用默认值来初始化表单元素非常重要。和传统的 HTML 一样,表单元素可以使用 value
和 checked
等属性。但由于 React 强调动态性,使用这些属性的方式有所不同。
value
vs defaultValue
在 HTML 中,你可以通过 value
属性来设置输入框的初始值。但在 React 中,如果直接使用 value
属性,这个输入框将变为“受控组件”,React 将完全控制其值。这意味着如果你使用 value
,用户无法更改输入框的内容,除非你在代码中明确允许。
示例:使用 value
<input type="text" value="Kent C. Dodds" />
在这个例子中,输入框的值始终是 "Kent C. Dodds",用户无法编辑它。
defaultValue
如果你想初始化输入框的值,但允许用户修改它,则需要使用 defaultValue
。这是 React 的特有属性,HTML 中没有这个属性。
示例:使用 defaultValue
<input type="text" defaultValue="Kent C. Dodds" />
在这个例子中,输入框的初始值是 "Kent C. Dodds",但用户可以自由修改。
<input type="text">
):可以使用 defaultValue
。<input type="checkbox">
和 <input type="radio">
):可以使用 defaultChecked
,而不是 checked
。<input type="date">
):defaultValue
的格式需要符合日期格式(如 YYYY-MM-DD
)。<input type="file">
):文件输入不能有默认值。<form>
<label>
用户名:
<input type="text" defaultValue="Kent C. Dodds" />
</label>
<label>
年龄:
<input type="number" defaultValue={30} />
</label>
<label>
公共账户:
<input type="checkbox" defaultChecked={true} />
</label>
<label>
开始日期:
<input type="date" defaultValue="2024-10-23" />
</label>
<button type="submit">提交</button>
</form>
defaultValue
只在第一次渲染时有效:之后如果你修改它,React 不会自动更新 UI。defaultChecked
:而不是 defaultValue
。YYYY-MM-DD
的字符串格式,确保浏览器可以正确解析。总结:在 React 中,使用 defaultValue
和 defaultChecked
可以设置表单元素的初始值,同时允许用户修改这些值。这与直接使用 value
和 checked
不同,后者将使表单元素成为受控组件。
在这个视频中,我们讨论了如何在 React 中为表单元素设置默认值 (defaultValue
和 defaultChecked
),以便用户可以更改输入,同时保留动态性。这与直接使用 value
和 checked
的不同之处在于,defaultValue
和 defaultChecked
允许用户更新输入,而不会被 React 完全控制。
defaultValue
和 defaultChecked
的使用defaultValue
用于初始化输入框的值,例如账号类型的默认值设为 student
。defaultChecked
用来设置默认选择项,例如将隐私设置默认设为公开 (public
)。<input type="number">
): 你可以设置默认值为特定年龄,例如默认 18 岁。<input type="color">
): 可以设置默认颜色,比如学校的代表色。<input type="date">
): 日期输入的默认值必须采用 YYYY-MM-DD
格式,你可以动态生成今天的日期。<input type="checkbox">
): 可以使用 defaultChecked
设置默认勾选状态。<form>
<label>
账号类型:
<select defaultValue="student">
<option value="admin">管理员</option>
<option value="teacher">教师</option>
<option value="parent">家长</option>
<option value="student">学生</option>
</select>
</label>
<label>
年龄:
<input type="number" defaultValue={18} min={0} max={200} />
</label>
<label>
默认颜色:
<input type="color" defaultValue="#0033A0" />
</label>
<label>
开始日期:
<input type="date" defaultValue={new Date().toISOString().slice(0, 10)} />
</label>
<label>
公开账户:
<input type="radio" name="visibility" value="public" defaultChecked />
</label>
<label>
私密账户:
<input type="radio" name="visibility" value="private" />
</label>
<label>
签署协议:
<input type="checkbox" defaultChecked />
</label>
<button type="submit">提交</button>
</form>
如果你需要为日期输入动态生成默认值(如当天日期),你可以使用 new Date().toISOString().slice(0, 10)
获取 YYYY-MM-DD
格式的日期。
defaultValue
和 defaultChecked
只在初次渲染时生效。之后如果需要更新它们的值,你需要使用受控组件的方式。通过使用 defaultValue
和 defaultChecked
,你可以在 React 中灵活设置表单元素的初始值,同时保持用户可以修改这些值的自由。
在这个课程中,我们将重点学习 错误边界(Error Boundaries),了解如何在 React 应用中优雅地处理错误。当应用发生错误时,错误边界可以防止用户看到空白页面,转而显示友好的错误提示或备用内容,从而提升用户体验。
setTimeout
)、服务端渲染的错误,或者 React 内部的错误。使用类组件创建错误边界:
componentDidCatch()
和 getDerivedStateFromError()
方法来处理错误。React Error Boundary 包:
处理异步错误:
fetch
请求或 setTimeout
)中发生的错误需要通过 try/catch
或 promise.catch
来处理。通过 React Error Boundary 提供的 useErrorBoundary()
钩子,可以在发生异步错误时触发错误边界。多重错误边界:
try/catch
块一样,你可以在应用中使用 嵌套错误边界。根据错误边界放置的位置,错误会被局部处理,这样可以为不同的 UI 部分提供更具体的错误提示或备用内容。在这个练习中,你需要:
通过使用错误边界,你可以增强应用的健壮性,在出错时为用户提供有意义的反馈,提升整体的用户体验。
在这个练习中,我们要处理在 React 应用中出现的意外错误,比如无效的时间值错误。在实际开发中,用户体验非常重要,即使出现错误,我们也希望能优雅地处理,而不是让应用崩溃。为此,我们将使用 错误边界(Error Boundary) 来捕获这些错误并显示一个备用 UI。
fallback
组件来展示错误发生时的友好提示,而不是让整个页面崩溃或显示空白。安装 React Error Boundary:
npm install react-error-boundary
来安装错误边界库,这是一个非常常用的第三方包,简化了 React 中错误处理的实现。设置错误边界:
ErrorBoundary
来包裹应用的其他部分。这样,当发生错误时,React 会渲染我们自定义的 fallback
组件。添加 fallback 组件:
ErrorFallback
组件,用来显示错误信息,并允许用户重新加载应用或继续操作。处理错误:
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>发生错误:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
function MyApp() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// 重置逻辑,例如重载页面或重置状态
}}
>
<YourMainApp />
</ErrorBoundary>
);
}
export default MyApp;
在这个实现中,ErrorBoundary
组件会捕获应用中可能出现的任何错误。无论是时间格式错误还是其他未处理的异常,应用都会显示我们定义的 ErrorFallback
组件,用户可以通过重试按钮重新加载应用。
通过这种方式,你不仅可以提升用户体验,还可以在生产环境中捕获更多的潜在错误,确保应用的健壮性。
在这个练习中,我们使用 Error Boundary(错误边界)来处理 React 应用中出现的错误。通过这种方式,当应用中某个组件抛出错误时,我们可以优雅地捕获并向用户提供友好的错误提示,而不是让整个应用崩溃。
引入 Error Boundary 和 Fallback Props:
首先我们从 react-error-boundary
中引入 ErrorBoundary
组件和 FallbackProps
类型,用于处理错误和定义当错误发生时显示的组件。
重构代码以添加 Error Boundary:
我们不能简单地在错误发生的地方添加 ErrorBoundary
,因为在组件还没有完全创建之前,错误已经发生了。所以需要先将应用的主要部分封装为一个 onboardingForm
组件,然后在外部包裹 ErrorBoundary
。
创建错误回退组件:
我们定义了一个 ErrorFallback
组件,用于在发生错误时向用户展示错误信息。通过接收 FallbackProps
,我们可以显示错误的具体信息,或者提供重试按钮,让用户重新加载页面。
ErrorBoundary 使用:
使用 ErrorBoundary
包裹 onboardingForm
,并传入 fallbackComponent
属性指定错误发生时显示的 UI。
import React from 'react';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>发生错误:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
function OnboardingForm() {
// 主应用代码
return <div>欢迎来到我们的应用!</div>;
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// 可选:在错误重置时执行的逻辑
}}
>
<OnboardingForm />
</ErrorBoundary>
);
}
export default App;
FallbackComponent
来处理错误显示。error
和 resetErrorBoundary
,允许我们根据错误类型自定义显示的信息,并为用户提供重试的机会。通过这种方式,React 应用的用户体验大大提升。即使发生了错误,用户也不会看到空白页面或崩溃的应用,而是能获得有用的信息或选择重新尝试。
在这个练习中,我们将探讨如何处理在 React 应用中的 异步错误。通常,try-catch
结构在同步代码中非常有效,但当代码涉及到异步操作或事件处理时,错误可能不会被 try-catch
捕捉到。
当我们在编写代码时,如果某些操作(例如事件处理器或异步回调函数)发生错误,try-catch
无法捕获这些错误。这是因为这些操作运行在不同的上下文中。React 也有类似的限制:它能够捕获同步渲染中的错误,但不能自动捕获异步错误或事件处理中的错误。
例如,在以下场景中,当用户点击“提交”按钮时,如果代码中有错误(如调用 null
对象的方法),错误不会被 React 直接捕获,而是会抛出到全局上下文中:
function handleSubmit(event) {
event.preventDefault();
console.log(accountType.toUpperCase()); // 如果 accountType 为 null,就会抛出错误
}
你需要确保这些类型的错误可以被捕获并且向用户提供有意义的错误信息,而不是让应用崩溃。
使用 Error Boundaries 处理渲染错误:
对于渲染过程中发生的错误,我们可以使用 ErrorBoundary
。然而,React 的 ErrorBoundary
无法处理 事件处理器 或 异步代码 中的错误。
使用 Try-Catch 或 Promise Catch:
对于异步操作和事件处理器,我们需要手动捕获错误,通常使用 try-catch
或在 Promise 中使用 .catch
。
让我们为 handleSubmit
函数添加一个 try-catch
语句来捕获可能发生的错误:
function handleSubmit(event) {
event.preventDefault();
try {
// 可能抛出错误的代码
console.log(accountType.toUpperCase());
} catch (error) {
console.error("捕获到错误:", error.message);
alert("发生了错误,请稍后重试。");
}
}
捕获事件处理中的错误:
在事件处理函数中包裹 try-catch
,这样我们就能捕获到任何可能抛出的错误并为用户显示有意义的错误信息。
处理异步操作中的错误:
对于异步操作,我们可以在 .catch()
方法中处理错误。例如:
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error("异步操作错误:", error);
alert("获取数据时发生了错误,请稍后重试。");
}
}
通过这些方法,我们可以确保 React 应用在处理异步操作或事件处理器中的错误时,仍然能提供良好的用户体验,避免应用崩溃或显示不友好的错误页面。
在这个练习中,我们重点学习了如何处理异步操作或事件处理程序中抛出的错误,特别是当 React 的错误边界(Error Boundary)无法自动捕获这些错误时。我们使用 useErrorBoundary
钩子来手动抛出错误并显示错误边界。
通常,React 只能在渲染过程中捕获同步错误。但在某些情况下,例如事件处理器或异步回调,错误是在不同的执行上下文中抛出的,React 无法自动捕获这些错误。这时,我们可以使用 try-catch
来捕获同步错误,或者在 Promise 中使用 .catch()
来处理异步错误。
在这个例子中,我们使用了 useErrorBoundary
钩子,该钩子为我们提供了一个 showBoundary
函数。这个函数可以手动触发错误边界,显示给用户预定义的错误消息。
以下是如何在提交表单时捕获错误并使用 showBoundary
触发错误边界的示例代码:
import { useErrorBoundary } from 'react-error-boundary';
function handleSubmit(event) {
event.preventDefault();
try {
// 模拟一个可能抛出错误的操作
console.log(accountType.toUpperCase());
} catch (error) {
// 如果发生错误,使用 showBoundary 显示错误边界
showBoundary(error);
}
}
function MyComponent() {
const { showBoundary } = useErrorBoundary();
return (
<form onSubmit={handleSubmit}>
<button type="submit">提交</button>
</form>
);
}
handleSubmit
)中,使用 try-catch
来捕获任何可能抛出的错误。catch
块中,调用 showBoundary(error)
,这会触发 React 的错误边界并显示错误消息。.catch()
:对于异步操作,您可以使用 .catch()
来处理 Promise 的错误,并同样使用 showBoundary
触发错误边界。通过使用 useErrorBoundary
钩子,我们能够在 React 无法自动捕获的情况下手动触发错误边界,为用户提供更好的错误处理体验。这种方法可以确保即使在异步操作或事件处理程序中发生错误,应用也能优雅地处理这些错误并向用户显示有意义的反馈。
在这个练习中,我们学习了如何使用 resetErrorBoundary
方法,让用户在遇到错误后可以尝试重新执行操作。例如,网络连接问题可能导致错误,用户重新连接后可能希望再次尝试提交表单或进行其他操作。
error fallback
组件的一个 prop 提供的,允许用户在遇到错误后重置错误边界。resetErrorBoundary
后,错误边界会重置组件的状态,就像页面或组件第一次加载时的状态。这意味着之前的状态不会保留,因此如果状态引发了错误,重置后组件将恢复到初始状态。假设你有一个错误边界和一个 error fallback
组件,用户可以点击按钮尝试重新执行操作:
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function MyComponent() {
const handleSubmit = () => {
// 模拟错误
throw new Error("Oops, something went wrong!");
};
return (
<div>
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
export default function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
>
<MyComponent />
</ErrorBoundary>
);
}
在 ErrorFallback
中使用 resetErrorBoundary
:当用户点击 "Try again" 按钮时,resetErrorBoundary
方法会被调用,重置错误边界。
错误重置的行为:组件将重新加载,恢复到最初的状态。如果用户的输入数据很重要(例如一个大的表单),你可能需要将数据存储在本地存储或其他持久化存储中,以便在重置后重新加载这些数据。
通过使用 resetErrorBoundary
,我们能够提供一种优雅的方式,允许用户在遇到错误时重试操作。这是处理网络问题、临时性错误等场景的常见方法。
在这个练习中,我们添加了一个功能,让用户在遇到错误时能够点击“再次尝试”按钮,从而重置错误边界,而不需要刷新整个页面。这通过使用 resetErrorBoundary
方法来实现,提供了一个更加流畅的用户体验。
从 props 中获取 resetErrorBoundary
:通过将 resetErrorBoundary
从 error fallback
组件的 props 中解构出来,我们能够在需要时调用它来重置错误状态。
创建“再次尝试”按钮:我们为按钮添加了一个 onClick
事件处理程序,点击该按钮时会调用 resetErrorBoundary
函数来重置错误边界,重新渲染组件。
重置后重新加载组件:当用户点击“再次尝试”时,组件会重新挂载,所有的输入状态(如表单数据)都会被重置。因此,如果用户希望保留他们在表单中的输入数据,可能需要在更高层级保存状态,或者使用浏览器的本地存储等方法来保存数据。
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function MyComponent() {
const handleSubmit = () => {
// 模拟错误
throw new Error("Oops, something went wrong!");
};
return (
<div>
<button onClick={handleSubmit}>Submit</button>
</div>
);
}
export default function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<MyComponent />
</ErrorBoundary>
);
}
resetErrorBoundary
方法,重置错误边界并重新加载组件。通过使用 resetErrorBoundary
,我们为用户提供了一个机会,在遇到错误时能够轻松地再次尝试提交,而不需要刷新整个页面。这种方式提升了用户体验,尤其是在发生偶发错误时,如网络错误等。
在这段讲解中,讲述了在 React 中渲染数组时需要使用 key
属性的原因,以及如何通过 key
来帮助 React 跟踪 DOM 元素的变化。
渲染数组:
li
列表项。key
属性,React 将无法有效跟踪 DOM 中每个元素的变化,从而无法优化渲染过程。为什么需要 key
属性:
key
属性帮助 React 唯一标识每个元素,从而准确地更新 DOM,而不是重新渲染整个列表。key
,React 无法正确处理动态变化的列表,并且可能导致不必要的 DOM 操作,或者出现错误的更新。如何选择 key
属性:
key
应该是一个唯一标识符,如数据库中的 id
。id
,可以使用数组中的内容作为 key
,但这需要确保内容是唯一的。key
,除非数组的顺序永远不会改变。key
的用途不仅仅是渲染数组:
key
还可以用于组件的重置和重新渲染。如果你为一个组件提供一个不同的 key
,React 将销毁旧组件并创建一个新的组件实例。const items = ['One', 'Two', 'Three'];
function List() {
return (
<ul>
{items.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}
在这个例子中,我们为每个 li
元素添加了 key
属性,使用 item
本身作为唯一标识符。React 现在可以正确地跟踪这些元素的变化。
key
是至关重要的,这样可以确保 React 只会对那些确实改变的元素进行更新,而不会重新渲染整个列表。key
属性,这有助于避免潜在的性能问题和错误。希望通过这个练习,你能理解为什么在 React 中渲染数组时需要 key
,以及如何正确使用 key
属性。
在这个练习中,我们要解决的是 React 渲染列表时没有 key
属性导致的问题。
当我们在 React 中使用数组来渲染 UI 元素(如列表)时,如果没有为每个列表项 (li
) 提供唯一的 key
,React 在处理列表项的动态变化时可能会做出错误的猜测。例如,当你删除或添加某个列表项时,React 可能会错误地更新现有的元素,而不是移除或添加新的元素,从而导致 UI 显示错误。
例如,当你删除列表中的第一个元素时,React 可能会认为你只是修改了第一个元素的文本,而不会删除它并重新渲染。这样的话,列表的状态(例如输入框的内容)会被错误地保留,导致不正确的结果。
为了解决这个问题,我们需要给每个 li
元素添加一个唯一的 key
属性,通常可以使用列表项的唯一标识符(例如 id
)作为 key
。这样 React 在遇到列表项的增删或顺序变化时,能够正确地识别每个元素,确保更新和删除操作能够正确执行。
假设我们有一个水果列表,通过点击按钮来删除某些水果:
import React, { useState } from 'react';
const fruits = [
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Orange' },
];
function FruitList() {
const [fruitList, setFruitList] = useState(fruits);
const removeFruit = (id) => {
setFruitList(fruitList.filter(fruit => fruit.id !== id));
};
return (
<ul>
{fruitList.map(fruit => (
<li key={fruit.id}>
<input type="text" value={fruit.name} readOnly />
<button onClick={() => removeFruit(fruit.id)}>Remove</button>
</li>
))}
</ul>
);
}
export default FruitList;
在这个例子中,我们为每个 li
元素指定了 key={fruit.id}
,以确保 React 能够正确识别每个列表项。当你删除某个水果时,React 会使用 key
来正确更新 DOM,移除相应的 li
元素,并且不会错误地保留其他元素的状态。
通过为每个列表项添加唯一的 key
属性,React 能够更智能地处理 DOM 的更新,从而避免一些因猜测不准而导致的 UI 错误。这个练习帮助你理解在 React 中管理动态数组时,使用 key
属性的重要性。
在这个片段中,我们讨论了如何使用 key
属性来帮助 React 更好地管理列表元素的更新。
map
渲染数组时,React 需要通过 key
属性来跟踪每个列表项的变化。key
,React 可能会错误地猜测哪些元素应该被更新或删除,导致 UI 不一致。li
元素添加一个唯一的 key
属性(例如 item.id
),React 可以更精确地处理 DOM 更新,确保列表项能够正确添加、删除或更新。li
提供唯一的 key
(如 item.id
),React 可以识别哪个元素被移除或新增,并相应地更新 DOM。key
属性,React 可能会保留现有的 li
元素,只更新其内容而不是删除或重新渲染该元素。key
属性是确保 React 正确更新和管理动态列表的关键工具。通过为列表中的每个项提供唯一标识,React 可以精确跟踪元素的变化,从而优化渲染过程并确保数据和界面保持一致。
在这个步骤中,我们深入探讨了 key
属性在 React 中的作用,特别是它对输入组件状态、焦点状态等的影响。
key
属性的影响:
key
影响 React 如何处理列表元素的 DOM 更新,特别是在重新排序或删除时,确保组件的状态(如输入内容)得以正确保留。key
设置得当,React 能够精确地跟踪哪些元素被添加、删除或更新,从而维护输入框的状态或焦点。使用索引 (index
) 作为 key
的问题:
key
并不能解决所有问题,尤其是在列表项目动态变化时。key
时,可能会导致 React 在重新渲染时无法正确跟踪组件状态,导致 UI 行为与预期不符。例如,在列表项被重新排序或删除时,输入框的状态可能会混乱。探索和体验:
key
会对输入状态和焦点产生怎样的影响。key
时,React 可能会不恰当地更新组件,而不是保留用户输入或焦点。通过这个练习,目的是让你理解 key
属性在 React 中的关键性作用。当正确使用 key
时,React 能够精确地管理列表元素及其状态,避免由于不正确的 key
设置引发的状态丢失或 UI 错乱的情况。
在这个视频片段中,重点讲解了在 React 中 key
属性的关键作用,特别是当你动态更新列表项时,它如何影响焦点状态、输入状态以及组件的渲染行为。
选择项的丢失:
key
或使用不当的 key
(例如数组索引)时,React 不能准确跟踪列表项的变化。导致当你选择某个项目后,因重新排序或列表变动,选中的项可能被错误地更新或丢失。React 的最佳猜测:
key
,React 可能会错误地认为某个列表项只是内容更新,而不是位置移动或项目被替换。这可能导致焦点状态丢失、输入框中的文本消失或组件状态重置。key
的重要性:
key
可以帮助 React 识别和跟踪列表中的每个元素,即使它们的位置发生变化。这样 React 可以保留焦点、输入值等状态,而不会导致重新渲染或不必要的状态丢失。id
作为 key
能让 React 准确地识别元素,即使元素顺序或内容发生变化,用户的选择、焦点和输入状态仍能得到保留。常见错误:
key
可能导致和不使用 key
相同的问题,React 仍然无法准确跟踪元素,因为当列表发生动态变化时,索引本身可能无法唯一标识元素。使用 key
是确保 React 正确管理组件状态和渲染行为的关键。当你正确使用 key
,尤其是在列表或动态 UI 中,React 能够高效地更新 DOM,避免状态丢失,并提供更好的用户体验。
在这个片段中,重点介绍了 key
属性的另一个应用场景,即在 React 中强制移除并重新加载某个组件或元素,以达到重置其状态的目的。
key
强制更新组件:
key
属性发生变化时,React 会认为这个元素或组件是一个全新的实例。所有与之相关的状态、事件处理程序和 DOM 节点都会被移除,React 会重新渲染该元素。这可以用来强制重置组件的状态。实际使用场景:
key
来实现。任务目标:
key
发生变化,从而让 React 重新渲染一个新的输入框。这样用户的输入内容会被清空,实现重置效果。通过简单地在按钮点击事件中改变输入框或相关父组件的 key
属性,React 将识别为该元素已更新,从而移除原有元素并重新渲染新的元素。
这个小技巧可以在需要清空组件状态或重置 UI 组件的场景下非常实用,尤其是在复杂的表单或交互中。
在这个片段中,主要讲解了如何通过 key
属性重置 React 组件的状态。
useState
状态管理:
useState
初始化了一个值(例如 0
),并将其作为 input
元素的 key
值。当点击 "reset" 按钮时,通过更新这个 key
的值(将其加 1
)来触发 React 认为这是一个全新的组件。key
发生变化时,React 会移除旧的 input
元素并插入一个新的,而所有与旧组件相关的状态(如用户输入的内容)将会被重置。点击按钮后的行为:
key
值更新,这会告诉 React 重新渲染该 input
元素,完全移除先前的输入状态,使得组件呈现为初始状态。应用场景:
key
属性在 React 中不仅用于高效渲染列表元素,它也可以用来重置 UI 中某些组件的状态。当你需要移除某个组件的所有状态并重新开始时,通过更新该组件的 key
属性可以实现这一点。
这是一个祝贺和总结的片段,感谢你完成了所有的React练习,并复习了你所学的内容:
你在这个系列练习中学到了很多有价值的React技能,从最基础的知识到更复杂的功能。现在你掌握了使用React构建各种有趣项目的能力。继续探索和构建,期待看到你未来的精彩作品!
这段内容介绍了即将进入的React Hooks的学习内容,通过一些实际的应用示例展示了React Hooks在创建动态和交互式应用中的作用。
动态交互:展示如何通过React Hooks实现动态应用,比如搜索、收藏文章、通过查询字符串保留状态等。
状态管理:学习如何管理应用中的状态,并通过用户交互更新界面。
React生命周期:通过一个示例说明React的渲染和更新流程,理解初始渲染和用户交互后状态更新触发的重新渲染机制。
外部库的整合:结合第三方库来实现一些视觉效果,比如计时器和动画。
React Hooks流程图:学习React Hooks的生命周期,理解在状态变化时,组件的渲染、更新等过程。
Tic-Tac-Toe 实践:通过实践项目(井字棋游戏),复习所学内容并深入理解,如使用本地存储保存状态。
最终,这段内容激励你开始学习,并鼓励大家通过React Hooks构建更加灵活、动态的应用。
这段内容详细介绍了React应用的生命周期及如何通过React Hooks来管理状态,特别是通过useState
hook实现动态更新的功能。
React生命周期:React应用的生命周期包括初始渲染和后续的用户交互。当用户与页面交互时,React通过更新状态(state)来决定哪些UI元素需要更新,保证界面能够动态响应用户行为。
State(状态)管理:状态可以被定义为随着时间变化的数据。在初始渲染时,React根据组件中的状态生成DOM节点。随着用户的操作,状态发生变化,React通过比较旧UI和新UI的不同来更新DOM。
使用useState
管理状态:
useState
hook接收一个初始值,并返回两个值:当前状态值和更新状态的函数。React更新机制:当状态变化时,React会重新调用相关的组件函数,生成新的UI,并通过比较新旧UI元素来高效地更新DOM,仅更新需要变化的部分。
useState
示例:讲解了一个计数器按钮的例子,展示了如何通过useState
管理按钮的计数状态,并根据用户点击事件更新计数值。
声明式UI更新:React通过声明式的方式管理UI更新,开发者只需定义在当前状态下UI应该如何显示,React会高效处理DOM更新,避免了手动操作DOM的复杂性。
最后,内容预告了即将开始的练习,将结合实际项目(如博客搜索)进一步理解这些概念。
这段内容描述了一个练习任务,目标是实现一个能够实时搜索博客文章的React应用。当前UI已经构建好,但输入框还没有与状态同步,因此在用户输入搜索内容时没有任何响应。
useState
来管理用户输入的搜索内容,确保输入框的变化能够与应用的状态同步。onChange
事件来捕捉用户输入,每次输入变化时,更新搜索的状态。onChange
事件中调用更新状态的函数。useState
hook管理输入框的当前值,并将其与UI绑定。开发者还可以通过一个额外的挑战来增强React组件和UI生成的实践:删除现有的代码并从头构建整个UI,练习如何从零构建React组件。
这段内容介绍了如何在React应用中使用useState
来管理状态,使应用可以动态更新UI。具体步骤如下:
初始化状态:
useState
来创建一个状态变量query
,初始值为空字符串。通过useState
返回的第二个函数setQuery
用于更新query
的值。更新UI状态:
onChange
事件处理器来捕捉用户在搜索框中的输入。通过event.currentTarget.value
获取输入框的当前值,并使用setQuery
更新状态。动态搜索:
query
状态的变化动态过滤和更新显示。多次调用useState
:
useState
可以在一个组件中多次使用,React会根据调用顺序跟踪状态值。每次调用都会返回当前状态和更新该状态的函数。通过使用useState
,你可以轻松地管理动态数据和UI状态,让应用在用户输入时做出实时响应。例如,在搜索框输入内容时,可以动态过滤和显示符合条件的博客文章。
这种模式广泛适用于React应用的各种场景,使得开发者能够轻松创建动态和交互式的用户界面。
这段视频内容讲解了受控输入(Controlled Input)的概念,以及如何在React中通过编程方式控制输入值。下面是关键点总结:
input
元素的value
属性为useState
中的值,React组件完全控制输入框的值。value
属性但没有处理onChange
事件,React会发出警告。原因是,用户尝试输入时React并没有被告知如何更新状态,所以输入框的值不会改变。解决方法是添加onChange
处理函数,以便在用户输入时更新状态。受控输入是React中常见的模式,允许开发者精确控制输入框的值,并根据需要进行编程处理。在React应用中,特别是当你需要自动填充或根据其他用户交互动态更改输入框内容时,受控输入非常有用。
这段内容详细介绍了如何在React中使用受控输入(controlled input)来确保复选框与查询输入框的同步更新。以下是关键步骤和要点的总结:
handleCheck
函数来处理复选框的状态变化:
const handleCheck = (tag, checked) => {
if (checked) {
setQuery(query + ' ' + tag); // 勾选时,添加标签
} else {
setQuery(query.replace(tag, '').trim()); // 取消勾选时,移除标签
}
};
onChange
事件中调用handleCheck
来根据复选框的选择状态(checked
)更新查询字符串。value
属性设置为React组件状态的值。通过添加:
<input value={query} />
query
的状态保持一致,并且当复选框更新时,输入框中的内容也会随之更新。useState
hook被用来管理查询字符串的状态:
const [query, setQuery] = useState('');
onChange
事件处理用户的输入,使得输入框中的每个字符的变化都会更新查询状态。handleCheck
函数确保复选框的选择状态能够正确地影响输入框的内容。handleCheck
函数进行优化,比如添加trim()
函数以确保最终的查询字符串没有不必要的空格。在这个练习中,学会了如何使用受控输入来控制用户界面的输入行为,特别是在用户与复选框交互时如何动态更新输入框的值。这是React中处理复杂状态和用户交互的一个非常重要的模式。
在这一段内容中,主要介绍了如何使用派生状态(derived state)来解决当前复选框与查询框同步的问题。当用户在输入框中手动输入标签时,复选框的选中状态没有同步更新,反之亦然,因此需要通过派生状态来确保复选框的状态与查询输入框的内容保持一致。
当前,复选框和输入框能够互相影响,但是如果手动在输入框中输入“dog”或“cat”,相应的复选框不会自动选中,这会导致用户体验的困惑。为了解决这个问题,我们需要根据查询字符串来动态派生复选框的选中状态。
派生状态的核心思想是根据现有的状态来计算新的状态,而不是独立管理多个状态。具体到这个场景,就是从查询输入框的内容派生出复选框的选中状态,而不是手动单独管理复选框的状态。
处理复选框的状态: 我们需要在每次渲染UI时,根据当前查询字符串动态确定复选框的选中状态。例如,如果查询中包含“dog”,那么对应的“dog”复选框应该被选中。
实现派生状态:
const isDogChecked = query.includes('dog');
const isCatChecked = query.includes('cat');
const isCaterpillarChecked = query.includes('caterpillar');
控制复选框状态:
checked
属性:
<input type="checkbox" checked={isDogChecked} onChange={...} />
<input type="checkbox" checked={isCatChecked} onChange={...} />
<input type="checkbox" checked={isCaterpillarChecked} onChange={...} />
同步逻辑的改进:
const [query, setQuery] = useState('');
// 根据查询字符串派生复选框的选中状态
const isDogChecked = query.includes('dog');
const isCatChecked = query.includes('cat');
const isCaterpillarChecked = query.includes('caterpillar');
// 处理复选框的点击事件
const handleCheck = (tag, checked) => {
if (checked) {
setQuery(query + ' ' + tag);
} else {
setQuery(query.replace(tag, '').trim());
}
};
// 渲染复选框并根据查询字符串设置复选框状态
return (
<div>
<input type="checkbox" checked={isDogChecked} onChange={(e) => handleCheck('dog', e.target.checked)} /> Dog
<input type="checkbox" checked={isCatChecked} onChange={(e) => handleCheck('cat', e.target.checked)} /> Cat
<input type="checkbox" checked={isCaterpillarChecked} onChange={(e) => handleCheck('caterpillar', e.target.checked)} /> Caterpillar
<input type="text" value={query} onChange={(e) => setQuery(e.target.value)} />
</div>
);
通过派生状态,我们能够确保复选框的状态与查询输入框内容保持一致,无论用户是手动输入还是勾选复选框,都能看到预期的结果。这种方式避免了管理多个冗余状态,代码更加简洁且易于维护。
这一部分讲解了如何通过派生状态(derived state)来解决复选框的状态与查询输入框的同步问题。派生状态的思路是在已有状态的基础上进行计算,以确保UI的不同部分保持同步。具体操作是将查询字符串拆分为单词数组,并根据这些单词数组动态确定复选框的选中状态。
派生单词状态:
const words = query.split(' ');
派生复选框的状态:
dog
、cat
、caterpillar
),从而派生出复选框的选中状态:
const isDogChecked = words.includes('dog');
const isCatChecked = words.includes('cat');
const isCaterpillarChecked = words.includes('caterpillar');
控制复选框状态:
checked
属性控制复选框的状态,而不是value
,因为checkbox
的选中状态是通过checked
属性来控制的:
<input type="checkbox" checked={isDogChecked} onChange={...} /> Dog
<input type="checkbox" checked={isCatChecked} onChange={...} /> Cat
<input type="checkbox" checked={isCaterpillarChecked} onChange={...} /> Caterpillar
同步逻辑的改进:
const [query, setQuery] = useState('');
// 派生单词数组
const words = query.split(' ');
// 派生复选框的状态
const isDogChecked = words.includes('dog');
const isCatChecked = words.includes('cat');
const isCaterpillarChecked = words.includes('caterpillar');
// 处理复选框点击事件
const handleCheck = (tag, checked) => {
if (checked) {
setQuery(prev => prev + ' ' + tag);
} else {
setQuery(prev => prev.replace(tag, '').trim());
}
};
// 渲染复选框并根据查询字符串设置状态
return (
<div>
<input type="checkbox" checked={isDogChecked} onChange={(e) => handleCheck('dog', e.target.checked)} /> Dog
<input type="checkbox" checked={isCatChecked} onChange={(e) => handleCheck('cat', e.target.checked)} /> Cat
<input type="checkbox" checked={isCaterpillarChecked} onChange={(e) => handleCheck('caterpillar', e.target.checked)} /> Caterpillar
<input type="text" value={query} onChange={(e) => setQuery(e.target.value)} />
</div>
);
派生状态是一种非常强大的方法,可以减少状态管理的复杂性。通过在当前状态的基础上动态生成新状态,可以确保应用程序的不同部分始终保持同步,从而提升用户体验和代码的可维护性。
这个部分讲解了如何在React应用中通过URL查询字符串来初始化应用的状态,使应用的状态可以被分享。当我们希望用户能够通过URL分享他们的查询内容(如:?query=cat+dog
),页面在加载时应能够根据URL中的查询参数初始化UI。
获取查询字符串:
URLSearchParams
从window.location.search
中获取查询参数,并从中提取出我们关心的query
参数。const params = new URLSearchParams(window.location.search);
const initialQuery = params.get('query') || '';
使用查询字符串初始化状态:
initialQuery
传递给useState
,作为初始值。
const [query, setQuery] = useState(initialQuery);
渲染UI:
query
来初始化搜索框和复选框的状态。?query=cat+dog
,那么页面加载后,搜索框中会显示cat dog
,并且相关的复选框会被选中。import React, { useState } from 'react';
const App = () => {
// 从URL中获取查询参数
const params = new URLSearchParams(window.location.search);
const initialQuery = params.get('query') || '';
// 使用查询字符串初始化状态
const [query, setQuery] = useState(initialQuery);
// 派生状态
const words = query.split(' ');
const isDogChecked = words.includes('dog');
const isCatChecked = words.includes('cat');
const isCaterpillarChecked = words.includes('caterpillar');
// 处理复选框的变化
const handleCheck = (tag, checked) => {
if (checked) {
setQuery(prev => prev + ' ' + tag);
} else {
setQuery(prev => prev.replace(tag, '').trim());
}
};
return (
<div>
<input
type="checkbox"
checked={isDogChecked}
onChange={(e) => handleCheck('dog', e.target.checked)}
/> Dog
<input
type="checkbox"
checked={isCatChecked}
onChange={(e) => handleCheck('cat', e.target.checked)}
/> Cat
<input
type="checkbox"
checked={isCaterpillarChecked}
onChange={(e) => handleCheck('caterpillar', e.target.checked)}
/> Caterpillar
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
);
};
export default App;
URLSearchParams
从URL中提取查询参数,以便在应用初始化时能够根据用户的分享链接来设置应用状态。query
参数初始化状态,使得页面在加载时可以自动填充输入框和复选框,确保应用状态与URL保持同步。通过这种方式,React应用能够根据URL中的查询参数初始化状态,确保应用状态的可分享性。这对于创建动态且易于分享的用户界面非常有用。
这一段讲解了如何基于URL查询字符串(如?query=cat+dog
)动态初始化React组件的状态。通过这样做,可以确保页面在加载时,UI根据URL中的参数展示正确的内容。
获取查询参数:
使用URLSearchParams
从window.location.search
中获取query
参数。这样就可以从URL中提取并使用查询字符串。
const params = new URLSearchParams(window.location.search);
const initialQuery = params.get('query') || '';
用查询参数初始化状态:
通过useState
将从URL查询字符串中提取的query
作为初始状态值。这确保页面加载时能使用URL中的值初始化UI。
const [query, setQuery] = useState(initialQuery);
实时更新UI:
查询参数会直接影响页面的输入框和复选框。例如,?query=cat+dog
会自动选中"cat"和"dog"的复选框,且搜索框中会显示cat dog
。
import React, { useState } from 'react';
const App = () => {
// 从URL中获取查询参数
const params = new URLSearchParams(window.location.search);
const initialQuery = params.get('query') || '';
// 用查询字符串初始化状态
const [query, setQuery] = useState(initialQuery);
// 派生状态
const words = query.split(' ');
const isDogChecked = words.includes('dog');
const isCatChecked = words.includes('cat');
const isCaterpillarChecked = words.includes('caterpillar');
// 处理复选框变化
const handleCheck = (tag, checked) => {
if (checked) {
setQuery(prev => prev + ' ' + tag);
} else {
setQuery(prev => prev.replace(tag, '').trim());
}
};
return (
<div>
<input
type="checkbox"
checked={isDogChecked}
onChange={(e) => handleCheck('dog', e.target.checked)}
/> Dog
<input
type="checkbox"
checked={isCatChecked}
onChange={(e) => handleCheck('cat', e.target.checked)}
/> Cat
<input
type="checkbox"
checked={isCaterpillarChecked}
onChange={(e) => handleCheck('caterpillar', e.target.checked)}
/> Caterpillar
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
);
};
export default App;
使用Router:
在实际应用中,不建议直接从window.location.search
中提取参数。推荐使用路由库(如React Router
)提供的查询参数钩子(如useSearchParams
),这样可以更灵活地处理路由和状态同步。
服务器端渲染(SSR)支持:
在服务端渲染的场景下,window
对象不存在,因此在这种情况下要谨慎使用直接引用window.location
的方式。路由库通常能更好地支持SSR。
通过这种方法,可以根据URL中的查询参数动态初始化组件状态,使得页面内容与URL保持同步,提供了方便的状态分享和持久化功能。这在构建动态、可分享的用户界面时非常有用。
这段讲解了如何优化 useState
的初始值,特别是当初始值的计算非常耗时时,可以通过传递一个函数来延迟计算,从而提高性能。
问题背景:
当你在 useState
中传递初始值时,该初始值只在组件的初次渲染时被使用。如果初始值的计算非常耗时(比如解析大型 JSON、计算复杂公式等),那么即使只在初次渲染时需要,后续渲染时仍会不必要地执行这一计算,从而影响性能。
解决方案:
React 允许你传递一个函数作为 useState
的参数。这样,初始值只会在第一次渲染时计算,后续渲染将不会再重复执行该计算。
语法示例:
const [value, setValue] = useState(() => {
// 只在初次渲染时执行的计算
return calculateInitialValue();
});
如何应用: 例如,如果你在页面加载时从 URL 中获取查询参数,可以将这一逻辑放在一个函数中,让 React 在需要时调用,而不是每次渲染都执行这个操作。
import React, { useState } from 'react';
const App = () => {
// 定义一个函数来获取初始查询参数
const getInitialQuery = () => {
const params = new URLSearchParams(window.location.search);
return params.get('query') || '';
};
// 使用 useState,传递函数作为初始值参数
const [query, setQuery] = useState(getInitialQuery);
// 处理输入框的变化
const handleInputChange = (event) => {
setQuery(event.target.value);
};
return (
<div>
<input
type="text"
value={query}
onChange={handleInputChange}
/>
</div>
);
};
export default App;
getInitialQuery
这个函数只会在初次渲染时被调用。虽然这种方式提高了性能,但如果初始计算非常复杂并影响页面的首次加载速度,应该考虑进一步优化该计算的性能或异步执行,以避免影响用户体验。
这种将初始值计算封装到函数中的方式,适用于需要进行复杂或耗时计算的场景,它能确保计算只在必要时进行,避免浪费性能。
在这个视频里,重点是如何通过将 useState
的初始值计算提取到一个单独的函数中,以实现更好的性能优化。这里展示了如何使用 React 的懒初始化功能,让初始值的计算只在首次渲染时执行,而不是在每次组件重新渲染时都执行。
getQueryParam
函数:你可以将用于计算初始查询参数的逻辑封装到 getQueryParam
函数中,并在 useState
中作为懒初始化函数传递。useState
中计算初始值,那么每次组件重新渲染时都会调用计算函数。而通过将计算逻辑封装为函数并传递给 useState
,你可以确保它只在初次渲染时调用。import React, { useState } from 'react';
const App = () => {
// 创建一个懒初始化函数,只有在组件初次渲染时才调用
const getQueryParam = () => {
const params = new URLSearchParams(window.location.search);
return params.get('query') || '';
};
// 使用懒初始化函数作为 useState 的参数
const [query, setQuery] = useState(getQueryParam);
// 处理输入框的变化
const handleInputChange = (event) => {
setQuery(event.target.value);
};
return (
<div>
<input
type="text"
value={query}
onChange={handleInputChange}
/>
</div>
);
};
export default App;
?query=cat+dog
,输入框将显示 cat dog
。getQueryParam
函数只在初次渲染时调用,不会再每次重新渲染时重复调用。通过懒初始化,可以避免在每次组件重新渲染时进行不必要的计算,从而优化性能。这种技术特别适合在计算量大或较为复杂的场景中使用。
好的,我们来深入了解一下 useEffect
钩子,并通过它来处理 React 应用中的副作用(side effects)。在 React 中,副作用通常是指那些不直接与渲染相关的操作,比如与外部 API 通信、订阅服务、或直接操作 DOM 等。
以下是 useEffect
的几个关键点:
useEffect
用于处理副作用,比如网络请求、订阅或手动操作 DOM 等操作。useEffect
中的副作用才会重新执行。useEffect
回调函数执行两次,以帮助捕获某些潜在的错误。提供的示意图帮助我们了解 useEffect
在 React 生命周期中的位置。通常,副作用会在初次渲染完成后执行,在组件卸载或依赖项改变时会清理先前的副作用并重新执行。
在这个练习中,我们需要确保 URL 中的查询参数与 React 组件中的搜索状态保持同步。虽然在实际应用中通常由路由器处理这一任务,但通过自己实现可以帮助更好地理解副作用是如何工作的,以及如何与外部状态进行同步。
现在让我们开始使用 useEffect
,改进博客搜索界面并确保 URL 和搜索状态保持一致。
popstate
事件更新 React 状态在这个练习中,我们需要处理用户通过浏览器的前进和后退按钮时,React 应用中的状态与 URL 不同步的问题。尽管浏览器更新了 URL,我们的 React 应用程序并没有及时同步内部状态。
要实现这个同步,核心思路是在 React 应用中监听浏览器的 popstate
事件,当用户点击浏览器的前进或后退按钮时,根据 URL 的变化更新应用状态。我们将使用 useEffect
钩子来实现这一功能。
监听 popstate
事件:
useEffect
钩子中,添加一个监听器来监听 popstate
事件。更新查询参数状态:
popstate
事件处理程序中,我们将使用 URLSearchParams
来解析当前 URL 中的查询参数,并将其设置为组件的查询状态。清理事件监听器:
useEffect
钩子中添加清理函数,以确保组件卸载时能够正确移除事件监听器。import React, { useState, useEffect } from "react";
import { useLocation } from "react-router-dom"; // 如果你的项目中使用了react-router
function SearchComponent() {
const [query, setQuery] = useState(""); // 查询参数的状态
useEffect(() => {
// 获取当前 URL 中的查询参数
const getQueryFromUrl = () => {
const params = new URLSearchParams(window.location.search);
return params.get("query") || "";
};
// 初始化查询状态
setQuery(getQueryFromUrl());
// popstate 事件处理函数
const handlePopState = () => {
setQuery(getQueryFromUrl());
};
// 添加 popstate 事件监听器
window.addEventListener("popstate", handlePopState);
// 清理函数:组件卸载时移除事件监听器
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);
const handleSubmit = (event) => {
event.preventDefault();
const params = new URLSearchParams();
params.set("query", query);
window.history.pushState({}, "", `?${params.toString()}`);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
/>
<button type="submit">Search</button>
</form>
);
}
export default SearchComponent;
query
状态。popstate
事件:当用户点击浏览器的前进或后退按钮时,popstate
事件会触发,更新应用的查询状态。通过这种方式,当用户点击浏览器的前进或后退按钮时,React 应用中的状态能够及时更新,实现 URL 和应用状态的同步。
popstate
事件更新查询状态在这个练习中,我们通过监听浏览器的 popstate
事件,使 React 应用的查询状态能够与浏览器的前进和后退操作同步更新。
popstate
事件:使用 useEffect
钩子在组件挂载时添加事件监听器,监听 popstate
事件。popstate
事件触发时,从 URL 中重新获取查询参数,并将其更新到组件的状态中。import React, { useState, useEffect } from "react";
function SearchComponent() {
const getQueryParam = () => {
const params = new URLSearchParams(window.location.search);
return params.get("query") || "";
};
const [query, setQuery] = useState(getQueryParam);
useEffect(() => {
const handlePopState = () => {
setQuery(getQueryParam);
};
// 添加 popstate 事件监听器
window.addEventListener("popstate", handlePopState);
// 清理函数,组件卸载时移除事件监听器
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);
const handleSubmit = (event) => {
event.preventDefault();
const params = new URLSearchParams();
params.set("query", query);
window.history.pushState({}, "", `?${params.toString()}`);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
/>
<button type="submit">Search</button>
</form>
);
}
export default SearchComponent;
getQueryParam
:这是一个函数,用于从当前 URL 中获取查询参数。useEffect
:在组件挂载时,我们使用 window.addEventListener
监听 popstate
事件,当用户点击浏览器的前进或后退按钮时,更新 query
状态。popstate
事件触发时,我们通过 getQueryParam
获取当前 URL 中的查询参数并更新组件的查询状态。popstate
事件监听器,避免内存泄漏。useEffect
中的事件监听器在 React 中,使用 useEffect
来设置事件监听器时,必须确保在组件卸载时清理这些监听器,否则会导致内存泄漏或其他潜在问题。
我们当前的解决方案中,useEffect
设置了一个 popstate
事件监听器,但没有在组件卸载时正确地移除它。如果组件卸载后事件监听器还在运行,它可能会导致内存泄漏,保留不需要的变量,甚至会出现性能问题。
通过在 useEffect
中返回一个清理函数来移除事件监听器。
import React, { useState, useEffect } from "react";
function SearchComponent() {
const getQueryParam = () => {
const params = new URLSearchParams(window.location.search);
return params.get("query") || "";
};
const [query, setQuery] = useState(getQueryParam);
useEffect(() => {
const handlePopState = () => {
setQuery(getQueryParam());
};
// 添加 popstate 事件监听器
window.addEventListener("popstate", handlePopState);
// 返回一个清理函数,在组件卸载时移除事件监听器
return () => {
window.removeEventListener("popstate", handlePopState);
};
}, []);
const handleSubmit = (event) => {
event.preventDefault();
const params = new URLSearchParams();
params.set("query", query);
window.history.pushState({}, "", `?${params.toString()}`);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
/>
<button type="submit">Search</button>
</form>
);
}
export default SearchComponent;
useEffect
的清理函数:在 useEffect
中返回一个函数,该函数将在组件卸载时运行。在这个例子中,我们使用 window.removeEventListener
来移除 popstate
事件监听器。通过在 useEffect
中添加清理函数,可以避免由于事件监听器未被移除而引起的内存泄漏问题。这是 React 中处理副作用(如事件监听、订阅等)时非常重要的一个实践。
useEffect
中的事件监听器的重要性在 React 中,使用 useEffect
时,清理副作用(如事件监听器)是非常重要的。否则,可能会导致内存泄漏、性能下降,甚至会在组件卸载后继续执行无效操作。
在之前的代码中,我们在 useEffect
中为 popstate
事件添加了监听器,但是没有返回清理函数。这样会导致内存泄漏问题,尤其是在处理大量数据时。例如,我们在代码中生成了包含一百万个元素的数组,未清理的事件监听器会持续占用内存,导致内存使用不断上升,严重时可能会使应用崩溃。
当我们没有移除事件监听器时,React 保留了对组件中的所有变量和函数的引用,即使组件已经卸载。这些未清理的引用会继续占用内存,并导致性能问题,尤其是在用户频繁操作页面时。
在 useEffect
中返回一个清理函数,确保组件卸载时正确移除事件监听器。
import React, { useState, useEffect } from "react";
function SearchComponent() {
const getQueryParam = () => {
const params = new URLSearchParams(window.location.search);
return params.get("query") || "";
};
const [query, setQuery] = useState(getQueryParam);
useEffect(() => {
const handlePopState = () => {
setQuery(getQueryParam());
};
// 添加事件监听器
window.addEventListener("popstate", handlePopState);
// 返回清理函数,移除事件监听器
return () => {
console.log("Cleaning up event listener");
window.removeEventListener("popstate", handlePopState);
};
}, []);
const handleSubmit = (event) => {
event.preventDefault();
const params = new URLSearchParams();
params.set("query", query);
window.history.pushState({}, "", `?${params.toString()}`);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search"
/>
<button type="submit">Search</button>
</form>
);
}
export default SearchComponent;
事件监听器的清理:在 useEffect
中,我们返回了一个函数,该函数将在组件卸载时被调用,用来移除 popstate
事件监听器。这样可以避免内存泄漏,并确保不会在组件卸载后继续调用无效的事件处理程序。
内存管理:通过正确清理副作用,确保即使在处理大量数据时,内存占用也能够被及时释放,从而避免性能问题。
性能影响:未清理的事件监听器可能导致组件卸载后仍在运行,继续占用内存和处理能力,特别是在移动设备等内存较少的环境中,影响更加明显。
每当在 useEffect
中设置副作用时(如事件监听器、订阅等),确保返回一个清理函数,及时释放资源是 React 开发中的重要实践。这样不仅可以提升应用性能,还能避免潜在的内存泄漏问题。
很多人经常会遇到这样的问题:假设我们有一个组件结构,一个主应用组件(App),然后有多个嵌套的子组件,比如这个组件和那个组件。这些组件可能会形成复杂的层级结构。有时候,你会发现某个状态在一处被管理,但另一个较远的组件却需要访问这个状态。
这个时候,如何共享状态呢?解决方法就是提升状态,也就是将状态提升到最接近的共同父组件中进行管理。通过这种方式,状态可以被多个组件共享,并传递到那些需要访问它的子组件。
假设我们有一个应用程序(App),其中有一个计数器组件(Counter)来管理其状态。我们还想添加一个显示计数的组件(CountDisplay)。此时,状态在Counter组件中管理,我们希望将它传递给CountDisplay组件显示。
要做到这一点,首先我们需要将状态从Counter组件中提取出来,移动到App组件中。然后,App组件可以将这个状态以及更新状态的方法分别传递给Counter和CountDisplay组件,这样两个组件都可以访问和使用这个共享状态。
通常,React开发者已经非常熟悉提升状态的模式,并且在需要时知道如何进行状态提升。不过,当应用发生变化时,可能会遇到一个问题:我们是否需要把状态再次下移?比如,假设我们不再需要显示计数功能了,那么我们就可以删除CountDisplay组件,并将状态重新下移到Counter组件。
如果状态只被某个子组件使用,那么它应该被下移到该子组件中管理,避免状态在不相关的父组件中管理,从而提高代码的可读性和性能。
因此,我们有两个概念:提升状态和本地化状态。提升状态是为了让多个组件共享状态,而本地化状态则是当某个状态只在一个组件中使用时,将其下移到该组件中进行管理。
在接下来的练习中,我们将探讨这两个概念——提升状态和本地化状态,并通过实现一个点赞功能来加深理解。希望你已经准备好操作这个点赞功能,因为这次练习不仅仅是操作计数器,还涉及到如何管理一个收藏状态,比如一组被收藏的帖子 ID 等。
准备好了吗?让我们开始吧!
在Kelly为我们重构代码以添加收藏功能时,导致了实际的过滤功能出现了问题。现在的任务是修复这个过滤功能,并且通过提升状态来实现这个修复。我们需要确保在操作收藏时,过滤功能仍能正常工作。
分析问题:
提升状态:
步骤概述:
让我们一起动手,修复过滤问题并提升状态吧!
在这个步骤中,我们将通过提升状态来修复过滤功能的问题。当前状态(query
)位于表单组件中,但多个组件都需要访问该状态。为了确保过滤功能和收藏功能都能正常工作,我们需要提升状态,将其放到一个共同的父组件中。
找到需要提升的状态:
query
,但这个query
状态目前位于表单组件中,无法共享给其他组件。query
和setQuery
提升到App
组件中(或父组件),以便其他子组件也能访问和使用这个状态。提升状态:
query
的状态逻辑(包括useState
和setQuery
)从表单组件中移动到父组件App
中。App
可以将query
状态作为prop
传递给表单和其他需要它的子组件。更新组件结构:
query
和setQuery
作为prop
传递给Form
组件,但仅传递query
给MatchingPosts
,因为MatchingPosts
不需要修改状态,只需要读取状态。query
和setQuery
的使用,确保表单能够修改状态,并将状态变更反馈给父组件。调整useEffect
:
App
。将与状态相关的useEffect
逻辑也移到父组件中,这样可以确保所有逻辑集中处理。验证结果:
MatchingPosts
组件能够正确过滤,并且在进行收藏等操作时,过滤功能仍然正常工作。// App 组件
function App() {
const [query, setQuery] = useState('');
return (
<div>
<Form query={query} setQuery={setQuery} />
<MatchingPosts query={query} />
</div>
);
}
// Form 组件
function Form({ query, setQuery }) {
const handleInputChange = (event) => {
setQuery(event.target.value);
};
return (
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="Search posts"
/>
);
}
// MatchingPosts 组件
function MatchingPosts({ query }) {
// 过滤逻辑基于传入的 query 状态
const filteredPosts = posts.filter(post => post.includes(query));
return (
<ul>
{filteredPosts.map(post => <li key={post}>{post}</li>)}
</ul>
);
}
通过这种方式,所有需要访问query
状态的组件都可以从父组件获得query
,并且状态可以在不同的组件之间同步。
在这个任务中,用户要求在显示帖子时,如果某个帖子被点赞(liked),那么它应该在列表中优先显示,随后再展示其他未被点赞的帖子。这意味着我们需要提升点赞状态(liked
),将其提升到能够影响整个帖子列表排序的地方。
提升状态:
liked
状态存储在它自己的组件中。为了实现排序逻辑,我们需要将liked
状态提升到父组件(例如MatchingPosts
),使它可以控制整个帖子列表的展示顺序。传递状态和更新函数:
MatchingPosts
中管理点赞状态,并将状态和更新状态的函数作为prop
传递给每个帖子的子组件。liked
状态,并影响帖子列表的排序。实现排序逻辑:
// App 组件
function App() {
const [query, setQuery] = useState('');
return (
<div>
<Form query={query} setQuery={setQuery} />
<MatchingPosts query={query} />
</div>
);
}
// MatchingPosts 组件
function MatchingPosts({ query }) {
const [likedPosts, setLikedPosts] = useState([]);
const handleLikeToggle = (postId) => {
setLikedPosts((prevLikedPosts) => {
if (prevLikedPosts.includes(postId)) {
return prevLikedPosts.filter(id => id !== postId);
} else {
return [...prevLikedPosts, postId];
}
});
};
// 模拟帖子数据
const posts = [
{ id: 1, content: 'Dog post' },
{ id: 2, content: 'Cat post' },
{ id: 3, content: 'Caterpillar post' }
];
// 过滤并按点赞状态排序
const filteredPosts = posts
.filter(post => post.content.includes(query))
.sort((a, b) => likedPosts.includes(b.id) - likedPosts.includes(a.id));
return (
<ul>
{filteredPosts.map(post => (
<PostCard
key={post.id}
post={post}
isLiked={likedPosts.includes(post.id)}
onLikeToggle={() => handleLikeToggle(post.id)}
/>
))}
</ul>
);
}
// PostCard 组件
function PostCard({ post, isLiked, onLikeToggle }) {
return (
<li>
<span>{post.content}</span>
<button onClick={onLikeToggle}>
{isLiked ? 'Unlike' : 'Like'}
</button>
</li>
);
}
// Form 组件
function Form({ query, setQuery }) {
const handleInputChange = (event) => {
setQuery(event.target.value);
};
return (
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="Search posts"
/>
);
}
状态提升:我们将每个帖子的liked
状态提升到了MatchingPosts
组件中,并在该组件中使用useState
来管理所有帖子的点赞状态(使用帖子ID数组来记录哪些帖子被点赞)。
排序逻辑:在渲染帖子列表之前,我们根据likedPosts
数组中的数据,对帖子进行排序。被点赞的帖子会先展示,未被点赞的帖子则在后面。
事件处理:在PostCard
组件中,点击"Like"按钮会触发onLikeToggle
,从而调用MatchingPosts
中的handleLikeToggle
函数来更新点赞状态。
这样实现了用户的需求,点赞的帖子将优先展示,并且可以正确处理用户的点赞操作。
在这个任务中,我们遇到了一个稍微复杂的情况。用户希望当点击"喜欢"按钮时,帖子可以根据点赞状态排序,将已点赞的帖子排在最前面。这意味着我们不仅要提升isFavorited
状态,还需要确保在点击时正确更新这个状态,并根据状态进行排序。
提升状态:
isFavorited
状态从卡片组件提升到MatchingPosts
组件中,并在父组件中用useState
管理所有帖子的点赞状态。我们用帖子ID数组来记录哪些帖子已被点赞。传递状态和更新函数:
MatchingPosts
需要传递isFavorited
状态以及用于更新该状态的回调函数onFavoriteClick
给每个子组件(即每个帖子卡片组件)。实现排序逻辑:
// MatchingPosts 组件
function MatchingPosts({ query }) {
const [favorites, setFavorites] = useState([]);
const handleFavoriteToggle = (postId) => {
setFavorites((prevFavorites) => {
if (prevFavorites.includes(postId)) {
return prevFavorites.filter(id => id !== postId); // 取消点赞
} else {
return [...prevFavorites, postId]; // 点赞
}
});
};
// 模拟帖子数据
const posts = [
{ id: 1, content: 'Dog post' },
{ id: 2, content: 'Cat post' },
{ id: 3, content: 'Caterpillar post' }
];
// 过滤并按点赞状态排序
const sortedPosts = posts
.filter(post => post.content.includes(query))
.sort((a, b) => favorites.includes(b.id) - favorites.includes(a.id));
return (
<ul>
{sortedPosts.map(post => (
<PostCard
key={post.id}
post={post}
isFavorited={favorites.includes(post.id)}
onFavoriteClick={() => handleFavoriteToggle(post.id)}
/>
))}
</ul>
);
}
// PostCard 组件
function PostCard({ post, isFavorited, onFavoriteClick }) {
return (
<li>
<span>{post.content}</span>
<button onClick={onFavoriteClick}>
{isFavorited ? 'Unlike' : 'Like'}
</button>
</li>
);
}
状态提升:我们将每个帖子的isFavorited
状态从卡片组件提升到MatchingPosts
组件中,并使用useState
来存储点赞的帖子ID数组。
排序逻辑:在渲染帖子列表之前,先根据favorites
数组中的数据对帖子进行排序,点赞的帖子优先展示,未被点赞的帖子排在后面。
事件处理:在PostCard
组件中,点击"Like"按钮会调用onFavoriteClick
函数,该函数会触发handleFavoriteToggle
,从而更新父组件中的点赞状态。
通过这种方式,我们实现了用户的需求,使得点赞的帖子会自动排到列表的最前面。
用户在使用点赞后将帖子自动移到列表顶部的功能时,觉得效果过于突兀,因此该功能已经被取消。现在需要将点赞状态的管理从父组件MatchingPosts
中下放回到各个子组件(即帖子卡片组件),以简化代码并避免不必要的状态提升。
你的任务是将点赞状态(favorites
数组)从父组件中移除,并在子组件中管理它,使状态只存在于需要的地方。这种做法称为状态的下放或共定位(co-location),是为了让状态在只需要使用它的组件中维护,避免全局状态的复杂性。
MatchingPosts
组件中删除favorites
和相关的状态管理逻辑。PostCard
组件中管理isFavorited
状态:点赞状态应只与每个帖子组件相关,因此应在PostCard
组件中管理它。onFavoriteClick
等回调函数,因此可以简化MatchingPosts
组件和PostCard
组件之间的通信。MatchingPosts
组件:// MatchingPosts 组件
function MatchingPosts({ query }) {
// 模拟帖子数据
const posts = [
{ id: 1, content: 'Dog post' },
{ id: 2, content: 'Cat post' },
{ id: 3, content: 'Caterpillar post' }
];
// 过滤帖子
const filteredPosts = posts.filter(post => post.content.includes(query));
return (
<ul>
{filteredPosts.map(post => (
<PostCard key={post.id} post={post} />
))}
</ul>
);
}
PostCard
组件:// PostCard 组件
function PostCard({ post }) {
const [isFavorited, setIsFavorited] = useState(false);
const handleFavoriteClick = () => {
setIsFavorited(!isFavorited); // 切换点赞状态
};
return (
<li>
<span>{post.content}</span>
<button onClick={handleFavoriteClick}>
{isFavorited ? 'Unlike' : 'Like'}
</button>
</li>
);
}
删除全局状态:我们从MatchingPosts
组件中删除了favorites
数组及其状态管理,现在这个组件只负责渲染过滤后的帖子列表。
点赞状态共定位:isFavorited
状态现在只在PostCard
组件中管理,不需要父组件参与。每个帖子都有自己独立的点赞状态。
简化组件通信:由于点赞状态的管理被下放到帖子组件中,父组件不再需要传递isFavorited
或onFavoriteClick
等回调,简化了组件之间的通信。
通过这个过程,我们将状态管理限定在需要的组件中,避免了不必要的状态提升,从而简化了代码结构并提高了可维护性。
我们已经完成了将点赞状态从父组件下放到子组件的过程,并简化了代码逻辑。通过共定位(co-location)状态,我们实现了以下几个关键点:
简化组件:不再需要将状态从父组件传递到子组件,减少了状态管理的复杂性。子组件独立管理其自身的状态,使代码更加模块化和易维护。
性能提升:通过在需要状态的组件内管理状态,减少了不必要的重渲染。只有实际需要更新的组件会重新渲染,从而提高了性能。
状态生命周期:共定位状态意味着当组件被卸载时,其状态也会随之消失。这是一把双刃剑,在某些场景下是合理的,但如果需要持久化状态(例如,用户点赞后状态不应丢失),则需要提升状态,甚至使用数据库来保存这些数据。
用户需求调整:尽管点赞功能最初需要将帖子移动到列表顶部,后期用户反馈认为这并不理想,因此我们移除了这一逻辑。但共定位状态的操作使代码结构更加清晰,为将来的扩展提供了良好的基础。
通过这次练习,你学会了如何在React中有效地管理状态,理解了状态提升和状态共定位的概念,以及它们对组件性能和复杂度的影响。
你可以继续思考如何通过数据库或本地存储持久化这些状态,以及如何在全栈应用中实现更复杂的状态管理逻辑。
在之前的讨论中,我们已经介绍过副作用的概念。然而,有些副作用和其他的有所不同,尤其是当我们需要直接与DOM交互时,这就是我们在这个练习中要探讨的重点。
假设我们有一个很酷的库叫Vanilla Tilt,它为元素提供了一些有趣的动画效果。这是一个基于纯JavaScript编写的库,它不依赖于React的任何特性。因此,当我们想在React应用中集成它时,就需要找到一种方式来直接与DOM交互。
在React中,通常我们无法直接获取DOM节点,因为在调用React.createElement
时,创建的仅仅是React元素,并不是DOM元素。真正的DOM元素只有在React完成渲染时才会被创建。因此,我们需要使用ref来获取已经渲染的DOM节点。
通过将ref属性应用在元素上,当React完成渲染后,ref属性会被调用并传递给我们DOM节点的引用。这就允许我们在DOM节点渲染完后执行一些操作,比如初始化第三方库或者添加一些原生的DOM事件监听。
<div ref={node => { /* 操作 node */ }}>
除了直接在JSX中使用回调函数获取ref,React还提供了一个更为灵活的方式——useRef Hook。与useState类似,useRef返回一个可以在组件的整个生命周期中保持稳定的对象。这种对象包含一个current
属性,我们可以通过它访问或更新引用的值。
不过,与useState不同的是,更新useRef不会引发组件的重新渲染。因此,在某些需要频繁更新的场景中,比如DOM节点的直接操作,useRef更加高效。
const myRef = useRef(null);
const MyComponent = () => {
const tiltRef = useRef(null);
useEffect(() => {
const tiltNode = tiltRef.current;
// 初始化 Vanilla Tilt
VanillaTilt.init(tiltNode);
return () => {
// 清理工作
tiltNode.vanillaTilt.destroy();
};
}, []);
return <div ref={tiltRef} className="tilt-element">Fancy Element</div>;
};
在上面的示例中,我们通过useRef
获取到DOM节点,并使用useEffect
在节点渲染完成后初始化第三方库,并在组件销毁时进行清理。这就是典型的副作用和DOM操作结合的场景。
通过这个练习,我们探索了两种与DOM交互的方式:一种是通过ref回调函数,另一种是使用useRef Hook。了解什么时候使用哪种方式有助于我们编写更高效和易维护的代码。
现在我们的目标是通过Vanilla Tilt为一个元素添加炫酷的3D倾斜效果。我们已经准备好了一些代码,但需要你来处理ref部分。
我们将实现一个可见性切换按钮,确保你能够正确地进行清理操作。同时,我们还会在元素上实现点击计数器功能,这使得体验更加互动和有趣。
当你完成这个练习后,你应该能够看到:
useRef
获取元素的DOM节点。useEffect
中初始化Vanilla Tilt,确保在组件渲染后绑定正确的DOM节点。import React, { useRef, useEffect, useState } from 'react';
import VanillaTilt from 'vanilla-tilt';
const TiltCard = () => {
const [isVisible, setIsVisible] = useState(true);
const [count, setCount] = useState(0);
const tiltRef = useRef(null);
useEffect(() => {
if (tiltRef.current) {
VanillaTilt.init(tiltRef.current, {
max: 25,
speed: 400,
glare: true,
"max-glare": 0.5,
});
return () => {
// 清理操作
tiltRef.current.vanillaTilt.destroy();
};
}
}, [isVisible]);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle Visibility
</button>
{isVisible && (
<div ref={tiltRef} className="tilt-element" onClick={() => setCount(count + 1)}>
<h1>Count: {count}</h1>
</div>
)}
</div>
);
};
export default TiltCard;
useRef
来获取DOM节点,在元素渲染完成后绑定。useEffect
中初始化Vanilla Tilt,并在组件卸载时销毁实例。完成之后,你应该会看到一个可交互的元素,带有炫酷的3D倾斜效果,并且能够在点击时增加计数。
我们现在已经成功通过 useRef
与 Vanilla Tilt
库互动,实现了一个 3D 倾斜效果,并且确保了内存的清理。我们还讨论了在 React 中如何通过 refs 与 DOM 进行交互,并保证了每次重新渲染时正确的清理工作。
总结一下:
useRef
: 我们用 useRef
来获取 DOM 元素的引用,并在 useEffect
中初始化 Vanilla Tilt。console.log
来观察 Vanilla Tilt 的初始化和清理过程,确保其行为符合预期。以下是最终的代码:
import React, { useRef, useEffect, useState } from 'react';
import VanillaTilt from 'vanilla-tilt';
const TiltCard = () => {
const [isVisible, setIsVisible] = useState(true);
const [count, setCount] = useState(0);
const tiltRef = useRef(null);
useEffect(() => {
if (tiltRef.current) {
VanillaTilt.init(tiltRef.current, {
max: 25,
speed: 400,
glare: true,
"max-glare": 0.5,
});
return () => {
// 清理操作,销毁 Vanilla Tilt 实例
tiltRef.current.vanillaTilt.destroy();
};
}
}, [isVisible]);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle Visibility
</button>
{isVisible && (
<div ref={tiltRef} className="tilt-element" onClick={() => setCount(count + 1)}>
<h1>Count: {count}</h1>
</div>
)}
</div>
);
};
export default TiltCard;
在这个练习中,你不仅学到了如何在 React 中处理 DOM 引用,还理解了如何通过 useEffect
和 useRef
来保证 DOM 操作的正确性和清理操作的及时性。这种模式非常适用于需要与第三方库互动的场景。
我们现在要优化之前的 Vanilla Tilt 实现,使其不在每次状态更新时重置效果,同时允许用户动态调整 tilt
的参数,例如调整速度和眩光等。
max glare
和 speed
。useEffect
和依赖项优化 Vanilla Tilt 的初始化和清理过程。import React, { useRef, useEffect, useState } from 'react';
import VanillaTilt from 'vanilla-tilt';
const TiltCard = () => {
const [isVisible, setIsVisible] = useState(true);
const [count, setCount] = useState(0);
const [tiltOptions, setTiltOptions] = useState({
max: 25,
speed: 400,
glare: true,
"max-glare": 0.5,
});
const tiltRef = useRef(null);
useEffect(() => {
if (tiltRef.current) {
// 初始化 Vanilla Tilt
VanillaTilt.init(tiltRef.current, tiltOptions);
return () => {
// 清理 Vanilla Tilt 实例
tiltRef.current.vanillaTilt.destroy();
};
}
}, [tiltOptions]); // 依赖项为 tiltOptions,只有当选项改变时重新初始化
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle Visibility
</button>
{isVisible && (
<div>
<div ref={tiltRef} className="tilt-element" onClick={() => setCount(count + 1)}>
<h1>Count: {count}</h1>
</div>
{/* 控制选项 */}
<div>
<label>
Speed:
<input
type="range"
min="100"
max="1000"
value={tiltOptions.speed}
onChange={(e) => setTiltOptions({
...tiltOptions,
speed: parseInt(e.target.value)
})}
/>
</label>
<label>
Max Glare:
<input
type="range"
min="0"
max="1"
step="0.1"
value={tiltOptions["max-glare"]}
onChange={(e) => setTiltOptions({
...tiltOptions,
"max-glare": parseFloat(e.target.value)
})}
/>
</label>
</div>
</div>
)}
</div>
);
};
export default TiltCard;
使用 useEffect
:我们将 Vanilla Tilt 的初始化逻辑移到了 useEffect
中,并将 tiltOptions
放入依赖数组。这样当用户调整参数时,Vanilla Tilt 会重新初始化,但不会在每次状态变更(如 count
变更)时重置。
控制参数的更新:我们为 max-glare
和 speed
提供了用户控制的 UI,使用 setTiltOptions
更新 tiltOptions
,并通过 useEffect
自动触发重置。
尽管我们通过依赖项限制了重新初始化的次数,但由于 Vanilla Tilt
的 API 不支持直接更新配置,我们仍需要每次手动销毁旧实例并重新初始化,这可能影响体验。接下来我们可以考虑如何进一步优化这种场景。
通过这个实现,用户可以自由控制 tilt 的行为,并避免不必要的重新渲染带来的问题。
useRef
和 useEffect
结合处理 DOM 节点的状态管理在这个部分,我们通过创建 useRef
来存储 DOM 节点的引用,并结合 useEffect
处理 Vanilla Tilt
的初始化和清理操作。最终目标是确保在每次用户更改配置时,能够正确地重新初始化 Vanilla Tilt
,而不是在每次组件重新渲染时都重新初始化。
创建 useRef
引用:
使用 useRef
来存储 DOM 节点的引用。我们通过 tiltRef
来引用具体的 DOM 节点。
处理 useEffect
:
将 Vanilla Tilt
的初始化逻辑放入 useEffect
,并设置依赖项,当这些依赖项(如用户的 tiltOptions
)改变时,触发重新初始化。
清理逻辑:
每次重新初始化之前,我们需要调用 Vanilla Tilt
的 destroy
方法来清理先前的事件监听器,避免内存泄漏。
import React, { useRef, useEffect, useState } from 'react';
import VanillaTilt from 'vanilla-tilt';
const TiltCard = () => {
const [tiltOptions, setTiltOptions] = useState({
max: 25,
speed: 400,
glare: true,
"max-glare": 0.5,
});
const tiltRef = useRef(null);
useEffect(() => {
const tiltNode = tiltRef.current;
if (tiltNode) {
// 初始化 Vanilla Tilt
VanillaTilt.init(tiltNode, tiltOptions);
// 清理函数,防止内存泄漏
return () => {
tiltNode.vanillaTilt.destroy();
};
}
}, [tiltOptions]); // 依赖项为 tiltOptions,确保只有当参数变化时重新初始化
return (
<div>
{/* 控制选项 */}
<div>
<label>
Speed:
<input
type="range"
min="100"
max="1000"
value={tiltOptions.speed}
onChange={(e) => setTiltOptions({
...tiltOptions,
speed: parseInt(e.target.value)
})}
/>
</label>
<label>
Max Glare:
<input
type="range"
min="0"
max="1"
step="0.1"
value={tiltOptions["max-glare"]}
onChange={(e) => setTiltOptions({
...tiltOptions,
"max-glare": parseFloat(e.target.value)
})}
/>
</label>
</div>
{/* Vanilla Tilt DOM 元素 */}
<div ref={tiltRef} className="tilt-element">
<h1>Interactive Tilt Card</h1>
</div>
</div>
);
};
export default TiltCard;
useRef
:我们通过 useRef
创建了 tiltRef
,用于存储 DOM 元素的引用。
useEffect
与依赖项:使用 useEffect
监听 tiltOptions
的变化,当选项更新时,重新初始化 Vanilla Tilt
,避免了不必要的多次初始化和销毁操作。
清理操作:在 useEffect
返回的函数中,我们使用 vanillaTilt.destroy()
清理所有的事件监听器,确保没有内存泄漏。
通过这种方式,我们确保了 Vanilla Tilt
只在用户改变参数时重新初始化,提升了应用的性能,同时避免了不必要的重新渲染和资源浪费。这种模式非常适用于在 React 中处理直接与 DOM 交互的第三方库。
React 19 深入浅出, 构建生产级React应用程序 Learn React 19 with Epic React v2
1 React基础 001 介绍
002 在JS中实现Hello World
003 在JS中实现Hello World (1)
004 生成根节点
005 生成根节点 (1)
006 爸爸笑话时间
007 原生React API简介
008 创建React元素
009 创建React元素 (1)
010 嵌套元素
011 嵌套元素 (1)
012 深度嵌套元素
013 深度嵌套元素 (1)
014 爸爸笑话时间 原生React API
015 使用JSX简介
016 编译JSX
017 编译JSX (1)
018 插值
019 插值 (1)
020 Spread props
021 Spread props (1)
022 嵌套JSX
023 嵌套JSX (1)
024 片段
025 片段 (1)
026 爸爸笑话时间 使用JSX
027 自定义组件简介
028 简单函数
029 简单函数 (1)
030 原生API
031 原生API (1)
032 JSX组件
033 JSX组件 (1)
034 Props
035 Props (1)
036 爸爸笑话时间 自定义组件
037 TypeScript简介
038 Props (2)
039 Props (3)
040 类型收窄
041 类型收窄 (1)
042 推导类型
043 推导类型 (1)
044 默认Props
045 默认Props (1)
046 减少重复
047 减少重复 (1)
048 Satisfies
049 Satisfies (1)
050 爸爸笑话时间 TypeScript
051 样式简介
052 样式
053 样式 (1)
054 自定义组件
055 自定义组件 (1)
056 尺寸Props
057 尺寸Props (1)
058 爸爸笑话时间 样式
059 表单简介
060 表单
061 表单 (1)
062 表单操作
063 表单操作 (1)
064 输入类型
065 输入类型 (1)
066 提交
067 提交 (1)
068 表单操作
069 表单操作 (1)
070 爸爸笑话时间 表单
071 输入简介
072 复选框
073 复选框 (1)
074 下拉选择
075 下拉选择 (1)
076 单选按钮
077 单选按钮 (1)
078 隐藏输入
079 隐藏输入 (1)
080 默认值
081 默认值 (1)
082 爸爸笑话时间 输入
083 错误边界简介
084 组合
085 组合 (1)
086 其他错误
087 其他错误 (1)
088 重置
089 重置 (1)
090 爸爸笑话时间 错误边界
091 数组渲染简介
092 Key prop
093 Key prop (1)
094 焦点状态
095 焦点状态 (1)
096 Key重置
097 Key重置 (1)
098 爸爸笑话时间 数组渲染
099 React基础结束
2 React钩子 001 React钩子简介
002 UI状态管理简介
003 useState
004 useState (1)
005 控制输入
006 控制输入 (1)
007 推导状态
008 推导状态 (1)
009 初始化状态
010 初始化状态 (1)
011 初始化回调
012 初始化回调 (1)
013 爸爸笑话时间 UI状态管理
014 副作用简介
015 useEffect
016 useEffect (1)
017 清理副作用
018 清理副作用 (1)
019 爸爸笑话时间 副作用
020 状态提升简介
021 提升状态
022 提升状态 (1)
023 提升更多状态
024 提升更多状态 (1)
025 状态合并
026 状态合并 (1)
027 爸爸笑话时间 状态提升
028 DOM副作用简介
029 Refs
030 Refs (1)
031 依赖项
032 依赖项 (1)
033 原始依赖项
034 原始依赖项 (1)
035 爸爸笑话时间 DOM副作用
036 唯一ID简介
037 useId
038 useId (1)
039 爸爸笑话时间 唯一ID
040 井字棋简介
041 setState回调
042 setState回调 (1)
043 在localStorage中保存状态
044 在localStorage中保存状态 (1)
045 添加游戏历史功能
046 添加游戏历史功能 (1)
047 爸爸笑话时间 井字棋
048 React钩子结束
3 高级React API
4 React Suspense
5 高级React模式
6 React性能优化
7 React服务器组件
8 额外:专家访谈
Dominik Dorfmeister谈他的开源之旅
1 React Fundamentals
001 Intro 002 Hello World in JS 003 Hello World in JS (1) 004 Generate the Root Node 005 Generate the Root Node (1) 006 Dad Joke Break 007 Intro to Raw React APIs 008 Create React Elements 009 Create React Elements (1) 010 Nesting Elements 011 Nesting Elements (1) 012 Deep Nesting Elements 013 Deep Nesting Elements (1) 014 Dad Joke Break Raw React APIs 015 Intro to Using JSX 016 Compiling JSX 017 Compiling JSX (1) 018 Interpolation 019 Interpolation (1) 020 Spread props 021 Spread props (1) 022 Nesting JSX 023 Nesting JSX (1) 024 Fragments 025 Fragments (1) 026 Dad Joke Break Using JSX 027 Intro to Custom Components 028 Simple Function 029 Simple Function (1) 030 Raw API 031 Raw API (1) 032 JSX Components 033 JSX Components (1) 034 Props 035 Props (1) 036 Dad Joke Break Custom Components 037 Intro to TypeScript 038 Props (2) 039 Props (3) 040 Narrow Types 041 Narrow Types (1) 042 Derive Types 043 Derive Types (1) 044 Default Props 045 Default Props (1) 046 Reduce Duplication 047 Reduce Duplication (1) 048 Satisfies 049 Satisfies (1) 050 Dad Joke Break TypeScript 051 Intro to Styling 052 Styling 053 Styling (1) 054 Custom Component 055 Custom Component (1) 056 Size Prop 057 Size Prop (1) 058 Dad Joke Break Styling 059 Intro to Forms 060 Form 061 Form (1) 062 Form Action 063 Form Action (1) 064 Input Types 065 Input Types (1) 066 Submission 067 Submission (1) 068 Form Actions 069 Form Actions (1) 070 Dad Joke Break Forms 071 Intro to Inputs 072 Checkbox 073 Checkbox (1) 074 Select 075 Select (1) 076 Radios 077 Radios (1) 078 Hidden Inputs 079 Hidden Inputs (1) 080 Default Value 081 Default Value (1) 082 Dad Joke Break Inputs 083 Intro to Error Boundaries 084 Composition 085 Composition (1) 086 Other Errors 087 Other Errors (1) 088 Reset 089 Reset (1) 090 Dad Joke Break Error Boundaries 091 Intro to Rendering Arrays 092 Key prop 093 Key prop (1) 094 Focus State 095 Focus State (1) 096 Key Reset 097 Key Reset (1) 098 Dad Joke Break Rendering Arrays 099 Outro to React Fundamentals
2 React Hooks 001 React Hooks Intro 002 Intro to Managing UI State 003 useState 004 useState (1) 005 Controlling Inputs 006 Controlling Inputs (1) 007 Derive State 008 Derive State (1) 009 Initialize State 010 Initialize State (1) 011 Init Callback 012 Init Callback (1) 013 Dad Joke Break Managing UI State 014 Intro to Side-Effects 015 useEffect 016 useEffect (1) 017 Effect Cleanup 018 Effect Cleanup (1) 019 Dad Joke Break Side-Effects 020 Intro to Lifting State 021 Lift State 022 Lift State (1) 023 Lift More State 024 Lift More State (1) 025 Colocate State 026 Colocate State (1) 027 Dad Joke Break Lifting State 028 Intro to DOM Side-Effects 029 Refs 030 Refs (1) 031 Dependencies 032 Dependencies (1) 033 Primitive Dependencies 034 Primitive Dependencies (1) 035 Dad Joke Break DOM Side-Effects 036 Intro to Unique IDs 037 useId 038 useId (1) 039 Dad Joke Break Unique IDs 040 Intro to Tic Tac Toe 041 setState callback 042 setState callback (1) 043 Preserve State in localStorage 044 Preserve State in localStorage (1) 045 Add Game History Feature 046 Add Game History Feature (1) 047 Dad Joke Break Tic Tac Toe 048 Outro to React Hooks
3 Advanced React APIs
Outro to Advanced React APIs
4 React Suspense
Outro to React Suspense
5 Advanced React Patterns
Outro to Advanced React Patterns
6 React Performance
Outro to React Performance
7 React Server Components
Outro to React Server Components
8 Bonus. Interviews With Experts