Vue 转 React的开发者,常犯的10个错误~

大厂技术  高级前端  Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群

前言


作者:_你当像鸟飞往你的山
链接:https://juejin.cn/post/7220043684293083195
来源:稀土掘金

1、用 0 做条件渲染

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

看下面这个例子:

2e563f946dc85cd5a15b4a0bb4dcb1dc.jpeg
ray-so-export (11).png

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

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

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

正确的做法是:

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、突变状态

先来看这个常见的页面:

48e155582cebde8957e946472e43ebf2.jpeg
image.png

代码:

1c7152d04660ba5ac60b3869672b67d3.jpeg
ray-so-export (12).png

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

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

正确的做法是:

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

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

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

// ❌ 不建议
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

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

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

比如:

4a090b9b59cbf50527917acc50ee2291.jpeg
ray-so-export (13).png

控制台就会报警告:

95ee97ab2d377bd0e3cdfc33e9047537.jpeg
image.png

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

你可以这么做:

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

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

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

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。

所以,更好的做法是:

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

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

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

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

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

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、空格缺失

你肯定遇到过这种场景:

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;
复制代码
676aa70ea1f7dc162995940b19dfe87b.jpeg
image.png

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

137789754bc4181b855cb9368ea391a9.jpeg
image.png

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

正确的做法是:

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

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

5、改变状态后访问状态

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

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;
复制代码

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

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

6、返回多个元素

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

function LabeledInput({ id, label, ...delegated }) {
  return (
    <label htmlFor={id}>
      {label}
    </label>
    <input
      id={id}
      {...delegated}
    />
  );
}

export default LabeledInput;
复制代码
58660c099d6067f1b7ca0dfea3f466b0.jpeg
image.png

我们希望我们的 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
  );
}
复制代码

正确的做法是:

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

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;
复制代码

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

c3dcf9eb22272e8921102863a65e2e24.jpeg
image.png

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

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

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

8、行内样式缺少括号

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

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

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

import React from 'react';

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

export default App;
复制代码

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

7c9785498c3ed3cf8b7b847f8e7b4b73.jpeg
image.png

正确的做法是:

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

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

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

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

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

// 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,从中获取一些服务端数据,通常需要将请求方法写成异步的,比如这样:

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;
复制代码

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

42e96bac7c14f8e1b47eceda6c695906.jpeg
image.png

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

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
复制代码

正确的做法应该是:

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]);
复制代码

10、未及时取消事件绑定

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

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

正确的做法是:

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

结语

我是考拉🐨,一个热心的前端菜鸟程序员。如果你上进,喜欢前端,想学习前端,那咱们可以交朋友,一起摸鱼哈哈,摸鱼群,关注我,拉你进群,有5000多名前端小伙伴在等着一起学习哦 -->

 
 

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

b50a61a35a670607585c4e5d5f404a08.png

“分享、点赞、在看” 支持一波👍
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值