本文正在参加「金石计划」
flag:每月至少产出三篇高质量文章~
曾经(7年前实习的时候,Vue 刚刚火起来的时候)也是个 Vuer!后来 主(bei)动(po) 转的 React,总结 10 个常犯的错误,给转行中的 Vuer ~
1、用 0 做条件渲染
这可能是很多新手都遇到过的问题了吧!鄙人当年也犯过这个错误,但你说它是错误,也可以说是 react
的一个坑:0 是假值,却不能做条件渲染。
看下面这个例子:
可能你想当然他会在 items
为空数组的时候显示 ShoppingList
组件。但实际上却显示了一个 0!这是因为 0 在 JavaScript
中是一个假值,&&
操作符短路了,整个表达式被解析为 0。它就等价于:
function App() { return ( <div> {0} </div> ); }
与其他假值(''、null、false 等)不同,数字 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、突变状态
先来看这个常见的页面:代码
每当增加一个新项目时,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
并且不改变状态,你过去的日志将不会被最近的状态破坏修改,你可以清楚的看到渲染之间的状态变化 - 优化:如果之前的
props
和state
和下一个状态相同,常见的react
优化策略将会跳过本次渲染,如果你从不改变状态,检查变化就会非常的块,如果prevProps === props
,react就可以确定它内部并没有发生变化 - 新功能:react正在构建的新功能依赖将状态视为快照,如果你正在更新过去的状态版本,这会导致无法使用新功能
- 需求变更:一些需要撤销/重做和显示历史记录的值,在没有突变的情况下更容易执行,这是因为你可以将过去的值保存在副本中,并在适用的情况下重做他们
- 更简单的实现:因为
react
不依赖突变,所以它不需要对你的对象做任何处理,不需要劫持你的对象。总是将它们包装到代理中,或者在初始化时像许多“反应式”解决方案那样做其他工作。这也是为什么react
允许您将任何对象置于状态(无论有多大)而没有额外的性能或正确性陷阱。
3、唯一的 key
你肯定经常会在控制台看到一个警告:
Warning: Each child in a list should have a unique "key" prop.
比如:
控制台就会报警告:
每当我们渲染一个元素数组时,我们需要向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;
注意“login in”
前面,是没有空格的:
这是因为 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;
我们希望我们的 LabeledInput
组件能够返回两个元素:一个和一个
。发生这种情况是因为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;
如果你开始键入,你会注意到一个控制台警告:
怎么解决这个问题?我们需要将我们的状态初始化为一个空字符串
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;
然后那就会发现控制台报错了:
正确的做法是:
<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, { 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
关键字吗:
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
我们都知道,useEffect
钩子函数的一个特性是清理功能,即 return
函数。如果你从 useEffect
钩子函数返回任何东西,它必须是一个清理函数,此函数将在组件卸载时运行。相当于类组件中的 componentWillUnmount
生命周期方法。而在 JavaScript 中, async...await
会让程序在等待异步任务完成后才会继续执行。 异步函数也总是返回一个 Promise;如果函数还没有返回,则返回值会自动包装在 Promise
中。
按照上面那种写法,箭头函数直接指向就是返回值,就相当于是返回了一个promise函数了,就不再是一个清理函数了。
正确的做法应该是:
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); } }, []);