ReactのuseEffectと非同期処理(async/await)の注意点を初心者向けに解説
生徒
「先生、ReactのuseEffectで非同期処理を使いたいんですが、普通にasync/awaitを書いても大丈夫ですか?」
先生
「実は直接useEffectにasyncをつけるのはおすすめできません。理由を順番に説明します。」
生徒
「どうしてですか?」
先生
「useEffectの関数はクリーンアップ機能も持っていて、Promiseを返すとReactが正しく扱えない場合があるからです。」
生徒
「じゃあ、どうやって非同期処理を書けばいいんですか?」
先生
「useEffectの中でasync関数を定義して呼び出すのが安全です。」
1. useEffectで非同期処理を安全に使う方法
ReactのuseEffectで非同期処理を行う場合、直接asyncをつけるのではなく、useEffectの中にasync関数を作って呼び出すようにします。useEffectは本来「必要ならクリーンアップ関数を返す」仕組みを持っているため、Promiseをそのまま返してしまう形は相性がよくありません。
少し言い換えると、useEffectは「ここで副作用を始めてね」という場所で、非同期処理は「あとで結果が返ってくる処理」です。だから、useEffectの中で非同期処理を担当する関数を分けておくと、流れが整理されて読みやすくなります。
import React, { useState, useEffect } from "react";
function App() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const result = await response.json();
setData(result);
};
fetchData();
}, []);
return (
<div>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default App;
まずはこの形を覚えておくと、useEffectとasync/awaitを組み合わせるときに迷いにくくなります。「useEffectの中でasync関数を作る → その関数を呼ぶ」という手順が、基本の型だと思って大丈夫です。
2. クリーンアップ処理と非同期処理の注意点
useEffectには「後片付け(クリーンアップ)」のための関数を返せる仕組みがあります。非同期処理は結果が返ってくるまで時間がかかることがあるので、その間に画面(コンポーネント)が消えてしまうと、返ってきた結果を使ってsetStateしようとしてエラーや警告の原因になります。
特にありがちなのが、ページ移動や表示の切り替えでコンポーネントがアンマウントされたあとに、API通信が完了してしまうケースです。そこで「まだ表示中なら更新してOK、もう消えていたら更新しない」という判定を入れておくと安心です。
一番シンプルな方法は、フラグ(ここではisMounted)を用意して、クリーンアップでfalseに切り替えるやり方です。
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const result = await response.json();
if (isMounted) {
setData(result);
}
};
fetchData();
return () => {
isMounted = false;
};
}, []);
この考え方を覚えておくと、useEffectと非同期処理を組み合わせたときに起きやすい「表示が変わったあとに更新が走る」トラブルを減らせます。
3. async/awaitを使うときのポイント
useEffectとasync/awaitを一緒に使うときは、いくつか意識しておきたい基本ルールがあります。ここを押さえておくだけで、「なぜか動かない」「警告が出る」といったトラブルをかなり減らせます。
- useEffectに直接asyncをつけない
useEffectはクリーンアップ関数を返す前提の仕組みなので、asyncを直接つけるとPromiseが返ってしまい、意図しない挙動につながります。 - 非同期処理はuseEffectの中で関数として定義する
async関数を中で作って呼び出すことで、処理の流れが整理され、あとから読み返したときも理解しやすくなります。 - 処理が長引く可能性を意識する
通信や待ち時間がある処理は、画面が切り替わる可能性を前提に考え、クリーンアップとセットで設計するのが安全です。 - エラーは想定内として扱う
非同期処理は失敗する前提で考え、try/catchを使ってアプリ全体が止まらないようにします。
初心者のうちは「useEffectは流れを管理する場所」「async/awaitは時間のかかる処理」と役割を分けて考えると混乱しにくくなります。まずは安全な書き方を習慣にすることが大切です。
4. エラーハンドリングの例
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const result = await response.json();
setData(result);
} catch (error) {
console.error("データ取得に失敗しました", error);
}
};
fetchData();
}, []);
5. ポイント整理
- useEffectと非同期処理を組み合わせるときは直接asyncを使わず、内部で定義した関数を呼び出す
- コンポーネントがアンマウントされる場合に備えてクリーンアップフラグを用意する
- try/catchでエラーを安全に処理する
- 非同期処理でsetStateを行うときは常に前回の状態やマウント状態を意識する
6. 依存配列と非同期処理を組み合わせるときの考え方
useEffectで非同期処理を使うときは、依存配列の指定にも注意が必要です。依存配列に値を入れるということは、「その値が変わったら非同期処理をやり直す」という意味になります。
例えば、検索キーワードが変わるたびにAPI通信をしたい場合、そのキーワードを依存配列に含めます。逆に、初回表示時に一度だけデータを取得したいなら、空配列を指定します。
useEffect(() => {
const fetchData = async () => {
const response = await fetch(url);
const result = await response.json();
setData(result);
};
fetchData();
}, [url]);
「いつ再取得したいのか」を先に考えてから依存配列を書くと、無駄な通信や意図しない再実行を防ぎやすくなります。
7. 非同期処理中のローディング状態を管理する
非同期処理では、データが取得できるまでの「待ち時間」が発生します。その間、何も表示されないとユーザーは不安になります。そこで、ローディング状態をstateで管理する方法がよく使われます。
useEffect(() => {
const fetchData = async () => {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
setData(result);
setLoading(false);
};
fetchData();
}, []);
このように状態を分けて管理することで、非同期処理の流れが分かりやすくなり、画面の挙動も安定します。
8. 非同期処理でよくある勘違い
初心者がよく勘違いしやすいのが、「useEffectの中は上から順番に必ず同期的に動く」という考え方です。実際には、awaitの位置で処理は一時停止し、その後に結果が返ってきます。
そのため、非同期処理の直後にstateの値を使おうとすると、まだ更新されていないことがあります。こうした場合は、取得した結果を直接使うか、別のuseEffectで処理を分けると安全です。
useEffect(() => {
const fetchData = async () => {
const result = await getData();
setData(result);
};
fetchData();
}, []);
「非同期=すぐ結果が返らない」という前提を意識することが、useEffectとasync/awaitを安全に使うコツです。