React 新手常犯的十个错误
介绍了刚入门 React 的新人,在编写代码的时候遇到一些常见问题,并给出了解决方案。
这是我关注的一位前端开发者的分享,分享的非常棒!值得被更多人看到,原文英文版本,我做了翻译,希望对 React 开发者有所帮助。
在我最近开发的时候,在遇到复杂状态管理和副作用的时候,就会经常不能完全想到所有可能发生的情况,导致一些莫名其妙的 bug,如果你也是,那看完吧,一定有新的收获。以下是正文部分:
“几年前,我在当地的一个编码训练营教 React,我注意到有一些事情让学生措手不及。人们不断掉进同一个坑里!在本教程中,我们将探索 8 个最常见的陷阱。您将学习如何绕过它们,并希望能够避免很多挫败感。为了保持这篇博客文章轻松流畅,我们不会过多探讨这些陷阱背后的原因。这更像是一个快速参考。下面看下常见的问题吧!
目标受众
这篇文章是为那些对React基础知识已经有所了解,但在他们的学习旅程中仍然处于初级阶段的开发者编写的。
1. 使用 0 作为判断依据
好的,我们从其中一个最普遍的陷阱开始。我在一些生产应用程序中实际遇到过这个问题!
请看下以下代码:
// react app.js
import React from 'react';
import ShoppingList from './ShoppingList';
function App() {
const [items, setItems] = React.useState([]);
return (
<div>
{items.length && <ShoppingList items={items} />}
</div>
);
}
export default App;
// ShoppingList.js
import React from 'react';
function ShoppingList({ items }) {
return (
<>
<h1>Shopping List</h1>
<ul>
{items.map((item, index) => {
// NOTE: We shouldn't use “index” as the key!
// This is covered later in this post 😄
return (
<li key={index}>
{item}
</li>
);
})}
</ul>
</>
);
}
export default ShoppingList
我们的目标是有条件地显示一个购物清单。如果数组中至少有1个项目,我们应该渲染一个ShoppingList元素。否则,我们不应该渲染任何内容。然而,我们最终在 UI 中得到一个随机的 0
!
发生这种情况是因为 items.length
的计算结果为 0
。由于 0 在 JavaScript 中是一个虚假值,因此 &&
运算符短路,整个表达式解析为 0
。实际上,就好像我们这样做了一样:
function App() {
return (
<div>
{0}
</div>
);
}
与其他假值(如''、null、false等)不同,数字0是JSX中的一个有效值。毕竟,在许多情况下,我们确实希望打印数字0!
如何修复: 我们的表达式应该使用一个“纯粹”的布尔值(true/false):
function App() {
const [items, setItems] = React.useState([]);
return (
<div>
{items.length > 0 && (
<ShoppingList items={items} />
)}
</div>
);
}
items.length > 0
将始终计算为 true
或 false
,因此我们永远不会有任何问题。或者,我们可以使用三元表达式:
function App() {
const [items, setItems] = React.useState([]);
return (
<div>
{items.length
? <ShoppingList items={items} />
: null}
</div>
);
}
这两种选择都是完全有效的,这取决于个人品味。
2. 不正确使用状态
让我们继续使用我们的购物清单示例。假设我们能够添加新项目:
// app.js
import React from 'react';
import ShoppingList from './ShoppingList';
import NewItemForm from './NewItemForm';
function App() {
const [items, setItems] = React.useState([
'apple',
'banana',
]);
function handleAddItem(value) {
items.push(value);
setItems(items);
}
return (
<div>
{items.length > 0 && <ShoppingList items={items} />}
<NewItemForm handleAddItem={handleAddItem} />
</div>
)
}
export default App;
// ShoppingList.js
import React from 'react';
function ShoppingList({ items }) {
return (
<>
<h1>Shopping List</h1>
<ul>
{items.map((item, index) => {
// NOTE: We shouldn't use “index” as the key!
// This is covered later in this post 😄
return (
<li key={index}>
{item}
</li>
);
})}
</ul>
</>
);
}
export default ShoppingList;
// NewItemFrom.js
import React from 'react';
function NewItemForm({ handleAddItem }) {
const [value, setValue] = React.useState('');
return (
<form
onSubmit={(event) => {
event.preventDefault();
handleAddItem(value);
setValue('');
}}
>
{/* We'll touch on this ID stuff later too! */}
<label htmlFor="new-item-input">
Item:
</label>
<input
value={value}
onChange={event => setValue(event.target.value)}
/>
<button>
Add Item
</button>
</form>
);
}
export default NewItemForm;
可看到如下:
每当用户提交一个新项目时,handleAddItem
函数会被调用。不幸的是,它目前无法正常工作!当我们输入一个项目并提交表单时,该项目并没有被添加到购物清单中。
问题 出在我们违反了React中可能是最重要的规则之一:我们修改了状态的值。具体来说,问题是这一行:
// app.js
function handleAddItem(value) {
items.push(value);
setItems(items);
}
React 依靠状态变量的身份(也就是这里的 useState hooks)来判断状态何时发生了变化。当我们把一个项目推到数组中时,我们不会改变该数组的身份,所以 React 无法判断这个值已经改变。
如何修复: 我们需要创建一个全新的数组。以下是我的做法:
// app.js
function handleAddItem(value) {
const nextItems = [...items, value]; // 不知道有人看过我之前的文章不,这里也是遵守 react 的不可变性原则
setItems(nextItems);
}
与其修改现有的数组,我选择从头开始创建一个新的数组。这个新数组包含了与原数组完全相同的所有项(借助展开语法...),以及新添加的项。这里的区别在于修改现有项与创建新项之间的差异。当我们将一个值传递给像 setCount 这样的状态设置函数时,它需要是一个新的实体。对象也是如此:
// ❌ Mutates an existing object
function handleChangeEmail(nextEmail) {
user.email = nextEmail;
setUser(user);
}
// ✅ Creates a new object
function handleChangeEmail(email) {
const nextUser = { ...user, email: nextEmail };
setUser(nextUser);
}
基本上,... 语法是一种将数组/对象中的所有内容复制/粘贴到全新实体中的方式。这确保一切正常运作。
3. 不生成密钥
以下是您之前可能看到的警告:
Warning: Each child in a list should have a unique "key" prop.
警告:列表中的每个子元素应该有一个唯一的 "key" 属性。
最常见的情况是在对数据进行映射时发生此错误。以下是一个违反此规则的示例:
// app.js
import React from 'react';
import ShoppingList from './ShoppingList';
import NewItemForm from './NewItemForm';
function App() {
const [items, setItems] = React.useState([
'apple',
'banana',
'carrot',
]);
function handleAddItem(value) {
const nextItems = [...items, value]
setItems(nextItems);
}
return (
<div>
{items.length > 0 && <ShoppingList items={items} />}
<NewItemForm handleAddItem={handleAddItem} />
</div>
)
}
export default App;
// ShoppingList.js
import React from 'react';
function ShoppingList({ items }) {
return (
<ul>
{items.map((item) => {
return (
<li>{item}</li>
);
})}
</ul>
);
}
export default ShoppingList;
// NewItemForm.js
import React from 'react';
function NewItemForm({ handleAddItem }) {
const [value, setValue] = React.useState('');
return (
<form
onSubmit={(event) => {
event.preventDefault();
handleAddItem(value);
setValue('');
}}
>
{/* We'll touch on this ID stuff later too! */}
<label htmlFor="new-item-input">
Item:
</label>
<input
value={value}
onChange={event => setValue(event.target.value)}
/>
<button>
Add Item
</button>
</form>
);
}
export default NewItemForm;
你会看到:
每当我们渲染一个元素数组时,我们需要为 React 提供额外的上下文信息,以便它能够识别每个项。关键是,这个标识符需要是唯一的。许多在线资源会建议使用数组索引来解决这个问题:
// ShoppingList.js
function ShoppingList({ items }) {
return (
<ul>
{items.map((item, index) => {
return (
<li key={index}>{item}</li>
);
})}
</ul>
);
}
我不认为这是一个好的建议。这种方法有时可以工作,但在其他情况下可能会引发一些严重的问题。随着对 React 工作原理的更深入理解,您将能够在每个具体情况下判断是否可以使用这种方法,但老实说,我认为以一种始终安全的方式解决问题会更容易。这样,您就不必担心它!
下面是计划: 每当向列表中添加新项时,我们将为其生成一个唯一的ID:
// app.js
function handleAddItem(value) {
const nextItem = {
id: crypto.randomUUID(),
label: value,
};
const nextItems = [...items, nextItem];
setItems(nextItems);
}
crypto.randomUUID
是内置于浏览器中的方法(它不是第三方包)。它在所有主要浏览器中都可用。它与加密货币无关。该方法生成一个类似于 d9bb3c4c-0459-48b9-a94c-7ca3963f7bd0
的唯一字符串。通过在用户提交表单时动态生成一个 ID,我们确保购物清单中的每个项都具有唯一的 ID。以下是如何将其应用为键的方法:
// ShoppingList.js
function ShoppingList({ items }) {
return (
<ul>
{items.map((item, index) => {
return (
<li key={item.id}>
{item.label}
</li>
);
})}
</ul>
);
}
重要的是,我们希望在状态更新时生成ID,而不是这样做:
// ❌ This is a bad idea
<li key={crypto.randomUUID()}>
{item.label}
</li>
在 JSX 中这样生成会导致键在每次渲染时发生变化。当键发生变化时,React将销毁并重新创建这些元素,这可能对性能产生重大负面影响。这种模式——在创建数据时生成键——可以应用于各种情况。例如,下面是我在从服务器获取数据时创建唯一ID的方式:
const [data, setData] = React.useState(null);
async function retrieveData() {
const res = await fetch('/api/data');
const json = await res.json();
// The moment we have the data, we generate
// an ID for each item:
const dataWithId = json.data.map(item => {
return {
...item,
id: crypto.randomUUID(),
};
});
// Then we update the state with
// this augmented data:
setData(dataWithId);
}
4. 缺少空格
这是我在网络上经常看到的一个非常让人困惑的问题。
import React from 'react';
function App() {
return (
<p>
Welcome to Corpitech.com!
<a href="/login">Log in to continue</a>
</p>
);
}
export default App;
你会看到:
注意到这两个句子被混在一起了:
这是因为 JSX 编译器(将我们编写的 JSX 转换为适用于浏览器的 JavaScript 的工具)无法真正区分语法上的空格和我们为缩进/代码可读性而添加的空格。
如何修复它: 我们需要在文本和锚点标记之间添加一个显式的空格字符:
<p>
Welcome to Corpitech.com!
{' '}
<a href="/login">Log in to continue</a>
</p>
一个小小的专业技巧:如果您使用 Prettier
,它会自动为您添加这些空格字符!只需确保让它进行格式化(不要提前将内容拆分为多行)。
为什么 React 团队没有解决这个问题?
当我第一次了解到这种策略时,我也觉得有些凌乱。为什么 React 团队不能修复它,使其按照我们的预期工作呢?!我后来意识到这个问题并没有完美的解决方案。如果 React 开始将缩进解释为语法上的空格,可以解决这个问题,但也会引入一系列其他问题。最终,尽管它看起来有些笨拙,但我认为这是正确的决定。这是最不糟糕的选择!
5. 在修改状态后访问它
这个问题在某个时刻都会让人措手不及。在我在一家本地编程训练营教授时,我已经记不清有多少次有人因为这个问题向我求助了。下面是一个最简单的计数器应用程序:点击按钮会增加计数。看看你能否发现问题所在:
import React from 'react';
function App() {
const [count, setCount] = React.useState(0);
function handleClick() {
setCount(count + 1);
console.log({ count });
}
return (
<button onClick={handleClick}>
{count}
</button>
);
}
export default App;
你会看到:
在增加计数状态变量后,我们将其值记录到控制台。令人奇怪的是,它记录了错误的值:
问题出在这里:React 中的状态设置函数(例如 setCount)是异步的。 这是有问题的代码是这里:
function handleClick() {
setCount(count + 1);
console.log({ count });
}
很容易错误地认为 setCount 函数类似于赋值,好像这样做是等同于这样的操作:
count = count + 1;
console.log({ count });
然而,React 并不是这样构建的。当我们调用 setCount 时,我们并没有重新赋值给一个变量,而是安排了一个更新操作。
对于我们完全理解这个概念可能需要一些时间,但下面的解释或许有助于更好地理解:我们无法重新赋值给 count 变量,因为它是一个常量!
// Uses `const`, not `let`, and so it can't be reassigned
const [count, setCount] = React.useState(0);
count = count + 1; // Uncaught TypeError:
// Assignment to constant variable
那么我们应该如何修复这个问题呢?幸运的是,我们已经知道这个值应该是什么。我们需要将它存储在一个变量中,以便我们可以访问它:
function handleClick() { // 再次验证不可变性重要性,使用新的变量记录最新的状态
const nextCount = count + 1;
setCount(nextCount);
// Use `nextCount` whenever we want
// to reference the new value:
console.log({ nextCount });
}
我喜欢在这种情况下使用“next”前缀(比如nextCount、nextItems、nextEmail等)。这样对我来说更清晰,我们不是更新当前值,而是安排下一个值。
6. 返回多个元素
有时,一个组件需要返回多个顶级元素。例如:
// app.js
import React from 'react';
import LabeledInput from './LabeledInput';
function App() {
const [name, setName] = React.useState('');
return (
<LabeledInput
id="name"
label="Your name"
value={name}
onChange={(event) => setName(event.target.value)}
/>
);
}
export default App;
// LabeledInput.js
function LabeledInput({ id, label, ...delegated }) {
return (
<label htmlFor={id}>
{label}
</label>
<input
id={id}
{...delegated}
/>
);
}
export default LabeledInput;
你会看到:
我们希望我们的 LabeledInput 组件返回两个元素:一个 <label>
和一个 <input>
。令人沮丧的是,我们遇到了一个错误:
这是因为 JSX 编译为普通的 JavaScript。下面是在浏览器中运行时这段代码的样子:
function LabeledInput({ id, label, ...delegated }) {
return (
React.createElement('label', { htmlFor: id }, label)
React.createElement('input', { id: id, ...delegated })
);
}
在 JavaScript 中,我们不能像这样返回多个值。这也是为什么这种写法不起作用的原因:
function addTwoNumbers(a, b) {
return (
"the answer is"
a + b
);
}
我们该如何修复呢?很长一段时间以来,标准做法是将这两个元素包装在一个包裹标签中,比如 <div>
:
function LabeledInput({ id, label, ...delegated }) {
return (
<div>
<label htmlFor={id}>
{label}
</label>
<input
id={id}
{...delegated}
/>
</div>
);
}
通过将 <label>
和 <input>
包装在 <div>
中,我们只返回一个顶层元素!以下是它在普通的 JavaScript 中的样子:
function LabeledInput({ id, label, ...delegated }) {
return React.createElement(
'div',
{},
React.createElement('label', { htmlFor: id }, label),
React.createElement('input', { id: id, ...delegated })
);
}
JSX 是一个很棒的抽象,但它常常会掩盖关于 JavaScript 的基本真理。我认为,查看 JSX 如何转换为普通的 JavaScript,以了解实际发生的情况往往是有帮助的。通过这种新的方法,我们返回一个单独的元素,而该元素包含两个子元素。问题解决了!但我们可以使用片段(fragments)进一步改进这个解决方案:
function LabeledInput({ id, label, ...delegated }) {
return (
<React.Fragment>
<label htmlFor={id}>
{label}
</label>
<input
id={id}
{...delegated}
/>
</React.Fragment>
);
}
React.Fragment
是一个专门用来解决这个问题的React组件。它允许我们将多个顶级元素捆绑在一起,而不会影响DOM。这非常棒:这意味着我们不会在标记中加入不必要的<div>
。它还有一个便捷的简写方式,我们可以像这样编写片段:
function LabeledInput({ id, label, ...delegated }) {
return (
<>
<label htmlFor={id}>
{label}
</label>
<input
id={id}
{...delegated}
/>
</>
);
}
我喜欢这里的符号意义:React团队选择使用一个空的HTML标签<>
来表示片段不会产生任何实际的标记。
7. 从不受控制变为受控制状态
让我们来看一个典型的表单示例,将一个输入与React状态绑定起来:
import React from 'react';
function App() {
const [email, setEmail] = React.useState();
return (
<form>
<label htmlFor="email-input">
Email address
</label>
<input
id="email-input"
type="email"
value={email}
onChange={event => setEmail(event.target.value)}
/>
</form>
);
}
export default App;
你会看到:
如果你在这个输入框中开始输入,你会注意到控制台上会出现一个警告:
解决方法如下:我们需要将 email
状态初始化为空字符串:
const [email, setEmail] = React.useState('');
当我们设置value属性时,我们告诉 React 我们希望这是一个受控输入框。但是,这只有在我们传递一个定义的值时才起作用!通过将email初始化为空字符串,我们确保 value 永远不会被设置为undefined。
受控输入
如果您想详细了解为什么这是必要的,以及什么是“受控输入”,我们将在我最近发布的教程中深入探讨这些想法:React中的数据绑定
JSX 被设计得看起来很像 HTML,但它们之间有一些令人惊讶的差异,往往会让人措手不及。大多数差异都有很好的文档记录,而且控制台的警告通常非常具体和有帮助。例如,如果你意外使用 class 而不是className,React会准确告诉你问题所在。但有一个微妙的差异经常让人困惑:style属性。在HTML中,style是以字符串的形式表示的:
<button style="color: red; font-size: 1.25rem">
Hello World
</button>
但是,在 JSX 中,我们需要将其指定为一个对象,并带有驼峰属性名称。在下面的代码中,我试图做到这一点,但出现了错误。你能找出错误吗?
import React from 'react';
function App() {
return (
<button
style={ color: 'red', fontSize: '1.25rem' }
>
Hello World
</button>
);
}
export default App;
你会看到:
问题是我需要使用双波浪线,如下所示:
<button
// "{{", instead of "{":
style={{ color: 'red', fontSize: '1.25rem' }}
>
Hello World
</button>
为了理解为什么这样是必要的,我们需要稍微了解一下这个语法。在JSX中,我们使用花括号来创建一个表达式插槽。我们可以在这个插槽中放置任何有效的JavaScript表达式。例如:
<button className={isPrimary ? 'btn primary' : 'btn'}>
无论我们在中放置什么,都将被视为JavaScript进行求值,并将结果设置为该属性的值。className将是'btn primary'或'btn'。对于style,我们首先需要创建一个表达式插槽,然后将一个JavaScript对象传递到这个插槽中。我认为如果我们将对象提取到一个变量中,会更清晰明了:
// 1. Create the style object:
const btnStyles = { color: 'red', fontSize: '1.25rem' };
// 2. Pass that object to the `style` attribute:
<button style={btnStyles}>
Hello World
</button>
// Or, we can do it all in 1 step:
<button style={{ color: 'red', fontSize: '1.25rem' }}>
外层的花括号创建了JSX中的"表达式插槽"。内层的花括号创建了一个JavaScript对象,用于保存我们的样式。
8. 异步的 effect 函数
假设我们有一个函数,在挂载时从 API 中获取一些用户数据。我们将使用 useEffect 钩子,并希望使用 await 关键字。以下是我第一次尝试的代码:
// app.js
import React from 'react';
import UserProfile from './UserProfile';
function App() {
return (
<UserProfile userId="abc123" />
);
}
export default App;
// UserProfile.js
import React from 'react';
import { API } from './constants';
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
const url = `${API}/get-profile?id=${userId}`;
const res = await fetch(url);
const json = await res.json();
setUser(json.user);
}, [userId]);
if (!user) {
return 'Loading…';
}
return (
<section>
<dl>
<dt>Name</dt>
<dd>{user.name}</dd>
<dt>Email</dt>
<dd>{user.email}</dd>
</dl>
</section>
);
}
export default UserProfile;
// constants.js
export const API = 'https://jor-test-api.vercel.app/api';
你会看到:
不幸的是,我们收到一个错误:
这是修正的方法: 我们需要在 effect 中创建一个单独的异步函数:
React.useEffect(() => {
// Create an async function...
async function runEffect() {
const url = `${API}/get-profile?id=${userId}`;
const res = await fetch(url);
const json = await res.json();
setUser(json);
}
// ...and then invoke it:
runEffect();
}, [userId]);
为了理解为什么需要这个变通方法,值得考虑一下 async 关键字的实际作用。例如,你会猜测下面这个函数返回什么?
async function greeting() {
return "Hello world!";
}
乍一看,似乎很明显:它返回字符串"Hello world!"
!但实际上,这个函数返回一个 Promise。这个 Promise 解析为字符串 "Hello world!"
。这是一个问题,因为 useEffect 钩子并不期望我们返回一个 Promise!它期望我们返回无值(就像我们上面的示例中所做的),或者返回一个清除函数。清除函数远超出了本教程的范围,但它们非常重要。大多数的 effect 都会有一些拆除逻辑,我们需要尽快将其提供给 React,这样 React 就可以在依赖项更改或组件卸载时调用它。 通过使用"单独的异步函数"策略,我们仍然可以立即返回一个清除函数:
React.useEffect(() => {
async function runEffect() {
// Effect logic here
}
runEffect();
return () => {
// Cleanup logic here
}
}, [userId]);
你可以将这个函数命名为任何你喜欢的名称,但我喜欢使用通用名称 runEffect。它清晰地表明它包含了主要的 effect 逻辑。
9. 培养直觉 (经验)
一开始,我们在这个教程中看到的很多修复方法似乎相当随意。为什么我们需要提供一个唯一的键(key)?为什么我们无法在改变状态后访问它?为什么useEffect如此棘手?React一直以来都相当棘手,尤其是现在使用hooks时更是如此。需要一段时间才能理解一切。我在2015年开始使用React,还记得当时想:“这玩意太酷了,但我完全不知道它是如何工作的。” 😅从那时起,我逐渐构建了自己关于React的思维模型,一块一块地拼凑。我经历了一系列的顿悟,每一次顿悟都使我的思维模型更加坚实、更加完善。我开始理解React为什么会以这种方式工作。我发现自己不必记住任意的规则;相反,我可以依靠自己的直觉。很难言喻 React 对我来说变得更加有趣了!在过去的一年里,我一直在开发一门名为《React的乐趣》的交互式自学在线课程。这是一门面向初学者的课程,旨在帮助你建立对React工作原理的直觉,以便用它构建丰富、动态的Web应用程序。我的课程与其他课程不同;你不会坐在那里看我连续几个小时地编码。《React的乐趣》结合了许多不同的媒体形式:视频、交互式文章、挑战性练习、以真实世界为灵感的项目,甚至还有一两个小游戏。《React的乐趣》将在几个月内发布。你可以在课程主页上了解更多信息,并注册接收更新:《react的乐趣》
” END
我想补充两个我最近开发遇到的问题,
- 第一个就是 useState 的异步问题,当我们setState后如果立刻取值 state 得到的可能不是最新的值,这真的很坑人,
- 第二个是 在React 中,如果想要立刻获取到最新的值,你可以使用 useRef 进行同步,但是它不能更新UI视图,所以一定要记得使用 state 去触发页面 UI 更新,
- 当面临列表数据,并且页面渲染复杂时候,一定要记得给组件使用 React.memo 包裹,当props更新的时候才重新渲染页面,这样就能避免不必要的刷新
文章很长,需要花10~15分钟才能看完,认真看完的同学,你真的好厉害,也希望你在 React 的开发者寻找到自己的最佳实践
原文链接: 常见的React初学者错误