在本书中,您学习了如何在编写 React 应用程序时应用最佳实践。在前几章中,我们回顾了基本概念,以建立坚实的理解,然后在接下来的章节中,我们跨越到更先进的技术。
现在,您应该能够构建可重用组件,使组件相互通信,并优化应用程序树以获得最佳性能。然而,开发人员也会犯错误,本章介绍了在使用 React 时应该避免的常见反模式。
查看常见错误将帮助您避免这些错误,并有助于您理解 React 如何工作以及如何以 React 方式构建应用程序。对于每个问题,我们将看到一个示例,说明如何重现和解决它。
在本章中,我们将介绍以下主题:
- 使用属性初始化状态
- 使用索引作为键
- DOM 元素的扩展属性
要完成本章,您需要以下内容:
- Node.js 12+
- Visual Studio 代码
您可以在本书的 GitHub 存储库中找到本章的代码:https://github.com/PacktPublishing/React-17-Design-Patterns-and-Best-Practices-Third-Edition/tree/main/Chapter13 。
在本节中,我们将看到如何使用从父级接收的属性初始化状态通常是一种反模式。我通常使用这个词**,因为我们将看到,一旦我们清楚这种方法的问题是什么,我们可能仍然会决定使用它。
*学习一些东西的最佳方法之一是查看代码,因此我们将首先创建一个带有+
按钮的简单组件来增加计数器。
该组件是使用类实现的,如以下代码片段所示:
import { FC, useState } from 'react'
type Props = {
count: number
}
const Counter: FC<Props> = (props) => {}
export default Counter
现在,让我们设置我们的count
状态:
const [state, setState] = useState<any>(props.count)
点击处理程序的实现非常简单——我们只需将1
添加到当前count
值中,并将结果值存储回state
:
const handleClick = () => {
setState({ count: state.count + 1 })
}
最后,我们呈现并描述输出,输出由count
状态的当前值和递增按钮组成:
return (
<div>
{state.count}
<button onClick={handleClick}>+</button>
</div>
)
现在,让我们呈现这个组件,将1
作为count
属性传递:
<Counter count={1} />
它按预期工作–每次点击*+*按钮都会增加当前值。那么,有什么问题?
有两个主要错误,概述如下:
- 我们有一个重复的真相来源。
- 如果传递给组件的
count
属性发生更改,则不会更新状态。
如果我们使用 React DevTools 检查Counter
元素,我们会注意到Props
和State
具有相似的值:
<Counter>
Props
count: 1
State
count: 1
这就不清楚在组件内部使用并向用户显示哪个是当前值得信赖的值。
更糟糕的是,点击*+*一次会导致数值出现偏差。以下代码中显示了这种差异的示例:
<Counter>
Props
count: 1
State
count: 2
此时,我们可以假设第二个值代表当前计数,但这不是显式的,可能会导致意外行为,或树中的错误值。
第二个问题集中在 React 如何创建和实例化类。组件的useState
函数在创建组件时只被调用一次。
在我们的Counter
组件中,我们读取count
属性的值并将其存储在状态中。如果该属性的值在应用程序的生命周期内发生变化(假设它变为10
,则Counter
组件将永远不会使用新值,因为它已经初始化。这会使组件处于不一致的状态,这不是最佳状态,也很难调试。
如果我们真的想使用道具的值来初始化组件,并且我们确信该值在将来不会改变,该怎么办?
在这种情况下,最好的做法是将其明确化,并给财产起一个明确的名称,例如initialCount
。例如,假设我们以以下方式更改Counter
组件的 prop 声明:
type Props = {
initialCount: number
}
const Counter: FC<Props> = (props) => {
const [count, setState] = useState<any>(props.initialCount)
...
}
如果我们像这样使用它,很明显父级只能初始化计数器,但是initialCount
属性的任何未来值都将被忽略:
<Counter initialCount={1} />
在下一节中,我们将学习有关键的知识。
在第 10 章中提高应用程序的性能,其中谈到了性能和调节器,我们看到了如何使用key
道具帮助 React 找出更新 DOM 的最短路径。
key 属性唯一地标识 DOM 中的元素,并使用它检查元素是否是新的,或者当组件属性或状态更改时是否必须更新元素。
使用键始终是一个好主意,如果不这样做,React 会在控制台中发出警告(在开发模式下)。然而,这不仅仅是使用钥匙的问题;有时,我们决定用作键的值可能会有所不同。事实上,在某些情况下,使用错误的键会给我们带来意想不到的行为。在本节中,我们将看到其中一个实例。
让我们再次创建一个List
组件,如下所示:
import { FC, useState } from 'react'
const List: FC = () => {
}
export default List
然后我们定义我们的状态:
const [items, setItems] = useState(['foo', 'bar'])
click 处理程序的实现与前一个略有不同,因为在本例中,我们需要在列表顶部插入一个新项:
const handleClick = () => {
const newItems = items.slice()
newItems.unshift('baz')
setItems(newItems)
}
最后,在render
中,我们显示列表和+
按钮,在列表顶部添加baz
项:
return (
<div>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
<button onClick={handleClick}>+</button>
</div>
)
如果在浏览器内运行组件,则不会看到任何问题;点击+
按钮,在列表顶部插入一个新项目。但是让我们做一个实验。
让我们按照以下方式更改render
,在每个项目附近添加一个输入字段。然后,我们使用输入字段,因为我们可以编辑其内容,从而更容易解决问题:
return (
<div>
<ul>
{items.map((item, index) => (
<li key={index}>
{item}
<input type="text" />
</li>
))}
</ul>
<button onClick={handleClick}>+</button>
</div>
)
如果我们在浏览器中再次运行此组件,复制输入字段中项目的值,然后单击*+*,我们将获得意外行为。
如以下屏幕截图所示,项目向下移动,而输入元素保持在相同的位置,其值与项目值不再匹配:
运行组件,单击+,然后检查控制台,应该会给出我们需要的所有答案。
我们可以看到,React 不是在顶部插入新元素,而是交换两个现有元素的文本,并在底部插入最后一项,就像它是新的一样。它这样做的原因是我们使用map
函数的索引作为键。
事实上,索引总是从0
开始,即使我们将一个新项目推到列表顶部,React 认为我们更改了现有两个项目的值,并在索引2
处添加了一个新元素。该行为与根本不使用 key 属性时的行为相同。
这是一种非常常见的模式,因为我们可能认为提供任何密钥都是最好的解决方案,但事实并非如此。密钥必须是唯一和稳定的,能够识别一个,并且只能识别一个项目。
为了解决这个问题,我们可以,例如,如果我们希望它不会在列表中重复,则使用该项的值,或者创建一个唯一标识符。
丹·阿布拉莫夫(Dan Abramov)最近将一种常见做法描述为反模式;当您在 React 应用程序中执行此操作时,它还会在控制台中触发警告。
*这是一种在社区中广泛使用的技术,我个人在现实项目中多次看到过这种技术。我们通常将属性分散到元素中,以避免手动写入每个元素,如下所示:
<Component {...props} />
这非常有效,Babel 将其转换为以下代码:
_jsx(Component, props)
但是,当我们将属性扩展到 DOM 元素中时,我们可能会添加未知的 HTML 属性,这是一种不好的做法。
问题不仅与排列运算符有关;逐个传递非标准属性会导致相同的问题和警告。由于 spread 操作符隐藏了我们正在扩展的单个属性,因此更难弄清楚我们传递给元素的是什么。
要在控制台中查看警告,我们可以执行的基本操作是呈现以下组件:
const Spread = () => <div foo="bar" />
我们得到的消息如下所示,因为foo
属性对于div
元素无效:
Unknown prop `foo` on <div> tag. Remove this prop from the element
在这种情况下,正如我们所说的,很容易找出要传递的属性并将其删除,但如果我们使用 spread 运算符(如以下示例中所示),则无法控制从父级传递哪些属性:
const Spread = props => <div {...props} />;
如果我们以以下方式使用该组件,则不存在任何问题:
<Spread className="foo" />
然而,如果我们做了如下的事情,情况就不是这样了。React 投诉,因为我们正在对 DOM 元素应用非标准属性:
<Spread foo="bar" className="baz" />
我们可以用来解决这个问题的一个解决方案是创建一个名为domProps
的属性,我们可以将它安全地扩展到组件,因为我们明确地说它包含有效的 DOM 属性。
例如,我们可以通过以下方式更改Spread
组件:
const Spread = props => <div {...props.domProps} />
然后,我们可以按如下方式使用它:
<Spread foo="bar" domProps={{ className: 'baz' }} />
正如我们在 React 中多次看到的那样,明确表达始终是一种良好的做法。
了解所有最佳实践始终是一件好事,但有时意识到反模式有助于我们避免走上错误的道路。最重要的是,了解某些技术被视为不良实践的原因有助于我们了解 React 是如何工作的,以及如何有效地使用它。
在本章中,我们介绍了使用组件的四种不同方式,这些组件可能会损害 web 应用程序的性能和行为。
对于其中的每一个,我们都使用了一个示例来重现问题,并提供了要应用的更改以修复问题。
我们了解了为什么使用属性初始化状态会导致状态和属性之间的不一致。我们还看到,使用错误的键属性会对协调算法产生不良影响。最后,我们了解了为什么将非标准属性扩展到 DOM 元素被认为是一种反模式。
在下一章中,我们将研究如何将 React 应用程序部署到生产环境中。**