译文:React 常见 9 个小陷阱

原文: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 逻辑。

相关推荐
阿伟来咯~5 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端10 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱13 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai22 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨23 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js