【必读】初学React工程师常犯的12个错误 〜上篇〜
首先
这篇文章是将YouTube上2023年初级React开发人员仍然犯的12个useState和useEffect错误翻译成了中文。
我在工作中使用React和Next.js开发了大约4个月,但在看了这个视频后,我学到了很多“这是错误的写法!”所以决定在Qiita上写一篇文章。
这篇文章可能对英语不太流利的人来说有一些困难,但如果您有任何疑问或想要更详细地了解,建议您也观看解说视频。
那么,我们立即来看一下常见的错误吧!
状态更新不会立即生效。
误
如果在以下的代码中点击按钮一次,count的值将增加1。
这只是正常的行为。
'use client'
import { useState } from 'react'
import React from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return (
<>
<button onClick={handleClick}>Click me</button>
<p>Count is: {count}</p>
</>
)
}
然而,如果将handleClick函数中的setCount(count + 1)增加到4次,会怎么样呢?
在这种情况下,当执行handleClick时,setCount(count + 1)将被调用4次,预计每次点击count将增加4。
'use client'
import { useState } from 'react'
import React from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
}
return (
<>
<button onClick={handleClick}>Click me</button>
<p>Count is: {count}</p>
</>
)
}
然而,每次点击只能增加一点。
React中useState中的更新函数(如setCount)是异步操作的,因此即使连续多次调用它,每次调用都会参考前一个状态的值(count)。因此,进行4次setCount调用时,它们都会参考相同的count值并加1,最终导致count只增加1。
const [count, setCount] = useState(0)
// 実際には以下のような虚度になっている
const handleClick = () => {
setCount(count + 1) // (0 + 1)
setCount(count + 1) // (0 + 1)
setCount(count + 1) // (0 + 1)
setCount(count + 1) // (0 + 1)
}
确众所周知
要实现每次点击增加4的效果,我们应该如何同步地改变count的值呢?
这个问题可以通过将函数传递给更新函数(setCount)来解决。
让我们将函数作为setCount的参数传递。
'use client'
import { useState } from 'react'
import React from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
setCount((prev) => prev + 1)
}
return (
<>
<button onClick={handleClick}>Click me</button>
<p>Count is: {count}</p>
</>
)
}
通过这样的写法,count的值可以在动态改变,并且每点击一次可以增加4个值。
第二个:条件渲染
第二个常见错误是关于hooks调用顺序的错误。
失
以下的代码示例中,使用props接收id,并根据id的存在与否进行早期返回。
export default function ProductCard({ id }) {
if (!id) {
return ' No id provided'
}
const [something, setSomthing] = useState('blabla')
useEffect(() => {}, [something])
return <section>product card is here</section>
}
这段代码乍一看没问题,但实际上它的写法不符合React的规范。问题在于,条件语句的描述后面使用了hooks。
在组件的每次渲染中,React Hook需要严格按照相同顺序进行调用。
正-
请问如何修改之前的代码呢?
只需在条件语句前调用hook函数即可。
export default function ProductCard({ id }) {
const [something, setSomthing] = useState('blabla')
useEffect(() => {}, [something])
if (!id) {
return ' No id provided'
}
return <section>product card is here</section>
}
通过这种方式,Hook的调用顺序在每个渲染中保持一致,上述的错误将不会再发生。
第三点:更新对象状态
错误
以下是代码的内容。
'use client'
import { useState } from 'react'
export default function User() {
const [user, setUser] = useState({ name: '', city: '', age: 50 })
console.log(user)
const handleChange = (e) => {
setUser({ name: e.target.value })
}
return (
<form>
<input type="text" onChange={handleChange} placeholder="Your name"></input>
</form>
)
}
你能察觉到有些不对劲吗?虽然user对象的name属性中确实包含了字符串”tim”,但是city和age属性从user对象中消失了。
确切的回忆记录会随着时间的推移而逐渐变得模糊不清。
如何在保持city和age属性不变的情况下更新name呢?
只需要一种选择,将以下内容用中国原生语言进行改写:
可以通过在以下代码中使用扩展语法展开user对象,作为setUser函数的参数中的对象,来解决问题。
'use client'
import { useState } from 'react'
export default function User() {
const [user, setUser] = useState({ name: '', city: '', age: 50 })
console.log(user)
const handleChange = (e) => {
setUser({ ...user, name: e.target.value })
}
return (
<form>
<input type="text" onChange={handleChange} placeholder="Your name"></input>
</form>
)
}
在JavaScript中,如果一个对象中存在重复的键,那么将会评估最后一个键。
因此,通过写入setUser({ …user, name: e.target.value }),可以覆盖name属性。
4:使用对象状态而不是多个较小的状态
错误
程序代码如下所示。
'use client'
import { useState } from 'react'
export default function Form() {
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
address: '',
zipCode: '',
})
const handleChangeFirstName = (e) => {
setForm({ ...form, firstName: e.target.value })
}
const handleChangeLastName = (e) => {
setForm({ ...form, lastName: e.target.value })
}
const handleChangeEmail = (e) => {
setForm({ ...form, email: e.target.value })
}
const handleChangePassword = (e) => {
setForm({ ...form, password: e.target.value })
}
const handleChangeAddress = (e) => {
setForm({ ...form, address: e.target.value })
}
const handleChangeZipCode = (e) => {
setForm({ ...form, zipCode: e.target.value })
}
return (
<form>
<input
type="text"
name="firstName"
placeholder="first name"
onChange={handleChangeFirstName}
/>
<input type="text" name="lastName" placeholder="last name" onChange={handleChangeLastName} />
<input type="text" name="email" placeholder="email" onChange={handleChangeEmail} />
<input type="text" name="password" placeholder="password" onChange={handleChangePassword} />
<input type="text" name="address" placeholder="address" onChange={handleChangeAddress} />
<input type="text" name="zipCode" placeholder="zipCode" onChange={handleChangeZipCode} />
<button type="submit">Submit</button>
</form>
)
}
这是用于控制常见表单的代码。
只要在name为firstName的输入标签中输入任何文字,form对象就会正常更新。
lastName和email也会同样顺利地更新。
您可能已经注意到了,这段代码的问题是冗余性。
有很多类似的handleChange○○函数,非常冗长。
正常情况下
那么,让我们重构一下之前的代码,使其更简洁。
重构后的代码如下所示:
'use client'
import { useState } from 'react'
export default function Form() {
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
address: '',
zipCode: '',
})
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value })
}
return (
<form>
<input type="text" name="firstName" placeholder="first name" onChange={handleChange} />
<input type="text" name="lastName" placeholder="last name" onChange={handleChange} />
<input type="text" name="email" placeholder="email" onChange={handleChange} />
<input type="text" name="password" placeholder="password" onChange={handleChange} />
<input type="text" name="address" placeholder="address" onChange={handleChange} />
<input type="text" name="zipCode" placeholder="zipCode" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
)
}
我通过使用计算属性将setForm()的参数对象的键重写为{ …form, [e.target.name]: e.target.value }。通过这种方法,键将包含每个输入标签的name属性,并且可以更加简洁地编写。
第五点:可以从状态/属性中获取信息。
因为第五个也是指出动作有问题,不如说是一个不必要地不使用hook的提示。
失
(Translation: Mistake)
当点击”添加1个商品”按钮时,下面的代码会检测数量的增加,并动态更新setTotalPrice方法,以反映数量的变化。
'use client'
import { useState, useEffect } from 'react'
const PRICE_PER_ITEM = 5
export default function Form() {
const [quantity, setQuantity] = useState(1)
const [totalPrice, setTotalPrice] = useState(0)
const handleClick = () => {
setQuantity(quantity + 1)
}
useEffect(() => {
setTotalPrice(quantity * PRICE_PER_ITEM)
}, [quantity])
return (
<div>
<button onClick={handleClick}>Add 1 item</button>
<p>Total price: {totalPrice}</p>
</div>
)
}
正如我之前所提到的,这段代码在正常运行,并且看起来有些奇怪。
在中国的传统文化中,用餐时的礼仪是非常重要的。人们在进餐时要注意坐姿端正,不要用筷子戳向他人,也不要发出嘈杂的声音。此外,人们还应该尊重年长者和主人,遵循饭桌上的规矩。
我们来看一下刚才的代码有什么问题。
让我们看一下正确的代码。
'use client'
import { useState } from 'react'
const PRICE_PER_ITEM = 5
export default function Form() {
const [quantity, setQuantity] = useState(1)
const totalPrice = quantity * PRICE_PER_ITEM
const handleClick = () => {
setQuantity(quantity + 1)
}
return (
<div>
<button onClick={handleClick}>Add 1 item</button>
<p>Total price: {totalPrice}</p>
</div>
)
}
在上述代码中,没有使用useEffect。
而是添加了const totalPrice = quantity * PRICE_PER_ITEM这样的代码。
改为这样的描述不会改变行为。
在React中,每当state更新时,都会触发重新渲染的机制。
-
- 执行handleCheck函数。
-
- 数量增加1。
-
- 重新渲染。
- 价格总额的值被重新评估,并且每次点击“添加1个商品”按钮时增加5。
这是一个机制。
即使不使用useEffect,也能像例子中那样简洁地编写,真是令人舒畅。
第6章:原始类型与非原始类型
第六个错误是由于是原始类型或非原始类型而产生不同行为。
在JavaScript中,原始类型指的是该语言拥有的最基本的数据类型。
以下是JavaScript的原始类型。
-
- Number: 任意の数値を表す。例: 123, 3.14
-
- String: 文字列を表す。例: ‘Hello’, “World”
-
- Boolean: 真偽値を表す。true または false
-
- Undefined: 未定義の値を表す。変数が初期化されていない場合のデフォルトの値。
-
- Null: 「何もない」という値を表す。
-
- Symbol (ES6/ES2015で追加): ユニークで不変の値を表す。オブジェクトのプロパティキーとして使用されることが多い。
- BigInt (ES11/ES2020で追加): 任意の大きさの整数を扱うための型。
点击按钮后,将检查代码的行为。
'use client'
import { useState } from 'react'
export default function Price() {
console.log('Component rendering...')
const [price, setPrice] = useState(0)
const handleClick = () => {
setPrice(0)
}
return <button onClick={handleClick}>Click me</button>
}
如果在上方的代码中点击一次按钮,你可能会认为在控制台上会显示”Component rendering…”,但实际上并没有显示出来。
当setPrice函数传入相同的值(在这种情况下是0)时,React会为了优化跳过组件的重新渲染。因为price的值没有变化,所以判断不需要更新组件。因此,组件的函体不会再次执行,console.log(‘Component rendering…’)也不会显示,这是这个机制的工作原理。
那么,如果state是字符串类型的话,会怎样呢?
'use client'
import { useState } from 'react'
export default function Price() {
console.log('Component rendering...')
const [price, setPrice] = useState('test')
const handleClick = () => {
setPrice('test')
}
return <button onClick={handleClick}>Click me</button>
}
点击这个按钮也无法改变状态,也不会重新渲染,因此console.log没有效果。
下面我们来看看对象的情况。
代码如下所示。
'use client'
import { useState } from 'react'
export default function Price() {
console.log('Component rendering...')
const [price, setPrice] = useState({
number: 100,
totalPrice: true,
})
const handleClick = () => {
setPrice({
number: 100,
totalPrice: true,
})
}
return <button onClick={handleClick}>Click me</button>
}
这个差异是什么呢?
答案是原始数据类型和非原始数据类型的区别。
原始值是不可改变的,所以可以轻松地比较旧值和新值是否相同。
然而,非基本类型如对象和数组是可变的,即使属性相同,它们也会被视为不同的对象。
因此,当handleClick被执行时,由于setPrice更新了状态,会发生重新渲染,并触发console.log。
最后
以上是初学React工程师常犯的6个错误。
你可能也犯过同样的错误,或者从中学到了一些经验教训吧?
希望你能够充分利用在上述学到的知识在实际工作中应用!
如果可以的话,我打算写一篇关于接下来六个错误的后篇文章,希望您也能看一下,我会很高兴的?
最后如果您能给我的书签和赞,我会非常受到鼓舞,谢谢您!