5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【必読】駆け出しReactエンジニアがしてしまう12個の間違い 〜前編〜

Posted at

はじめに

この記事はYouTubeのAll 12 useState & useEffect Mistakes Junior React Developers Still Make in 2023を翻訳したものになります。

当方、実務でReactとNext.jsを使って4ヶ月ほど開発をしているのですが、この動画を見て「これ間違った書き方なんだ!」と勉強になることが多かったのでQiitaに記事を書くことにしました。

英語で取っ掛かりづらいかもしれませんが、この記事で不明な点があったりもっと詳しく知りたいという方はぜひ解説動画の方も見ていただけると良いかと思います。

それでは早速よくある間違いについて見ていきましょう!

その1: State updates aren't immediate

以下のコードでbuttonを一度クリックした場合、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回呼び出され、1クリックごとに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>
    </>
  )
}

しかし、結果は1クリックごとに1ずつしか増えません。

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

ではどうすれば同期的にcountの値を変更し、1クリックごとに値を4増やすことができるのでしょうか。
それは更新関数(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は動的に変更され1クリックごとに4つ値を増やすことができます。


その2: Conditional rendering

二つ目のよくある間違いはhooksの呼び出し順に関する間違いです。

以下のコードのようにpropsidを受け取り、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の呼び出し順序は各レンダリングで一貫しており、上記のエラーは発生しなくなります。


その3: Updating object state

3つ目の間違いはobjectの更新方法についてです。
以下のようなinputタグがあるとします。
スクリーンショット 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>
  )
}

userオブジェクトにはnameの他にもcity,ageプロパティを持っているとします。
上記のコードではinputタグに何かしら文字を入力するとuserオブジェクトのnamehandleChange関数によって変更されます。
実際にtimという文字を入力してみましょう。
結果が以下になります。スクリーンショット 2023-09-24 13.38.06.png

なにかおかしいことがお分かりいただけましたでしょうか?
userオブジェクトのnameにはしっかりとtimという文字列が入っていますが、cityageプロパティがuserオブジェクトからなくなってしまっています。


ではどのようにすればcityageプロパティを持ったままnameを更新できるのでしょうか。

以下のコードにようにsetUser({ ...user, name: e.target.value })の引数のobjectの中にスプレッド構文でuserオブジェクトを展開してあげると解決できます。

'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ではオブジェクトの中に同じkeyが重複する場合、最後のkeyが評価されます。
そのためsetUser({ ...user, name: e.target.value })と書くことでnameプロパティを上書きできるということです。


その4: Object state instead of multiple smaller ones

4つ目の間違いは先ほどと同様objectの更新方法についてです。
こちらは間違いというより、きれいに書くtipsのようなものになります。
例えば以下のようなフォームがあるとします。
スクリーンショット 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>
  )
}


よくあるformを制御するコードです。
namefirstNameのinputタグに何か文字を入力すればformオブジェクトは問題なく更新されます。
lastNameemailも同様に問題なく更新されます。
お気づきだと思いますが、このコードの問題点は無駄な冗長性です。
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 }と書き直しました。
このように書くことでキーは入力された各inputタグのname属性が入り、DRYに書くことができます。


その5: Information can be deriverd from state / props

5つ目も挙動がおかしいからというより、無駄にhookを使わなくてもいいよというtipsです。

以下のコードは画像のようにAdd 1 itemボタンをクリックすると、quantityが1増え、quantityが変化したことを検知し、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が実行される。
  2. quantityが1増える。
  3. 再レンダリングされる。
  4. totalPriceの値が再評価され、Add 1 itemボタンがクリックされるたびに5ずつ増える。

という仕組みになっています。

useEffectを使わなくても例のような感じでかけるのはスッキリして良いですね。


その6: Primitives vs non-primitives

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

上記のコードで一度、buttonをクリックしてみると、consoleにComponent rendering...と表示されるかと思われますが、実は表示されません。

setPriceに同じ値(この場合は0)を渡すと、Reactは最適化のためにコンポーネントの再レンダリングをスキップします。priceの値が変更されていないため、コンポーネントを更新する必要がないと判断されるのです。そのため、コンポーネントの本体が再度実行されず、console.log('Component rendering...')も表示されないという仕組みです。

では次にstatestringだった場合はどうでしょう?

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

こちらもbuttonをクリックしてもstateが変わらず、再レンダリングされないため、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>
}

こちらの場合、buttonをクリックした回数分だけconsole.logが実行されています。
スクリーンショット 2023-09-24 22.05.33.png

この違いは何でしょうか?
答えはプリミティブか非プリミティブかの違いです。

プリミティブ値は変更不可能(immutable)なので、前の値と新しい値が同じであるかどうかを簡単に比較できます。

ですが、objectや配列といった非プリミティブは変更可能(mutable)なため、たとえ、プロパティが同じでも別のオブジェクトとして評価されます。

そのため、handleClickが実行されるとsetPricestateを更新されるため、再レンダリングが起き、console.logが動くというわけです。

おわりに

以上が駆け出しReactエンジニアがしてしまう6個の間違いになります。
同じ間違いをしてしまっていたり、勉強になったこともあったのではないでしょうか?

ぜひ、上記で学んだことを実務で活かしてもらえると幸いです!

次回は後編の残り6つの間違いについて記事を書こうと思いますのでよろしければそちらも見ていただけると嬉しいです😁

最後にブックマークといいねしていただけるととても励みになりますのでよろしくお願いします!

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?