
目次
You Might Not Need An Effectとは
You Might Not Need An Effectとは、Reactの公式ドキュメントで紹介されているuseEffectに関する考え方です。
ReactのuseEffectは副作用を扱うためのフックですが、実際には多くの場面で本来は不要なuseEffectが書かれていることがよくあります。
propsまたはstateに基づいて状態を更新する
firstNameとlastNameという2つの状態変数を持つコンポーネントがあるとします。
これらを連結してfullNameを生成したいとします。
さらに、firstNameまたはlastNameが変更されるたびにfullNameも更新したいとします。この場合、まず思いつくのはfullName状態変数を追加してuseEffect内で更新することです。
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [firstName, setFirstName] = useState('Taro')
const [lastName, setLastName] = useState('Yamada')
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(firstName + ' ' + lastName)
}, [firstName, lastName])
return (
<div>
<h1>Hello, {fullName}!</h1>
<div>
<div>
<label htmlFor="firstName">First Name: </label>
<input
id="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</div>
<div>
<label htmlFor="lastName">Last Name: </label>
<input
id="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
</div>
</div>
</div>
)
}
export default Appこのコードは動作しますが、fullNameの値が古いままレンダリングパス全体を実行し、その後すぐに更新された値で再レンダリングするため、非効率的です。
この場合は「const fullName = firstName + ' ' + lastName」にしたほうが無駄な処理を削減できるので効率的です。
import { useState } from 'react'
import './App.css'
function App() {
const [firstName, setFirstName] = useState('Taro')
const [lastName, setLastName] = useState('Yamada')
const fullName = firstName + ' ' + lastName
return (
<div>
<h1>Hello, {fullName}!</h1>
<div>
<div>
<label htmlFor="firstName">First Name: </label>
<input
id="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
</div>
<div>
<label htmlFor="lastName">Last Name: </label>
<input
id="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
</div>
</div>
</div>
)
}
export default Appこのコードはシンプルな内容なので、Reactの中級者以上であればコードに問題があることはすぐにわかると思います。
でも、初心者だったり、コードが複雑だったりすると前述のような非効率なコードがあっても気づかない可能性があります。
ESLintのプラグインでReactの非効率なコードを自動検出
ReactのYou Might Not Need an Effectに該当する非効率なコードを目視だけですべて判別するのは難しいです。
しかし、以下のYou Might Not Need an Effectに該当するReactの不要なuseEffectsを検出するESLintプラグインをインストールすれば誰でも簡単に問題のある箇所がわかります。
使い方は、まず以下のコマンドでESLintのプラグインをインストールします。
npm i -D eslint-plugin-react-you-might-not-need-an-effect次にeslint.config.jsを以下のようにして、You Might Not Need an EffectのESLintプラグインを有効化します。
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import reactYouMightNotNeedAnEffect from 'eslint-plugin-react-you-might-not-need-an-effect'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
reactYouMightNotNeedAnEffect.configs.recommended,
])これであとは「npm run lint」を実行すればESLintが実行されるので、You Might Not Need an Effectのルールがあれば検出されます。
VS CodeならESLintの拡張機能をインストールして、問題のあるコードをリアルタイムで検出できるようにしておくと良いでしょう。
You Might Not Need an Effect no-derived-state Sample
ESLintのYou Might Not Need an Effectプラグインは以下の10種類のルールに違反していないかチェックします。
- no-derived-state
useEffectに派生状態を保存できないようにする。 - no-chain-state-updates
useEffect内での状態更新の連鎖を禁止する。 - no-event-handler
状態と効果をイベントハンドラーとして使用することを禁止します。 - no-adjust-state-on-prop-change
propが変更されたときにuseEffectの状態を調整することを禁止します。 - no-reset-all-state-on-prop-change
propが変更されたときにuseEffect内のすべての状態をリセットすることを禁止します。 - no-pass-live-state-to-parent
useEffect内でliveのstateを親に渡すことを禁止します。 - no-pass-data-to-parent
useEffect内で親にデータを渡すことを禁止します。 - no-initialize-state
useEffect内での状態の初期化を禁止します。 - no-manage-parent
propsのみを使用するエフェクトを禁止する。 - no-empty-effect
空のuseEffectを許可しない。
1から10のうち1については最初に解説しましたので、2から10についても解説します。
no-chain-state-updates
useEffect内での状態更新の連鎖を禁止します。
例えば、以下のようにuseStateのroundが更新された際に、useEffectでroundを検知して別のuseStateのsetIsGameOverの状態更新が検出された場合は警告を出します。
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [round, setRound] = useState(1)
const [randomNum, setRandomNum] = useState(
Math.floor(Math.random() * 100) + 1
)
const [gameStatus, setGameStatus] = useState<'playing' | 'cleared'>('playing')
const [isGameOver, setIsGameOver] = useState(false)
useEffect(() => {
if (round === 10) {
setIsGameOver(true)
} else {
setIsGameOver(false)
}
}, [round])
const isCleared = gameStatus === 'cleared'
const handleDrawNumber = () => {
const newRandomNum = Math.floor(Math.random() * 100) + 1
setRandomNum(newRandomNum)
if (newRandomNum === 7) {
setGameStatus('cleared')
} else {
setRound(round + 1)
}
}
const handleReset = () => {
setRound(1)
setRandomNum(Math.floor(Math.random() * 100) + 1)
setGameStatus('playing')
}
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>Game Round: {Math.min(round, 10)}</h1>
<h2>{round < 11 && randomNum}</h2>
{isCleared ? (
<div>
<h2>🎉 Clear 🎉</h2>
<p>You got 7! Congratulations!</p>
<button onClick={handleReset}>Play Again</button>
</div>
) : isGameOver ? (
<div>
<h2>💀 Game Over 💀</h2>
<button onClick={handleReset}>Try Again</button>
</div>
) : (
<div>
<p>Get 7 to win!</p>
<button onClick={handleDrawNumber}>Draw Number</button>
</div>
)}
</div>
)
}
export default AppYou Might Not Need an Effect no-chain-state-updates Sample
この場合は以下のようにuseStateを適切に使用して状態を管理すれば、useEffectを使用せずに処理できます。
import { useState } from 'react'
import './App.css'
function App() {
const [round, setRound] = useState(1)
const [randomNum, setRandomNum] = useState(
Math.floor(Math.random() * 100) + 1
)
const [gameStatus, setGameStatus] = useState<
'playing' | 'cleared' | 'gameOver'
>('playing')
const isGameOver = gameStatus === 'gameOver'
const isCleared = gameStatus === 'cleared'
const handleDrawNumber = () => {
const newRandomNum = Math.floor(Math.random() * 100) + 1
setRandomNum(newRandomNum)
if (newRandomNum === 7) {
setGameStatus('cleared')
} else {
setRound(round + 1)
if (round + 1 > 9) {
setGameStatus('gameOver')
}
}
}
const handleReset = () => {
setRound(1)
setRandomNum(Math.floor(Math.random() * 100) + 1)
setGameStatus('playing')
}
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h1>Game Round: {Math.min(round, 10)}</h1>
<h2>{round < 11 && randomNum}</h2>
{isCleared ? (
<div>
<h2>🎉 Clear 🎉</h2>
<p>You got 7! Congratulations!</p>
<button onClick={handleReset}>Play Again</button>
</div>
) : isGameOver ? (
<div>
<h2>💀 Game Over 💀</h2>
<button onClick={handleReset}>Try Again</button>
</div>
) : (
<div>
<p>Get 7 to win!</p>
<button onClick={handleDrawNumber}>Draw Number</button>
</div>
)}
</div>
)
}
export default Appno-event-handler
useStateとuseEffectをイベントハンドラーとして使用することを禁止します。
例えば、以下のようにカートに商品を追加するコードで、useEffectを使用してuseStateのproductの変更を検知して処理するイベントハンドラーとしての使用は警告が表示されます。
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [product, setProduct] = useState({
id: 1,
name: 'Sample Product',
price: 1000,
isInCart: false,
})
const [cart, setCart] = useState<
{ id: number; name: string; price: number; isInCart: boolean }[]
>([])
const addToCart = (product: {
id: number
name: string
price: number
isInCart: boolean
}) => {
setCart((prevCart) => [...prevCart, product])
setProduct((prevProduct) => ({ ...prevProduct, isInCart: true }))
}
const showNotification = (message: string) => {
alert(message)
}
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`)
}
}, [product])
function handleBuyClick() {
addToCart(product)
}
function handleResetClick() {
setCart([])
setProduct((prevProduct) => ({ ...prevProduct, isInCart: false }))
}
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div>
<h2>{product.name}</h2>
<p>Price: ¥{product.price}</p>
<p>Status: {product.isInCart ? 'In Cart' : 'Not in Cart'}</p>
<p>Cart Items: {cart.length}</p>
<div style={{ marginTop: '20px' }}>
<button onClick={handleBuyClick}>Add to Cart</button>
<button onClick={handleResetClick}>Reset Cart</button>
</div>
</div>
</div>
)
}
export default AppYou Might Not Need an Effect no-event-handler Sample
この場合はuseEffectを使用してイベントハンドラーのような処理をする必要はないので、以下のようなコードにすると良いです。
import { useState } from 'react'
import './App.css'
function App() {
const [product, setProduct] = useState({
id: 1,
name: 'Sample Product',
price: 1000,
isInCart: false,
})
const [cart, setCart] = useState<
{ id: number; name: string; price: number; isInCart: boolean }[]
>([])
const addToCart = (product: {
id: number
name: string
price: number
isInCart: boolean
}) => {
setCart((prevCart) => [...prevCart, product])
setProduct((prevProduct) => ({ ...prevProduct, isInCart: true }))
alert(`Added ${product.name} to the shopping cart!`)
}
function handleBuyClick() {
addToCart(product)
}
function handleResetClick() {
setCart([])
setProduct((prevProduct) => ({ ...prevProduct, isInCart: false }))
}
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div>
<h2>{product.name}</h2>
<p>Price: ¥{product.price}</p>
<p>Status: {product.isInCart ? 'In Cart' : 'Not in Cart'}</p>
<p>Cart Items: {cart.length}</p>
<div style={{ marginTop: '20px' }}>
<button onClick={handleBuyClick}>Add to Cart</button>
<button onClick={handleResetClick}>Reset Cart</button>
</div>
</div>
</div>
)
}
export default Appno-adjust-state-on-prop-change
propが変更されたときにuseEffectの状態を調整することを禁止します。
以下のように、prop (messageColor) を受け取って、useEffectで検知してuseStateの変更を行うと警告が表示されます。
import { useState, useEffect } from 'react'
import './App.css'
type Props = { messageColor: string }
function Message({ messageColor }: Props) {
const [isColorChange, setIsColorChange] = useState(false)
useEffect(() => {
if (messageColor) {
setIsColorChange(true)
}
}, [messageColor])
return (
<>
<h1 style={{ color: messageColor }}>Change Color!</h1>
<h2>isColorChange: {isColorChange.toString()}</h2>
</>
)
}
function App() {
const [color, setColor] = useState('')
const colors = ['red', 'green', 'blue']
return (
<div>
<div style={{ marginBottom: '20px' }}>
<h2>Select Color</h2>
{colors.map((colorOption) => (
<button
key={colorOption}
onClick={() => setColor(colorOption)}
style={{
backgroundColor: colorOption,
color: 'white',
}}
>
{colorOption}
</button>
))}
</div>
<Message messageColor={color} />
</div>
)
}
export default AppYou Might Not Need an Effect no-adjust-state-on-prop-change Sample
この場合はuseEffectを使用せずにuseStateでprop (messageColor) が変更されているか検知して、変更されていたらuseStateで必要に応じて更新するほうが良いです。
以下の例ではprevMessageColorに前回のpropをuseState(messageColor)で保存しています。
よく誤解されるのですが、useStateの初期値はコンポーネントの初回マウント時のみ設定されるので、初期値はボタンを押すたびに変わりません。(つまり、初期値は空文字で固定)
import { useState } from 'react'
import './App.css'
type Props = { messageColor: string }
function Message({ messageColor }: Props) {
const [isColorChange, setIsColorChange] = useState(false)
const [prevMessageColor, setPrevMessageColor] = useState(messageColor)
if (messageColor !== prevMessageColor) {
setIsColorChange(true)
setPrevMessageColor(messageColor)
}
return (
<>
<h1 style={{ color: messageColor }}>Change Color!</h1>
<h2>isColorChange: {isColorChange.toString()}</h2>
</>
)
}
function App() {
const [color, setColor] = useState('')
const colors = ['red', 'green', 'blue']
return (
<div>
<div style={{ marginBottom: '20px' }}>
<h2>Select Color</h2>
{colors.map((colorOption) => (
<button
key={colorOption}
onClick={() => setColor(colorOption)}
style={{
backgroundColor: colorOption,
color: 'white',
}}
>
{colorOption}
</button>
))}
</div>
<Message messageColor={color} />
</div>
)
}
export default Appno-reset-all-state-on-prop-change
propが変更されたときにuseEffect内のすべての状態をリセットすることを禁止します。
import { useState, useEffect } from 'react'
import './App.css'
function List({ item }: { item: string }) {
const [selection, setSelection] = useState(null)
useEffect(() => {
setSelection(null)
}, [item])
return <div>{selection}</div>
}
function App() {
return <List item="test" />
}
export default AppYou Might Not Need an Effect no-reset-all-state-on-prop-change Sample
no-pass-live-state-to-parent
useEffect内で親にデータを渡すことを禁止します。
import { useState, useEffect } from 'react'
import './App.css'
function Toggle({ onChange }: { onChange: (isOn: boolean) => void }) {
const [isOn, setIsOn] = useState(false)
useEffect(() => {
onChange(isOn)
}, [isOn, onChange])
function handleClick() {
setIsOn(!isOn)
}
return (
<button
onClick={handleClick}
style={{
backgroundColor: isOn ? '#4CAF50' : '#f44336',
}}
>
{isOn ? 'ON' : 'OFF'}
</button>
)
}
function App() {
const [isOn, setIsOn] = useState(false)
return (
<div>
<Toggle onChange={setIsOn} />
<p>現在は{isOn ? 'ON' : 'OFF'}です。</p>
</div>
)
}
export default AppYou Might Not Need an Effect no-pass-live-state-to-parent Sample
この場合はuseEffectを使用しなくても、直接useStateだけで変更できます。
import { useState } from 'react'
import './App.css'
function Toggle({ onChange }: { onChange: (isOn: boolean) => void }) {
const [isOn, setIsOn] = useState(false)
function handleClick() {
setIsOn(!isOn)
onChange(!isOn)
}
return (
<button
onClick={handleClick}
style={{
backgroundColor: isOn ? '#4CAF50' : '#f44336',
}}
>
{isOn ? 'ON' : 'OFF'}
</button>
)
}
function App() {
const [isOn, setIsOn] = useState(false)
return (
<div>
<Toggle onChange={setIsOn} />
<p>現在は{isOn ? 'ON' : 'OFF'}です。</p>
</div>
)
}
export default Appno-pass-data-to-parent
useEffect内で親にデータを渡すことを禁止します。
import { useState, useEffect } from 'react'
import './App.css'
function Child({
onFetched,
}: {
onFetched: (data: { status: string }) => void
}) {
useEffect(() => {
fetch('https://dummyjson.com/test')
.then((res) => res.json())
.then((fetchedData) => {
onFetched(fetchedData)
})
}, [onFetched])
return null
}
function App() {
const [data, setData] = useState<{ status: string } | null>(null)
return (
<div>
<Child onFetched={setData} />
{data ? <p>Status: {data.status}</p> : <p>Loading...</p>}
</div>
)
}
export default Appfetchなどを使用したAPIの取得などは子ではなく親で処理して子に渡さないほうが良いです。
import { useState, useEffect } from 'react'
import './App.css'
function Child({ data }: { data: { status: string } | null }) {
return data ? <p>Status: {data.status}</p> : <p>Loading...</p>
}
function App() {
const [data, setData] = useState<{ status: string } | null>(null)
useEffect(() => {
fetch('https://dummyjson.com/test')
.then((res) => res.json())
.then(setData)
}, [])
return (
<div>
<Child data={data} />
</div>
)
}
export default Appno-initialize-state
useEffect内での状態の初期化を禁止します。
import { useEffect, useState } from 'react'
import './App.css'
function App() {
const [userName, setUserName] = useState(null)
useEffect(() => {
setUserName('Anonymous')
}, [])
return <h1>{userName}</h1>
}
export default AppYou Might Not Need an Effect no-initialize-state Sample
useStateの初期化はuseState('Anonymous')のようにuseStateで行ってください。
条件分岐によって初期値が異なる場合は、以下のようにuseState自体を使わなくても良いことがあります。
import './App.css'
function App() {
const userName = new Date().getMonth() === 11 ? 'Santa Claus' : 'Anonymous'
return (
<h1>{userName}</h1>
)
}
export default Appno-manage-parent
propsのみを使用するエフェクトを禁止する。
import { useState, useEffect } from 'react'
import './App.css'
function Child({ isOpen, onClose }) {
useEffect(() => {
if (!isOpen) {
onClose()
}
}, [isOpen, onClose])
return (
<div>
{isOpen ? <p>開いています</p> : <p>閉じています</p>}
</div>
)
}
function App() {
const [isOpen, setIsOpen] = useState(true)
const handleClose = () => {
console.log('Childが閉じられました')
}
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? '閉じる' : '開く'}
</button>
<Child isOpen={isOpen} onClose={handleClose} />
</div>
)
}
export default AppYou Might Not Need an Effect no-manage-parent Sample
この場合はuseEffectを使わずに以下のように書けます。
import { useState } from 'react'
import './App.css'
function Child({ isOpen }: { isOpen: boolean }) {
return <div>{isOpen ? <p>開いています</p> : <p>閉じています</p>}</div>
}
function App() {
const [isOpen, setIsOpen] = useState(true)
const handleToggle = () => {
if (isOpen) {
console.log('Childが閉じられました')
}
setIsOpen(!isOpen)
}
return (
<div>
<button onClick={handleToggle}>{isOpen ? '閉じる' : '開く'}</button>
<Child isOpen={isOpen} />
</div>
)
}
export default Appno-empty-effect
空のuseEffectを許可しない。
空のuseEffectなんて書かれるケースは初心者でもないと思いますが、ルールとして存在しています。
useEffect(() => {}, [])まとめ
ESLintのYou Might Not Need an Effectプラグインの10種類のルールについてサンプルコード付きで説明しましたが、要するにuseEffectを不必要に書いて非効率なコードを書かないよう気をつけましょうということです。
10種類もルールがありますが、プラグインとVS Codeの拡張機能を入れておけば、ルール違反があればリアルタイムで警告が表示されるので、自然とルールを覚えることができます。
useEffectは使いすぎるとパフォーマンスが悪くなったり、バグの温床になることがあるので、このプラグインを入れて無駄なuseEffectの使用を避けるようにすれば、必ずパフォーマンスが良く、バグの少ないコードになると思います。


