原文:Common Beginner Mistakes with React (joshwcomeau.com)
对原文有些许修改。
在本教程中,我们将探讨 9 个最常见的陷阱。我们不会过多地深入探讨这些陷阱背后的原因。这更像是一个快速参考。
Evaluating with zero 用零进行评估
jsx
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;
我们的目标是有条件地显示一个购物清单。如果数组中至少有 1 个项目,我们应该渲染一个 ShoppingList 元素。否则,我们不应该渲染任何东西。
然而,我们最终可能会在界面上得到了一个 0 !
这是因为 items.length 被评估为 0 。由于在JavaScript中 0 是一个假值,所以 && 运算符会短路,整个表达式的结果为 0 。
我们的表达式应该使用"纯粹"的布尔值(true/false):
jsx
function App() {
const [items, setItems] = React.useState([]);
return (
<div>
{items.length > 0 && (
<ShoppingList items={items} />
)}
</div>
);
}
或者,我们可以使用三元表达式:
jsx
function App() {
const [items, setItems] = React.useState([]);
return (
<div>
{items.length
? <ShoppingList items={items} />
: null}
</div>
);
}
这两个选项都是完全有效的,最终取决于个人喜好。
Mutating state 状态变异
让我们继续使用购物清单的例子。假设我们有能力添加新的物品:
jsx
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;
每当用户提交一个新项目时,都会调用 handleAddItem 函数。不幸的是,它不起作用!当我们输入一个项目并提交表单时,该项目不会被添加到购物清单中。
问题在于:我们违反了 React 中可能最神圣的规则。我们正在改变状态。
具体问题在于这一行代码:
jsx
function handleAddItem(value) {
items.push(value);
setItems(items);
}
React依赖于状态变量的身份来判断状态是否发生了变化。当我们向数组中添加一个项目时,我们并没有改变该数组的身份,因此 React 无法知道值已经发生了变化。
如何修复:我们需要创建一个全新的数组。以下是我会这样做的方法:
jsx
function handleAddItem(value) {
const nextItems = [...items, value];
setItems(nextItems);
}
我不是修改现有的数组,而是从头开始创建一个新的数组。它包含了所有数组之前的项(通过 ... 扩展语法),以及新输入的项目。
当我们将一个值传递给类似于 setCount 的状态设置函数时,它需要是一个新的实体。
对于对象来说也是一样的:
jsx
// ❌ 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);
}
... 语法是一种将数组/对象中的所有内容复制/粘贴到全新实体中的方法。这确保了一切正常运作。
Not generating keys 不生成密钥
这是一个你可能以前见过的警告:
bash
Warning: Each child in a list should have a unique "key" prop.
警告:列表中的每个子元素都应该有一个唯一的"key"属性。
许多在线资源会建议使用数组索引来解决这个问题:
jsx
function ShoppingList({ items }) {
return (
<ul>
{items.map((item, index) => {
return (
<li key={index}>{item}</li>
);
})}
</ul>
);
}
更好的做法是,每当向列表中添加新项目时,我们最好为其生成一个唯一的 ID:
jsx
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。
以下是我们如何将其应用为 key:
jsx
function ShoppingList({ items }) {
return (
<ul>
{items.map((item, index) => {
return (
<li key={item.id}>
{item.label}
</li>
);
})}
</ul>
);
}
重要的是,当状态更新时,我们希望生成 ID。我们不希望这样做:
jsx
// ❌ This is a bad idea
<li key={crypto.randomUUID()}>
{item.label}
</li>
在 JSX 中这样生成会导致每次渲染时键值发生变化。每当键值发生变化时,React 将销毁并重新创建这些元素,这可能对性能产生很大的负面影响。
我们可以在数据首次创建时生成密钥来应用于各种情况。例如,下面是我从服务器获取数据时如何创建唯一 ID 的方法:
jsx
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);
}
Missing whitespace 缺少空格
这是我经常在网上看到的一个恶心的陷阱。
jsx
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 的工具)无法真正区分语法上的空白和我们为了缩进/代码可读性而添加的空白。
如何修复:我们需要在文本和锚点标签之间添加一个明确的空格字符:
jsx
<p>
Welcome to Corpitech.com!
{' '}
<a href="/login">Log in to continue</a>
</p>
一个小技巧:如果你使用 Prettier,它会自动为你添加这些空格字符!只需确保让它进行格式化(不要提前将内容拆分成多行)。
Accessing state after changing it 在修改状态后访问状态
这是一个最简单的计数器应用程序:点击按钮会增加计数。看看你能否发现问题:
jsx
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;
在增加 count 状态变量之后,我们将其值记录到控制台。奇怪的是,它记录的是错误的值:
问题在于:React 中的状态设置函数,如 setCount ,是异步的。
我们可以将它保存在一个变量中,这样我们就可以访问它:
jsx
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 等)。这样对我来说更清晰,我们不是更新当前值,而是安排下一个值。
Returning multiple elements 返回多个元素
有时候,一个组件需要返回多个顶级元素。
例如:
jsx
function LabeledInput({ id, label, ...delegated }) {
return (
<label htmlFor={id}>
{label}
</label>
<input
id={id}
{...delegated}
/>
);
}
export default LabeledInput;
我们希望我们的 LabeledInput 组件返回两个元素:一个 和一个 。令人沮丧的是,我们得到了一个错误:
bash
Adjacent JSX elements must be wrapped in an enclosing tag.
相邻的JSX元素必须包裹在一个封闭的标签中。
这是因为 JSX 编译成了普通的 JavaScript。当它在浏览器中运行时,代码看起来是这样的:
bash
function LabeledInput({ id, label, ...delegated }) {
return (
React.createElement('label', { htmlFor: id }, label)
React.createElement('input', { id: id, ...delegated })
);
}
在JavaScript中,我们不能像这样返回多个东西。这也是为什么这样做不起作用的原因:
bash
function addTwoNumbers(a, b) {
return (
"the answer is"
a + b
);
}
我们该如何修复它?长期以来,标准做法是将两个元素都包裹在一个包装标签中,比如 <div>
:
jsx
function LabeledInput({ id, label, ...delegated }) {
return (
<div>
<label htmlFor={id}>
{label}
</label>
<input
id={id}
{...delegated}
/>
</div>
);
}
通过将我们的 <label>
和 <input>
分组在 <div>
中,我们只返回一个顶级元素!
这是纯 JS 的样子:
jsx
function LabeledInput({ id, label, ...delegated }) {
return React.createElement(
'div',
{},
React.createElement('label', { htmlFor: id }, label),
React.createElement('input', { id: id, ...delegated })
);
}
我们还可以使用片段来进一步改进这个解决方案:
jsx
function LabeledInput({ id, label, ...delegated }) {
return (
<React.Fragment>
<label htmlFor={id}>
{label}
</label>
<input
id={id}
{...delegated}
/>
</React.Fragment>
);
}
React.Fragment 是一个 React 组件,它的存在完全是为了解决这个问题。它允许我们将多个顶级元素捆绑在一起,而不影响 DOM。这非常好:这意味着我们不会用一个不必要的 <div>
来污染我们的标记。
它还有一个方便的速记法。我们可以像这样写片段:
jsx
function LabeledInput({ id, label, ...delegated }) {
return (
<>
<label htmlFor={id}>
{label}
</label>
<input
id={id}
{...delegated}
/>
</>
);
}
React 团队选择使用一个空的 HTML 标签 <>
来展示片段不会产生任何真实的标记。
Flipping from uncontrolled to controlled 从不受控制到受控制的转变
让我们来看一个典型的表单,将输入与 React 状态绑定:
jsx
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;
如果您开始在此输入框中键入内容,您会注意到控制台会出现一个警告:
jsx
Warning: A component is changing an uncontrolled input to be controlled.
以下是修复方法:我们需要将 email 状态初始化为空字符串:
jsx
const [email, setEmail] = React.useState('');
当我们设置 value 属性时,我们告诉 React 我们希望这是一个受控制的输入。但只有在传递一个有定义的值时才有效!通过将 email 初始化为空字符串,我们确保 value 永远不会被设置为 undefined 。
Missing style brackets 缺少样式括号
JSX 的外观和感觉与 HTML 非常相似,但两者之间存在一些令人意外的差异,往往会让人措手不及。
在 HTML 中, style 被写成一个字符串:
html
<button style="color: red; font-size: 1.25rem">
Hello World
</button>
在 JSX 中,我们需要将其指定为一个对象,使用驼峰命名法的属性名称。
jsx
// 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 中创建了一个"表达式插槽"。内层的波浪线创建了一个包含样式的 JS 对象。
Async effect function 异步效果函数
假设我们有一个函数,在挂载时从我们的 API 中获取一些用户数据。我们将使用 useEffect 钩子,并且我们想要使用 await 关键字。
这是我第一次尝试:
jsx
import React from 'react';
import { API } from './constants';
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null);
React.useEffect(async () => {
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;
我们得到了一个晦涩的错误信息:
bash
destroy is not a function
destroy不是一个函数
这是修复方法:我们需要在 effect 内部创建一个单独的异步函数:
jsx
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 关键字实际上是做什么的。
举个例子,你猜猜这个函数返回什么?
jsx
async function greeting() {
return "Hello world!";
}
乍一看,似乎很明显:它返回字符串 "Hello world!" !但实际上,这个函数返回一个 _promise,_这个 promise 最后会解析为字符串 "Hello world!" 。
这是一个问题,因为 useEffect 钩子不希望我们返回一个 promise!它希望我们返回的要么是空(就像我们上面所做的),要么是一个清理函数。
清理函数超出了本教程的范围,但它们非常重要。大多数 effect 都会有一些清理逻辑,我们需要尽快将其提供给 React,以便 React 在依赖项发生变化或组件卸载时调用它。
通过我们的"分离异步函数"策略,我们仍然能够立即返回一个清理函数:
jsx
React.useEffect(() => {
async function runEffect() {
// Effect logic here
}
runEffect();
return () => {
// Cleanup logic here
}
}, [userId]);
你可以随意给这个函数取名,但我喜欢通用的名字 runEffect 。这样清楚地表明它包含了主要的 effect 逻辑。