【译】React 新手踩坑指南:9 个让你秃头的常见错误 🚨

🔗 原文链接:# Common Beginner Mistakes with React

👨‍💻 原作者:Josh W. Comeau

📅 发布时间:2023年3月6日

🕐 最后更新:2024年10月13日
⚠️ 关于本译文

本文基于 Josh W. Comeau 的原文进行忠实翻译,力求准确传达原作者的技术观点和逻辑结构。

🎨 特色亮点:

  • 保持原文的完整性和技术准确性
  • 采用自然流畅的中文表达,避免翻译腔
  • 添加画外音板块,提供译者的补充解读和实践心得
  • 使用生动比喻帮助理解复杂概念

💡 画外音说明: 文中标注为画外音的部分是译者基于实际开发经验添加的拓展解释,旨在帮助读者更好地理解和应用这些概念,不代表原作者观点。

🖼️ 关于交互式示例: 本文中的图片和交互式演示以截图和GIF动图形式呈现。如需体验完整的交互式功能,可前往原文进行实际操作。

几年前我在当地训练营教 React 的时候,发现学生们总是在同样的地方翻车。就像有个看不见的陷阱一直在那儿等着大家往里跳!

这篇文章就是要盘点 9 个最坑人的陷阱。学会了这些,你就能华丽地绕过这些坑,避免无数次想砸电脑的冲动。

咱们就不深究这些坑为啥存在了,直接上干货,把这当成一本"避坑指南"来用就行。

🎯 这篇文章适合谁看?

如果你已经知道 React 的基本概念,但还在新手村里打怪升级,那这篇文章就是为你量身定制的。


错误1️⃣:用零值进行条件渲染

咱们先从一个特别常见的坑开始。这个坑我在生产环境里见过好多次,简直是新手收割机!

看看下面这个例子:

我们本来想这样做:如果购物车里有东西就显示购物清单,没有就什么都不显示。听起来很简单对吧?

结果界面上蹦出来一个莫名其妙的 0

这是咋回事呢?原来 items.length 算出来是 0,而在 JavaScript 里,0 是个假值,所以 && 操作符直接短路了,整个表达式就变成了 0

简单说就是相当于你写了这样的代码:

jsx 复制代码
function App() {
  return (
    <div>
      {0}
    </div>
  );
}

💡 画外音: 这绝对是 React 新手最容易掉的坑,没有之一!和其他假值(空字符串、nullfalse 这些)不一样,数字 0 在 JSX 里是个合法的显示内容。毕竟咱们有时候确实需要显示 0 这个数字嘛!

🔧 怎么修复?

简单!让表达式返回真正的布尔值就行了:

jsx 复制代码
function App() {
  const [items, setItems] = React.useState([]);

  return (
    <div>
      {items.length > 0 && (
        <ShoppingList items={items} />
      )}
    </div>
  );
}

items.length > 0 永远只会返回 truefalse,再也不会出幺蛾子了。

当然你也可以用三元表达式(看个人喜好):

jsx 复制代码
function App() {
  const [items, setItems] = React.useState([]);

  return (
    <div>
      {items.length
        ? <ShoppingList items={items} />
        : null}
    </div>
  );
}

这两种写法都没问题,选你看着顺眼的就行。


错误2️⃣: 直接修改 state

继续拿购物清单举例子。假设现在要给它加个添加商品的功能:

用户提交新商品的时候会调用 handleAddItem 函数。结果...完全没反应!明明输入了商品还点了提交,但购物清单还是老样子。

问题在哪儿? 我们犯了 React 里的大忌------直接改 state!

具体来说就是这行代码有问题:

jsx 复制代码
function handleAddItem(value) {
  items.push(value);
  setItems(items);
}

React 是怎么知道 state 变了的呢?它看的是对象的"身份证"。你用 push 往数组里塞东西,数组的身份证没变,React 就觉得"咦?没变化啊,我就不更新了"。

💡画外音: 这玩意儿刚开始真的很反直觉!其他语言里直接改数据是常规操作,但 React 偏偏要搞特殊。习惯了就好,就当它有洁癖吧。

🔧 怎么修复?

简单粗暴------造个新数组:

jsx 复制代码
function handleAddItem(value) {
  const nextItems = [...items, value];
  setItems(nextItems);
}

我没有改原来的数组,而是造了个全新的。用 ... 把原来的内容全部复制过来,再加上新商品。

记住一个原则:给 setCount 这类 "state 设置"函数传值的时候,必须传个"崭新"的东西。

对象也是同样的道理:

jsx 复制代码
// ❌ 直接改现有对象,React会装作没看见
function handleChangeEmail(nextEmail) {
  user.email = nextEmail;
  setUser(user);
}

// ✅ 造个新对象,React开心了
function handleChangeEmail(nextEmail) {
  const nextUser = { ...user, email: nextEmail };
  setUser(nextUser);
}

... 这个语法就是个复制粘贴神器,把原来的东西全部拷贝到新地方,保证 React 能认出来"哦,这是新的!"


错误 3️⃣ 没有给列表项加 key

你肯定见过这个让人头疼的警告:

Warning: Each child in a list should have a unique "key" prop.

最常出现的场景就是用 map 渲染列表的时候。来看个经典的反面教材:

每次渲染数组的时候,React 需要知道"这些东西谁是谁",所以需要给每个元素一个唯一的身份标识。

网上很多教程会告诉你直接用数组的索引:

jsx 复制代码
function ShoppingList({ items }) {
  return (
    <ul>
      {items.map((item, index) => {
        return (
          <li key={index}>{item}</li>
        );
      })}
    </ul>
  );
}

但我觉得这就是在挖坑! 这种方法有时候能用,但很多时候会出大问题。

💡 画外音: 用 index 做 key 确实是网上最常见的"快速解决方案",但这就像用创可贴修水管------治标不治本,早晚出事!

等你对 React 理解更深了,你可能能判断什么时候能这么用,但说实话,我觉得直接用永远安全的方法更省心。毕竟谁愿意天天提心吊胆呢?

🔧 正确的做法

每次往列表里加东西的时候,给它生成一个唯一 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。

然后这么用:

jsx 复制代码
function ShoppingList({ items }) {
  return (
    <ul>
      {items.map((item) => {
        return (
          <li key={item.id}>
            {item.label}
          </li>
        );
      })}
    </ul>
  );
}

记住一定要在更新 state 的时候生成 ID,千万别这么写:

jsx 复制代码
// ❌ 这样写就是在作死
<li key={crypto.randomUUID()}>
  {item.label}
</li>

在 JSX 里面这么生成会导致每次渲染 key 都变,React 会疯狂地删除重建元素,性能直接拉胯。

💡画外音: 从服务器拿数据的时候也可以这么搞。就算后端没给 ID,咱们前端自己造一个也行。


错误4️⃣:缺少空白字符

这是个特别阴险的坑,我在网上见过无数次。

看到没?两个句子直接黏在一起了,就像连体字一样!

这是为啥呢?JSX 编译器(就是那个把你写的 JSX 转换成浏览器能懂的 JavaScript 的工具)分不清哪些空白是为了美化代码的,哪些是真的需要显示的空格。

🔧 怎么修复?

手动加个空格字符就行了:

jsx 复制代码
<p>
  欢迎来到 Corpitech.com!
  {' '}
  <a href="/login">登录继续</a>
</p>

小贴士: 如果你用 Prettier,它会自动帮你加这些空格!前提是你得让它自由发挥(别提前手动换行)。

React 团队为啥不修这个 bug?

刚知道这个解决方案的时候,我就想:React 团队咋不把这个修了呢?让它按正常逻辑工作不行吗!

后来我才明白,这事儿没有完美解决方案。如果 React 把所有缩进都当成空格,虽然解决了这个问题,但会引发一堆新的麻烦。

所以虽然看起来有点 hack,但这确实是最合理的选择。总得有人妥协不是?
💡 画外音

这确实是 React 设计上的一个取舍。刚开始觉得别扭,用久了就习惯了。反正程序员嘛,适应能力都很强的。


错误5️⃣: 改变 state 后立即访问

这个问题迟早会整到每个人。我当老师的时候,学生们拿着这个问题来找我的次数,我都数不过来了。

来看个简单的计数器:点击按钮数字就加 1。你能发现哪里有问题吗?

我们改完 count 之后马上打印到控制台,结果...打印出来的是旧值!

问题在哪? React 里像 setCount 这样的 state 设置函数是异步的!

就是这段代码有问题:

jsx 复制代码
function handleClick() {
  setCount(count + 1);
  console.log({ count });
}

很容易以为 setCount 跟普通赋值一样,就像这样:

jsx 复制代码
count = count + 1;
console.log({ count });

但 React 不是这么工作的!调用 setCount 的时候,你不是在重新赋值变量,而是在"预约"一个更新。

想要理解这个概念,有个小窍门:你根本就不能给 count 重新赋值,因为它是个常量!

jsx 复制代码
// 用的是 `const`,不是 `let`,所以根本改不了
const [count, setCount] = React.useState(0);

count = count + 1; // Uncaught TypeError:
                   // Assignment to constant variable

🔧 怎么修复?

既然我们知道新值应该是啥,那就把它存到变量里呗:

jsx 复制代码
function handleClick() {
  const nextCount = count + 1;
  setCount(nextCount);

  // 想用新值的时候就用 `nextCount`:
  console.log({ nextCount });
}

我喜欢用 "next" 开头来命名这些变量(nextCount、nextItems、nextEmail 啥的)。这样一看就知道这是"下一个值",不是当前值。

💡 画外音: 这个异步更新的概念刚开始确实绕脑子,但这是 React 高效更新界面的秘诀。一旦想通了,你就会发现 React 的很多"奇怪"行为都有道理了。


错误6️⃣: 返回多个元素

有时候组件需要返回好几个并列的元素。

比如这样:

我们希望 LabeledInput 组件能返回两个东西:一个 <label> 和一个 <input>。结果报错了:

Adjacent JSX elements must be wrapped in an enclosing tag.

为啥会这样呢?因为 JSX 最终会编译成普通的 JavaScript。上面的代码编译后长这样:

jsx 复制代码
function LabeledInput({ id, label, ...delegated }) {
  return (
    React.createElement('label', { htmlFor: id }, label)
    React.createElement('input', { id: id, ...delegated })
  );
}

在 JavaScript 里,你不能一下子返回两个东西。就像这样写肯定不行:

jsx 复制代码
function addTwoNumbers(a, b) {
  return (
    "答案是"
    a + b
  );
}

🔧 怎么修复?

以前的老办法是用个 <div> 包起来:

jsx 复制代码
function LabeledInput({ id, label, ...delegated }) {
  return (
    <div>
      <label htmlFor={id}>
        {label}
      </label>
      <input
        id={id}
        {...delegated}
      />
    </div>
  );
}

<label><input> 塞进 <div> 里,就只返回一个顶级元素了!

但现在有更好的办法------用 Fragment:

jsx 复制代码
function LabeledInput({ id, label, ...delegated }) {
  return (
    <React.Fragment>
      <label htmlFor={id}>
        {label}
      </label>
      <input
        id={id}
        {...delegated}
      />
    </React.Fragment>
  );
}

React.Fragment 就是专门为了解决这个问题而生的。它能把多个元素打包,但不会在 DOM 里留下任何痕迹。这样就不会有多余的 <div> 污染页面结构了!

还有个更简洁的写法:

jsx 复制代码
function LabeledInput({ id, label, ...delegated }) {
  return (
    <>
      <label htmlFor={id}>
        {label}
      </label>
      <input
        id={id}
        {...delegated}
      />
    </>
  );
}

我觉得这个设计很巧妙:React 团队用空标签 <> 来表示"这里不会产生真实的 HTML 标签"。

💡 画外音: Fragment 是 React 16 引入的神器。在这之前每个组件都得有个根元素,经常搞得 DOM 结构一层套一层,看着就难受。


错误7️⃣: 从非受控切换到受控

来看个典型的表单,把输入框绑定到 React state:

你开始在输入框里打字的时候,会看到这样的警告:

Warning: A component is changing an uncontrolled input to be controlled.

🔧 怎么修复?

把 email 的初始值设为空字符串就行了:

jsx 复制代码
const [email, setEmail] = React.useState('');

当你设置 value 属性的时候,实际上是在告诉 React "这个输入框我要管"。但前提是你得传个有效值!把 email 初始化为空字符串,就能保证 value 永远不会是 undefined。

受控输入框(Controlled inputs)

如果你想深入了解为什么要这样做,以及什么是"受控输入框",可以看看作者最近发布的教程:

➡️ React 中的数据绑定(Data Binding in React)**
💡 画外音:

受控组件 vs 非受控组件是 React 里的重要概念。受控组件就是"我全听你的",值完全由 React state 说了算;非受控组件就是"我自己做主",靠 DOM 自己的状态。


错误8️⃣: 缺少 style 括号

JSX 看起来跟 HTML 很像,但细节上有些差异经常坑人。

大部分差异都有很好的提示,控制台警告也很友好。比如你写成 class 而不是 className,React 会直接告诉你哪里错了。

但有个微妙的差异特别容易踩坑:style 属性

在 HTML 里,style 是这样写的:

html 复制代码
<button style="color: red; font-size: 1.25rem">
  Hello World
</button>

但在 JSX 里,你得写成对象形式,还要用驼峰命名:

下面这样写肯定报错,你能看出问题在哪吗?

问题就是 需要用双花括号:

jsx 复制代码
<button
  // 是 "{{",不是 "{":
  style={{ color: 'red', fontSize: '1.25rem' }}
>
  Hello World
</button>

为啥要这样呢?咱们来拆解一下这个语法。

在 JSX 里,花括号是用来创建表达式槽(expression slot) 的。你可以在里面放任何有效的 JS 表达式,比如:

html 复制代码
<button className={isPrimary ? 'btn primary' : 'btn'}>

花括号里的内容会被当作 JavaScript 执行,结果会设置给这个属性。

对于 style,我们需要先创建一个表达式槽,然后往里面传一个 JavaScript 对象。

提取成变量可能更好理解:

jsx 复制代码
// 1. 先创建 style 对象:
const btnStyles = { color: 'red', fontSize: '1.25rem' };

// 2. 然后传给 `style` 属性:
<button style={btnStyles}>
  Hello World
</button>

// 当然也可以一步到位:
<button style={{ color: 'red', fontSize: '1.25rem' }}>

外层花括号在 JSX 里创建"表达式槽",内层花括号创建装样式的 JS 对象。

💡 画外音: 这个双花括号语法刚开始确实让人摸不着头脑,但记住"外层是 JSX 语法,内层是 JavaScript 对象"就容易理解了。


错误9️⃣: 异步 effect 函数

假设我们要在组件挂载的时候从 API 拉取用户数据。用 useEffect 钩子,想用 await 关键字。

第一次尝试是这样的:

结果报错了:

'await' is only allowed within async functions

行吧,那给 effect 回调加个 async 关键字:

jsx 复制代码
React.useEffect(async () => {
  const url = `${API}/get-profile?id=${userId}`;
  const res = await fetch(url);
  const json = await res.json();

  setUser(json);
}, [userId]);

结果还是不行,又出现了一个莫名其妙的错误:

destroy is not a function

🔧 正确的做法

在 effect 里面单独创建一个异步函数:

jsx 复制代码
React.useEffect(() => {
  // 先创建个异步函数...
  async function runEffect() {
    const url = `${API}/get-profile?id=${userId}`;
    const res = await fetch(url);
    const json = await res.json();

    setUser(json);
  }

  // ...然后调用它:
  runEffect();
}, [userId]);

为啥要这么绕呢?咱们得理解 async 关键字到底干了啥。

比如说,你觉得这个函数返回什么?

js 复制代码
async function greeting() {
  return "Hello world!";
}

看起来应该返回字符串 "Hello world!" 对吧?但实际上,这个函数返回的是一个 Promise ,这个 Promise 会解析成字符串 "Hello world!"

问题就在这儿!useEffect 钩子不想要你返回 Promise!它想要的是要么什么都不返回,要么返回一个清理函数

清理函数(Cleanup functions)的内容超出了本教程的范围,但它们非常重要。大多数 effect 都会有某种收尾逻辑,我们需要尽快把它提供给 React,这样 React 就能在依赖项变化或组件卸载时调用它。

用我们这种"单独异步函数"的策略,还是可以正常返回清理函数的:

jsx 复制代码
React.useEffect(() => {
  async function runEffect() {
    // Effect 逻辑在这里
  }
  runEffect();

  return () => {
    // 清理逻辑在这里
  }
}, [userId]);

这个函数你想叫啥都行,我喜欢叫 runEffect,一看就知道是主要的 effect 逻辑。

💡 画外音: useEffect 的设计确实有点绕,但掌握了这些套路之后,异步操作就很好理解了。记住,React Hooks 的设计理念就是让副作用管理更加清晰可控。


🎯 培养直觉

看完这些修复方法,你可能会想:为啥非得提供唯一的 key?为啥不能在改 state 后立马访问?为啥 useEffect 这么事儿多?!

React 确实不好掌握,特别是有了 hooks 之后。需要时间慢慢消化,让一切都 点击 到位。

我从 2015 年开始用 React,当时就想:"这玩意儿太酷了,但我完全搞不懂它咋工作的。"😅

这些年来,我一点点搭建自己的 React 心理模型。经历了无数次顿悟,每次都让我的理解更深一层。慢慢地,我开始明白 React 为什么 要这么设计。

最爽的是,我不用再死记硬背那些规则了,完全可以凭直觉判断。这让写 React 变成了一件超级有趣的事情!

过去两年,我一直在打磨一门叫《The Joy of React》的交互式课程。这门课程的出发点很简单:帮你建立对 React 的直觉理解,而不是机械地记忆规则。

不同于传统的视频教学,这门课程融合了多种学习方式------既有视频讲解,也有交互式文章、实战练习、真实项目案例,甚至还穿插了一些小游戏。如果你对系统性学习 React 感兴趣,可以去了解一下。

译者注: 这是原作者的课程推广。虽然是付费内容,但从文章质量来看,课程应该也不错。国内也有很多优秀的 React 学习资源,可以根据自己的情况选择。
💡 画外音 React 的学习曲线确实很陡,但翻过这座山后你会发现一片新天地。记住,每个 React 大牛都踩过这些坑,重要的是踩完坑要爬起来,总结经验,继续前进!

相关推荐
用户9422443570242 小时前
JSAR 空间小程序开发全指南:从环境搭建到跨场景应用落地
javascript
渣哥3 小时前
从构造器注入到 setter:Spring 循环依赖的常见场景解析
javascript·后端·面试
鹏多多3 小时前
前端音频兼容解决:音频神器howler.js从基础到进阶完整使用指南
前端·javascript·音视频开发
前端架构师-老李4 小时前
12、electron专题(electron-builder)
前端·javascript·electron
艾小码4 小时前
这份超全JavaScript函数指南让你从小白变大神
前端·javascript
不学习何以强国4 小时前
Cool Unix + OpenAuth.Net 实现一款校园小程序的开发
mysql·前端框架·asp.net
reembarkation4 小时前
vue 右键菜单的实现
前端·javascript·vue.js
gplitems12310 小时前
Consua WordPress Theme — Business Consulting Sites That Convert With Clarity
javascript
雾削木11 小时前
stm32解锁芯片
javascript·stm32·单片机·嵌入式硬件·gitee