前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue 选手转 React 常犯的 10 个错误,你犯过几个?

Vue 选手转 React 常犯的 10 个错误,你犯过几个?

作者头像
前端老道
发布2023-09-01 15:59:12
1770
发布2023-09-01 15:59:12
举报
文章被收录于专栏:Vue开发社区Vue开发社区

曾经(7年前实习的时候,Vue 刚刚火起来的时候)也是个 Vuer!后来 主(bei)动(po) 转的 React,总结 10 个常犯的错误,给转行中的 Vuer ~

1、用 0 做条件渲染

这可能是很多新手都遇到过的问题了吧!鄙人当年也犯过这个错误,但你说它是错误,也可以说是 react 的一个坑:0 是假值,却不能做条件渲染。

看下面这个例子:

可能你想当然他会在 items 为空数组的时候显示 ShoppingList 组件。但实际上却显示了一个 0!这是因为 0 在 JavaScript 中是一个假值,&& 操作符短路了,整个表达式被解析为 0。它就等价于:

代码语言:javascript
复制
function App() {
  return (
    <div>
      {0}
    </div>
  );
}

与其他假值(''nullfalse 等)不同,数字 0 在 JSX 中是一个有效值。毕竟,在很多情况下,我们确实想打印数字 0!

正确的做法是:

代码语言:javascript
复制
function App() {
  const [items, setItems] = React.useState([]);
  
  return (
    <div>
      {items.length > 0 && (
        <ShoppingList items={items} />
      )}
    </div>
  );
}

// 或者用三元运算符
function App() {
  const [items, setItems] = React.useState([]);
  
  return (
    <div>
      {items.length
        ? <ShoppingList items={items} />
        : null}
    </div>
  );
}

2、突变状态

先来看这个常见的页面:

代码:

每当增加一个新项目时,handleAddItem 函数就会被调用。但是,它并不起作用!当我们输入一个项目并提交表单时,该项目没有被添加到购物清单中。

问题就在于我们违反了也许是 React 中最核心的原则 —— 不可变状态。React依靠一个状态变量的地址来判断状态是否发生了变化。当我们把一个项目推入一个数组时,我们并没有改变该数组的地址,所以 React 无法判断该值已经改变。

正确的做法是:

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

不建议修改一个现有的数组,而是从头开始创建一个新的数组。这里的区别在于编辑一个现有的数组和创建一个新的数组之间的区别。

同样的,对于对象类型的数据也是:

代码语言:javascript
复制
// ❌ 不建议
function handleChangeEmail(nextEmail) {
  user.email = nextEmail;
  setUser(user);
}

// ✅ 推荐
function handleChangeEmail(email) {
  const nextUser = { ...user, email: nextEmail };
  setUser(nextUser);
}

为什么react不推荐突变状态

  • 调试:如果你使用console.log 并且不改变状态,你过去的日志将不会被最近的状态破坏修改,你可以清楚的看到渲染之间的状态变化
  • 优化:如果之前的propsstate和下一个状态相同,常见的react优化策略将会跳过本次渲染,如果你从不改变状态,检查变化就会非常的块,如果prevProps === props,react就可以确定它内部并没有发生变化
  • 新功能:react正在构建的新功能依赖将状态视为快照,如果你正在更新过去的状态版本,这会导致无法使用新功能
  • 需求变更:一些需要撤销/重做和显示历史记录的值,在没有突变的情况下更容易执行,这是因为你可以将过去的值保存在副本中,并在适用的情况下重做他们
  • 更简单的实现:因为react不依赖突变,所以它不需要对你的对象做任何处理,不需要劫持你的对象。总是将它们包装到代理中,或者在初始化时像许多“反应式”解决方案那样做其他工作。这也是为什么 react 允许您将任何对象置于状态(无论有多大)而没有额外的性能或正确性陷阱。

3、唯一的 key

你肯定经常会在控制台看到一个警告:

代码语言:javascript
复制
Warning: Each child in a list should have a unique "key" prop.

比如:

控制台就会报警告:

每当我们渲染一个元素数组时,我们需要向React提供一些额外的上下文,以便它能够识别每一个项目,通常就是需要一个唯一的标识符。

你可以这么做:

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

但这并不是个推荐的做法。这种方法有时会奏效,但在有些情况下会造成一些相当大的问题。随着你对React的工作原理有了更深的了解,你就能根据具体情况来判断它是否没问题。

有一种绝对安全的方式来解决这个问题:

代码语言:javascript
复制
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。

所以,更好的做法是:

代码语言:javascript
复制
function ShoppingList({ items }) {
  return (
    <ul>
      {items.map((item, index) => {
        return (
          <li key={item.id}>
            {item.label}
          </li>
        );
      })}
    </ul>
  );
}

你也最好不要耍小聪明,图方便,这么做:

代码语言:javascript
复制
// ❌ 不建议这么做
<li key={crypto.randomUUID()}>
  {item.label}
</li>

像这样在 JSX 中生成它将导致 key 在每次渲染时都会改变。每当 key 发生变化时,React 就会销毁并重新创建这些元素,这对性能会产生很大的负面影响。

这种模式,在第一次创建数据时生成 key,可以应用于各种情况。例如,这里是我从服务器获取数据时创建唯一ID的方法:

代码语言:javascript
复制
async function retrieveData() {
  const res = await fetch('/api/data');
  const json = await res.json();
  
  const dataWithId = json.data.map(item => {
    return {
      ...item,
      id: crypto.randomUUID(),
    };
  });
  
  setData(dataWithId);
}

4、空格缺失

你肯定遇到过这种场景:

代码语言:javascript
复制
import React from 'react';

function App() {
  return (
    <p>
      Welcome to the new world!
      <a href="/login">Log in to continue</a>
    </p>
  );
}

export default App;

注意“login in”前面,是没有空格的:

这是因为 JSX编译器(将我们编写的JSX转化为对浏览器友好的JavaScript的工具)无法真正区分语法上的空白和我们为缩进/代码可读性而添加的空白。

正确的做法是:

代码语言:javascript
复制
<p>
  Welcome to the new world!
  {' '}
  <a href="/login">Log in to continue</a>
</p>

tips:如果你使用 Prettier,它会自动为你添加这些空格字符!

5、改变状态后访问状态

这恐怕是react新手最常犯的错了吧:

代码语言:javascript
复制
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;

如果非要取怎么办呢?有办法:

代码语言:javascript
复制
function handleClick() {
  const nextCount = count + 1;
  setCount(nextCount);
  
  console.log({ nextCount });
}

6、返回多个元素

有时,一个组件需要返回多个顶层元素。

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

export default LabeledInput;

我们希望我们的 LabeledInput 组件能够返回两个元素:一个<label>和一个<input>。发生这种情况是因为JSX被编译成普通的JavaScript后是这样子:

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

在JavaScript中,我们不允许像这样返回多个东西。这也是这个方法不可行的原因,就好比:

代码语言:javascript
复制
function addTwoNumbers(a, b) {
  return (
    "the answer is"
    a + b
  );
}

正确的做法是:

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

7、非受控到受控的切换

来看一个比较典型的表单场景,将一个输入与一个React状态绑定:

代码语言:javascript
复制
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;

如果你开始键入,你会注意到一个控制台警告:

怎么解决这个问题?我们需要将我们的状态初始化为一个空字符串:

代码语言:javascript
复制
const [email, setEmail] = React.useState('');

当我们设置了 value 属性时,等于就是告诉 React,我们希望这是一个受控的组件。不过,这只有在我们传递给它一个定义好的值时才会起作用!通过将 email 初始化为一个空字符串,确保该值永远不会被设置为 undefined

8、行内样式缺少括号

JSX 语法直观上与 HTML 很相似,但两者之间还是有一些不一样的地方。例如,如果你使用了 class 而不是 className。还有就是样式,在 HTML 中,style 被写成一个字符串:

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

然而,在 JSX 中,我们需要将其指定为一个对象,并使用 camelCased (驼峰)属性名称。比如,你通常会这么做:

代码语言:javascript
复制
import React from 'react';

function App() {
  return (
    <button
      style={ color: 'red', fontSize: '1.25rem' }
    >
      Hello World
    </button>
  );
}

export default App;

然后那就会发现控制台报错了:

正确的做法是:

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

为什么要这样做?在 JSX 中,我们可以把任何有效的JS表达式放在这个标签里。比如说:

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

无论我们在 {} 里面放了什么,都会被认为是 JavaScript,结果将被设置为这个属性。className 要么是 'btn primary',要么是 'btn'

如果我们把它分得更细一点,对象拉出来放到一个变量中会更清楚:

代码语言:javascript
复制
// 1. 创建一个样式属性对象
const btnStyles = { color: 'red', fontSize: '1.25rem' };

// 2. 把样式对象放到标签属性中
<button style={btnStyles}>
  Hello World
</button>

// 或者,一步到位
<button style={{ color: 'red', fontSize: '1.25rem' }}>

9、useEffect 中的异步方法

假设我们在 useEffect 中请求 API,从中获取一些服务端数据,通常需要将请求方法写成异步的,比如这样:

代码语言:javascript
复制
import React, { useEffect } from 'react';
import { API } from './constants';

function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null);
  
  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;

然后你就会发现控制台这样了:

你肯定想,不就缺少个 async 关键字吗:

代码语言:javascript
复制
useEffect(async () => {
  const url = `${API}/get-profile?id=${userId}`;
  const res = await fetch(url);
  const json = await res.json();
  setUser(json);
}, [userId]);

不幸的是,这仍然不起作用;你将会得到一个新的错误信息:

代码语言:javascript
复制
destroy is not a function

我们都知道,useEffect 钩子函数的一个特性是清理功能,即 return 函数。如果你从 useEffect 钩子函数返回任何东西,它必须是一个清理函数,此函数将在组件卸载时运行。相当于类组件中的 componentWillUnmount 生命周期方法。而在 JavaScript 中, async...await 会让程序在等待异步任务完成后才会继续执行。异步函数也总是返回一个 Promise;如果函数还没有返回,则返回值会自动包装在 Promise 中。

按照上面那种写法,箭头函数直接指向就是返回值,就相当于是返回了一个promise函数了,就不再是一个清理函数了。

正确的做法应该是:

代码语言:javascript
复制
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]);

10、未及时取消事件绑定

当使用 useEffect() 来管理副作用时,一定要记得自己手动清理一下。如果不这样做,会导致内存泄漏和其他问题。

代码语言:javascript
复制
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);

正确的做法是:

代码语言:javascript
复制
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => {
      window.removeEventListener('resize', handleResize);
  }
}, []);

参考

  • https://www.joshwcomeau.com/react/common-beginner-mistakes/

作者: _你当像鸟飞往你的山... https://juejin.cn/post/7220043684293083195

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-08-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端开发社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、用 0 做条件渲染
  • 2、突变状态
  • 3、唯一的 key
  • 4、空格缺失
  • 5、改变状态后访问状态
  • 6、返回多个元素
  • 7、非受控到受控的切换
  • 8、行内样式缺少括号
  • 9、useEffect 中的异步方法
  • 10、未及时取消事件绑定
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档