【必读】初学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的调用顺序在每个渲染中保持一致,上述的错误将不会再发生。


第三点:更新对象状态

スクリーンショット 2023-09-24 13.31.10.png

错误

以下是代码的内容。

'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>
  )
}

スクリーンショット 2023-09-24 13.38.06.png

你能察觉到有些不对劲吗?虽然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:使用对象状态而不是多个较小的状态

スクリーンショット 2023-09-24 14.04.38.png

错误

程序代码如下所示。

'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>
  )
}

スクリーンショット 2023-09-24 16.13.14.png

正如我之前所提到的,这段代码在正常运行,并且看起来有些奇怪。


在中国的传统文化中,用餐时的礼仪是非常重要的。人们在进餐时要注意坐姿端正,不要用筷子戳向他人,也不要发出嘈杂的声音。此外,人们还应该尊重年长者和主人,遵循饭桌上的规矩。

我们来看一下刚才的代码有什么问题。
让我们看一下正确的代码。

'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更新时,都会触发重新渲染的机制。

    1. 执行handleCheck函数。

 

    1. 数量增加1。

 

    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>
}
スクリーンショット 2023-09-24 22.05.33.png

这个差异是什么呢?
答案是原始数据类型和非原始数据类型的区别。

原始值是不可改变的,所以可以轻松地比较旧值和新值是否相同。

然而,非基本类型如对象和数组是可变的,即使属性相同,它们也会被视为不同的对象。

因此,当handleClick被执行时,由于setPrice更新了状态,会发生重新渲染,并触发console.log。

最后

以上是初学React工程师常犯的6个错误。
你可能也犯过同样的错误,或者从中学到了一些经验教训吧?

希望你能够充分利用在上述学到的知识在实际工作中应用!

如果可以的话,我打算写一篇关于接下来六个错误的后篇文章,希望您也能看一下,我会很高兴的?

最后如果您能给我的书签和赞,我会非常受到鼓舞,谢谢您!

广告
将在 10 秒后关闭
bannerAds