はじめに
この記事は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の呼び出し順に関する間違いです。
誤
以下のコードのように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の呼び出し順序は各レンダリングで一貫しており、上記のエラーは発生しなくなります。
その3: Updating object state
3つ目の間違いはobjectの更新方法についてです。
以下のようなinput
タグがあるとします。
誤
そしてコードは以下の通りです。
'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
オブジェクトのname
がhandleChange
関数によって変更されます。
実際にtim
という文字を入力してみましょう。
結果が以下になります。
なにかおかしいことがお分かりいただけましたでしょうか?
user
オブジェクトのname
にはしっかりとtim
という文字列が入っていますが、city
とage
プロパティがuser
オブジェクトからなくなってしまっています。
正
ではどのようにすればcity
とage
プロパティを持ったまま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のようなものになります。
例えば以下のようなフォームがあるとします。
誤
コードは以下の通りです。
'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を制御するコードです。
name
がfirstName
のinputタグに何か文字を入力すれば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 }
と書き直しました。
このように書くことでキーは入力された各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>
)
}
上記にも述べた通り、このコードは正常に動きますし、一見おかしなように見えますん。
正
では先ほどのコードの何がいけなかったのか。
正しいコードを見てみましょう。
'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
が実行される。 -
quantity
が1増える。 - 再レンダリングされる。
-
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...')も表示されないという仕組みです。
では次にstate
がstring
だった場合はどうでしょう?
'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が実行されています。
この違いは何でしょうか?
答えはプリミティブか非プリミティブかの違いです。
プリミティブ値は変更不可能(immutable)なので、前の値と新しい値が同じであるかどうかを簡単に比較できます。
ですが、objectや配列といった非プリミティブは変更可能(mutable)なため、たとえ、プロパティが同じでも別のオブジェクトとして評価されます。
そのため、handleClick
が実行されるとsetPrice
でstate
を更新されるため、再レンダリングが起き、console.logが動くというわけです。
おわりに
以上が駆け出しReactエンジニアがしてしまう6個の間違いになります。
同じ間違いをしてしまっていたり、勉強になったこともあったのではないでしょうか?
ぜひ、上記で学んだことを実務で活かしてもらえると幸いです!
次回は後編の残り6つの間違いについて記事を書こうと思いますのでよろしければそちらも見ていただけると嬉しいです😁
最後にブックマークといいねしていただけるととても励みになりますのでよろしくお願いします!