ReactのuseEffectでasyncを直接書けない理由を解説!初心者でも安心の非同期処理ガイド
生徒
「先生、useEffectの中にasyncって直接書いてもいいんですか?」
先生
「実はuseEffectのコールバックには直接asyncは書けないんです。」
生徒
「えっ?どうしてですか?asyncは便利だから使いたいのに…」
先生
「理由を知ればスッキリしますよ。では、初心者向けにわかりやすく説明していきましょう。」
1. useEffectとコールバック関数の基本
useEffectは「副作用」を処理するためのフックです。副作用とは、画面を描画する以外の処理、たとえばサーバー通信やタイマーの設定、イベントリスナーの登録などです。
useEffectは「コールバック関数」と呼ばれる関数を引数に取ります。このコールバック関数は、Reactがレンダリング後に呼び出す仕組みになっています。
2. asyncを直接書けない理由
では、なぜコールバックにasyncをつけてはいけないのでしょうか?理由は、async関数が必ず「Promise(プロミス)」という特別な値を返してしまうからです。
ReactのuseEffectは、コールバック関数から「クリーンアップ関数」か「何も返さない」のどちらかを期待しています。クリーンアップ関数とは、例えば「画面を切り替えるときにリスナーを解除する」処理です。
もしasyncをつけると、Reactは「Promiseが返ってきたぞ!これはクリーンアップなのか?」と混乱してしまいます。その結果、意図しないエラーや警告が出ることになるのです。
イメージしやすいように例えると、Reactは「掃除の方法(クリーンアップ関数)か何もない(undefined)だけ返してね」とお願いしているのに、asyncをつけると「約束の紙(Promise)」を渡してしまうようなものです。Reactはその紙をどう扱っていいか分からず困ってしまいます。
3. 正しい書き方の基本パターン
ではどうすればいいのでしょうか?答えはシンプルで、useEffectの中でasync関数を「定義してから呼び出す」方法を取ります。
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/posts/1");
const result = await response.json();
setData(result);
};
fetchData();
}, []);
return (
<div>
<h1>非同期処理の例</h1>
{data ? <p>{data.title}</p> : <p>読み込み中...</p>}
</div>
);
}
export default App;
このように、useEffectの直下にasyncをつけるのではなく、中で別の関数を定義して実行するのが正解です。
4. エラーハンドリングを加えた書き方
さらに安全にするために、try...catchを使ってエラー処理も書いておきましょう。これでネットワークエラーが起きてもアプリが止まりません。
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
if (!response.ok) {
throw new Error("データ取得に失敗しました");
}
const result = await response.json();
setData(result);
} catch (error) {
console.error(error);
}
};
fetchData();
}, []);
これなら初心者でも安心して非同期処理を扱うことができます。
まとめ
ReactのuseEffectでasyncを直接書けない理由を振り返ると、まず大切なのは「useEffectが期待している戻り値」と「asyncが必ず返すPromise」という性質の違いです。useEffectはレンダリング後に実行される副作用処理を管理するための仕組みであり、Reactはここで渡されたコールバック関数から「クリーンアップ関数」か「何も返さない(undefined)」のどちらかを受け取る前提で動きます。しかし、asyncをつけた瞬間、その関数は必ずPromiseを返す関数へと変わり、Reactの期待とは異なる形になってしまいます。これが“useEffectにasyncを直接書けない理由”というわけです。 この仕組みが理解できると、初心者でも非同期処理とReactのレンダリングの関係が自然と整理され、なぜ正しい書き方が必要なのかがよく分かります。ReactはUIの更新と副作用の実行タイミングを厳密に管理しているため、Promiseが返ってくると「これはクリーンアップなのか?それとも何かの意図なのか?」と判断できなくなり、結果として予期しない挙動や警告が発生します。これを避けるために、useEffect内ではasyncを直接書かず、内部でasync関数を定義して実行するというパターンが採用されています。 正しいパターンを使えば、APIの取得、データの読み込み、外部サービスとの通信などを安全に行うことができます。また、非同期処理は初心者にとってつまずきやすいポイントですが、useEffectの仕組みとasyncの基本的な性質を理解すると、React開発の幅が一気に広がります。特にAPI通信のような実務でも頻繁に登場する処理を正しく記述できると、安定したアプリを作りやすくなり、エラーを避けつつ効率良く開発できるようになります。 useEffectの仕組みを深く理解すると、クリーンアップ関数の役割も見えてきます。イベントリスナーの解除、タイマーのリセット、サーバー接続の終了など、画面の切り替わり時に必要な後片付けの処理をまとめることで、アプリ全体の安定性とパフォーマンスが向上します。asyncを直接書いてしまうとクリーンアップ関数の識別ができなくなり、メモリリークや不要なイベント登録が残るなどの問題が起きる可能性もあるため、React内部のルールを尊重し、安全な書き方を選ぶことが重要です。 また、非同期処理を書いていくうえで欠かせないのがエラーハンドリングです。ネットワークエラーや予期せぬデータ不備が発生したときも、try...catchを使うことでアプリの停止を防ぎ、利用者に正しいメッセージを返すことができます。これによりユーザー体験も向上し、画面が止まってしまうトラブルを避けられます。ReactではUIの表示を担う部分が明確に分かれているため、エラー時の挙動を丁寧に記述しておくことは非常に重要です。 初心者の段階では「なぜasyncが使えないのか?」という疑問はよく生まれますが、その背景にある“Reactが求めている関数の形”と“async関数の性質”を知ることで納得感を持って理解できます。useEffectの本質を理解し、適切な非同期処理の書き方を身につけることは、React学習の大きな一歩となります。こうした基礎を丁寧に積み上げていくことで、実際の開発でも慌てずに処理の流れを組み立てられるようになり、より複雑なアプリケーションに挑戦する準備が整っていきます。
正しいuseEffect×asyncのサンプルコード
復習として、内部でasync関数を定義する正しい書き方をもう一度載せておきます。
useEffect(() => {
const loadUser = async () => {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const data = await res.json();
console.log(data);
} catch (e) {
console.error("取得失敗:", e);
}
};
loadUser();
}, []);
この書き方はReactが期待する仕組みと完全に一致しており、非同期処理を行いながらもクリーンアップ関数の邪魔をしません。Reactでネットワーク通信を扱うときの基本形としてしっかり覚えておくと安心です。また、実務でもほぼ同じ書き方が採用されるため、このパターンを理解しておくことは大きな武器になります。
生徒
「useEffectにasyncを直接書けない理由がやっと分かりました!Promiseが返っちゃうからReactが困るんですね。」
先生
「そうなんです。Reactはクリーンアップ関数かundefinedを期待しているので、Promiseが返ってくると混乱してしまうんです。」
生徒
「中でasync関数を定義するパターンなら安全に使えるし、実際の開発でもよく使われるんですね。」
先生
「その通りです。非同期処理はReactでは欠かせないので、この書き方に慣れておくと成長が早くなりますよ。」
生徒
「今日学んだ内容を使えば、API通信のときも安心してuseEffectを書けそうです!」