ReactのuseStateとuseEffectでよくあるエラーと解決方法ガイド!初心者でもわかるReactフック
生徒
「ReactでuseStateやuseEffectを使うとエラーが出ることがあるんですけど、どうしたらいいですか?」
先生
「よくあるエラーにはパターンがあります。順番に見て解決方法も紹介します。」
生徒
「具体的にはどんなエラーですか?」
先生
「たとえば、useStateで初期値を設定していないとundefinedのエラーが出たり、useEffectで依存配列を間違えると無限ループになったりします。」
生徒
「無限ループって何ですか?」
先生
「コンポーネントが何度も繰り返しレンダリングされてしまう状態のことです。useEffectの依存配列を正しく設定することで防げます。」
1. useStateでよくあるエラーと解決方法
useStateは状態を管理するフックですが、初期値を設定していなかったり、状態を直接変更しようとするとエラーや予期せぬ挙動が発生します。
- 初期値がない場合のundefinedエラー
解決策:useStateには必ず初期値を設定する - 状態を直接変更してしまう
解決策:setState関数を使って更新する
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0); // 初期値を必ず設定
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増やす</button>
</div>
);
}
export default App;
2. useEffectでよくあるエラーと解決方法
useEffectは副作用を扱うフックですが、依存配列の指定や非同期処理の扱いを間違えるとエラーや無限ループが発生します。
- 依存配列の指定ミスで無限ループ
解決策:必要な値だけを依存配列に入れる - 非同期関数を直接useEffectに渡す
解決策:関数内で非同期関数を定義して呼び出す
import React, { useState, useEffect } from "react";
function App() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch("https://api.example.com/data");
const result = await response.json();
setData(result);
}
fetchData();
}, []); // 空配列で初回のみ実行
return <div>データ: {JSON.stringify(data)}</div>;
}
export default App;
3. よくあるエラー
ReactでuseStateやuseEffectを使うときに注意したいポイントは次の通りです。
- useStateは初期値を必ず設定する
- 状態を直接変更せず、setState関数で更新する
- useEffectの依存配列は正しく設定する
- 非同期処理は関数内で定義して呼び出す
- コンポーネント削除時にはクリーンアップを意識する
これらを意識するだけで、多くのエラーを防ぐことができます。
4. useEffectのクリーンアップが必要なケースと対処方法
useEffectを使用していると、コンポーネントが削除されたときに古い処理が残ってしまい、メモリリークや意図しない挙動につながることがあります。 特にイベントリスナーやタイマー、外部APIとの接続などを使う場合には、クリーンアップを正しく実行する必要があります。
useEffectの返り値として関数を返すことで、コンポーネントのアンマウント時にクリーンアップが行われます。 この仕組みを理解しておくと、複雑な副作用を扱う場合でも安全に処理を管理できます。
import React, { useEffect, useState } from "react";
function TimerComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
return () => clearInterval(timer); // クリーンアップ
}, []);
return <div>カウント: {count}</div>;
}
export default TimerComponent;
クリーンアップ処理を忘れるとタイマーが残り続けたり、メモリの無駄遣いにつながるため、しっかりと意識しておくことが重要です。
5. useStateでオブジェクトや配列を扱うときの注意点
useStateは文字列や数値だけでなく、オブジェクトや配列も管理できます。しかし、これらを更新するときに 直接変更してしまうと再レンダリングが行われなかったり、予期せぬデータ破損が起きることがあります。
オブジェクトや配列を更新するときは、スプレッド構文を使って新しいデータ構造を作成することが大切です。 Reactでは「不変性」を保つことで、変更を正しく検知し、コンポーネントが更新されるようになっています。
import React, { useState } from "react";
function UserList() {
const [users, setUsers] = useState(["太郎", "花子"]);
const addUser = () => {
setUsers([...users, "新しいユーザー"]); // 配列をコピーして追加
};
return (
<div>
<ul>
{users.map((user, index) => (
<li key={index}>{user}</li>
))}
</ul>
<button onClick={addUser}>追加</button>
</div>
);
}
export default UserList;
オブジェクトや配列を扱う場面は多いため、不変性を意識した更新方法を習得しておくと、エラーを大幅に減らすことができます。
6. useEffectの依存配列でよく起こる意図しない再レンダリング
useEffectを使うとき、依存配列に入れた値によっては意図しないタイミングで処理が再実行されることがあります。 特にオブジェクトや関数を依存配列に入れる場合、毎回新しい参照が生成されるため、無限ループの原因になることがあります。
このような場合は、useCallbackやuseMemoを使って関数や計算結果をメモ化したり、依存しない値は配列に含めない工夫が必要です。 適切に依存配列を管理することで、不要な再レンダリングを防ぎ、パフォーマンスの最適化にもつながります。
import React, { useEffect, useState, useCallback } from "react";
function FetchComponent() {
const [query, setQuery] = useState("react");
const fetchData = useCallback(async () => {
const response = await fetch(`https://api.example.com?q=${query}`);
const result = await response.json();
console.log(result);
}, [query]); // queryを依存にする
useEffect(() => {
fetchData();
}, [fetchData]); // メモ化された関数を依存に指定
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="検索ワード"
/>
</div>
);
}
export default FetchComponent;
意図しない再実行や無限ループは依存配列の設定に原因があることが多いため、値の参照の仕組みを理解しながら設計することが大切です。
まとめ
ReactのuseStateやuseEffectを使うときには、見落としやすいポイントが多く、ちょっとした記述の違いが大きなエラーや予期せぬ動作につながることがある。とくに状態管理と副作用処理は、アプリケーションの動きに深く関わるため、初期値の設定や依存配列の扱い、コンポーネントのアンマウント時に行うクリーンアップ処理など、基本的な考え方をしっかり理解しておくことが重要となる。これらの仕組みを適切に扱えるようになることで、Reactアプリケーションが安定し、レンダリングの無限ループやデータの不整合、意図しない更新などのトラブルを防ぐことにつながる。
また、useStateで配列やオブジェクトを扱う場面が増えると、Reactの不変性という概念を意識しなければならない状況も多くなる。不変性を守ることで、Reactが変更を検知し、再レンダリングを適切にトリガーすることができる。スプレッド構文を使った新しいオブジェクトや配列の生成は、その最も一般的な方法であり、実際のプロジェクトでも頻繁に利用されるテクニックである。これらを自然に使いこなせるようになると、複雑な状態管理でも迷わず扱えるようになり、コードの見通しも良くなる。
一方でuseEffectの扱いもまた繊細で、特に依存配列をどのように設定するかがコンポーネントの動作に大きく影響を与える。依存配列の設定を誤ると、無限ループのようにレンダリングが止まらなくなる問題が発生することがあるため、どの値がeffectを再実行するきっかけになるのかを理解しておく必要がある。また、非同期処理をuseEffectに直接書くのではなく、内部で非同期関数を定義して呼び出す方法も、エラーを避けるためには欠かせないポイントである。さらに、fetchなどのAPI通信を行う際には、更新の競合が起きないように注意し、必要に応じてクリーンアップ処理を取り入れることも求められる。
こうしたReact特有のルールや注意点は、使い慣れてくるほど自然に理解できるようになるが、初心者にとっては最初の壁になりやすい。しかし、エラーの原因には一定のパターンがあり、その背景にある仕組みを知ることで、トラブルの多くは解決できる。たとえば「依存配列の指定ミス」「不変性を保たない更新」「クリーンアップの不足」など、気をつけるべき観点がわかっていれば、エラーが発生しても冷静に原因を探り対処できるようになる。
サンプルプログラムで全体を振り返る
import React, { useState, useEffect, useCallback } from "react";
function SummarySample() {
const [text, setText] = useState("");
const [list, setList] = useState([]);
const [count, setCount] = useState(0);
const addItem = () => {
setList([...list, text]);
setText("");
};
const updateCount = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
useEffect(() => {
const timer = setInterval(updateCount, 1000);
return () => clearInterval(timer); // クリーンアップ
}, [updateCount]);
return (
<div className="summary-box">
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="入力してください"
/>
<button onClick={addItem}>追加</button>
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
<div>自動カウント: {count}</div>
</div>
);
}
export default SummarySample;
このサンプルでは、useStateで文字列と配列を管理し、useCallbackで関数を安定させ、useEffectでタイマー処理を動かしている。ひとつひとつのフックの特性を理解することで、複数の処理が絡み合うような場合でも落ち着いて構築できる。また、副作用には必ず終わりがあり、クリーンアップが正しく行われることでコンポーネントの動作はより安全なものとなる。
生徒:「useStateとuseEffectのエラーって複雑に見えましたが、原因がわかると意外と整理できるんですね。」
先生:「そうですね。仕組みがわかると、エラーもヒントのように見えてきますよ。」
生徒:「クリーンアップの必要性もよく理解できました。タイマーやイベントを放置すると大変なんですね。」
先生:「その通りです。コンポーネントのライフサイクルを意識すると、より安全なコードが書けますよ。」
生徒:「依存配列の設定も慣れればスムーズにできそうです。うっかり無限ループにしないよう気を付けます。」
先生:「よく理解できていますね。使っていくうちに自然と身につくので、たくさん書いてみましょう。」