はじめに (対象読者・この記事でわかること)
この記事は、React Hooksを使用して非同期処理を行う方法について、特に「非同期処理はuseEffect内で行うこと」という一般的な考え方に疑問を持つ開発者を対象にしています。 この記事を読むことで、React Hooksで非同期処理をuseEffect以外の場所で行う方法とその利点・欠点を理解し、より適切なコード設計ができるようになります。また、なぜ一般的にuseEffect内で非同期処理を行うことが推奨されるのか、その背景についても理解を深められます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - JavaScriptの基本的な知識 - Reactの基本的な概念(コンポーネント、props、stateなど) - React Hooksの基本的な使い方(useState, useEffectなど)
React Hooksと非同期処理の関係
React Hooksが導入された当初、非同期処理(API呼び出しなど)は基本的にuseEffectフック内で行われることが推奨されてきました。これは、Reactのレンダリングサイクルと副作用(Side Effects)を明確に分離するための設計思想に基づいています。
useEffectは、コンポーネントのレンダリング後に実行されるため、非同期処理のような外部との通信を行う副作用を安全に実行できます。また、依存配列を指定することで、特定の条件でのみ実行を制御することも可能です。
しかし、この「非同期処理はuseEffect内で行う」というルールは絶対的なものではなく、状況によっては他のフックやパターンを利用した方が適切な場合もあります。この記事では、そのような代替案とそれぞれの特徴について詳しく解説します。
非同期処理の代替パターン
イベントハンドラ内での非同期処理
最もシンプルな代替案は、イベントハンドラ内で直接非同期処理を行う方法です。これは、ユーザーのアクション(ボタンクリックなど)に応じてデータを取得する場合に有効です。
Jsxfunction UserProfile({ userId }) { const [user, setUser] = useState(null); const fetchUser = async () => { const response = await fetch(`/api/users/${userId}`); const userData = await response.json(); setUser(userData); }; return ( <div> <button onClick={fetchUser}>ユーザー情報を取得</button> {user && <div>{user.name}</div>} </div> ); }
この方法の利点は、明確なトリガー(ユーザーのアクション)があるため、非同期処理の実行タイミングが分かりやすい点です。また、useEffectを使わないため、不要な再レンダリングや実行を避けることができます。
ただし、注意点として、イベントハンドラ内で直接非同期処理を行うと、コンポーネントがアンマウントされた後でも処理が続行される可能性があります。これにより、メモリリークや状態更新エラーが発生する可能性があるため、適切なキャンセル処理が必要です。
カスタムフックの活用
より再利用性の高い方法として、カスタムフックを作成する方法があります。非同期処理のロジックをカスタムフックにまとめることで、複数のコンポーネントで同じ処理を簡単に再利用できます。
Jsxfunction useAsyncData(fetchFunction, dependencies = []) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { let isMounted = true; const fetchData = async () => { setLoading(true); try { const result = await fetchFunction(); if (isMounted) { setData(result); } } catch (err) { if (isMounted) { setError(err); } } finally { if (isMounted) { setLoading(false); } } }; fetchData(); return () => { isMounted = false; }; }, dependencies); return { data, loading, error }; }
このカスタムフックを使うことで、コンポーネント側では以下のように簡潔に非同期処理を記述できます。
Jsxfunction UserProfile({ userId }) { const { data: user, loading, error } = useAsyncData( () => fetch(`/api/users/${userId}`).then(res => res.json()), [userId] ); if (loading) return <div>読み込み中...</div>; if (error) return <div>エラーが発生しました</div>; if (!user) return null; return <div>{user.name}</div>; }
この方法の利点は、非同期処理のロジックをカスタムフックにカプセル化できるため、コンポーネントの責務が明確になる点です。また、ローディング状態やエラーハンドリングを共通化できるため、一貫したUI/UXを実現しやすくなります。
React QueryやSWRのようなライブラリの利用
実際のプロジェクトでは、React QueryやSWRのような専用のライブラリを利用するのが一般的です。これらのライブラリは、非同期処理に関する多くの課題(キャッシュ、再取得、エラーハンドリングなど)を解決してくれます。
React Queryの使用例:
Jsximport { useQuery } from 'react-query'; function UserProfile({ userId }) { const { data: user, isLoading, error } = useQuery( ['user', userId], () => fetch(`/api/users/${userId}`).then(res => res.json()), { enabled: !!userId // userIdが存在する場合のみ実行 } ); if (isLoading) return <div>読み込み中...</div>; if (error) return <div>エラーが発生しました</div>; if (!user) return null; return <div>{user.name}</div>; }
これらのライブラリの利点は、以下の通りです。
- 自動キャッシュ: データをキャッシュし、必要に応じて再取得します
- バックグラウンド再取得: ユーザーが操作している間にもデータを最新の状態に保ちます
- ウィンドウフォーカス時の再取得: ブラウザタブがフォーカスされた際にデータを更新します
- エラーハンドリング: エラー状態を管理し、リトライ機能を提供します
- ページネーションやインフィニットスクロール: データの取得状態を簡単に管理できます
コンポーネント外での非同期処理
場合によっては、コンポーネントの外で非同期処理を行い、結果をpropsとして渡す方法も有効です。これは、非同期処理の結果がコンポーネントのレンダリングに直接依存しない場合に適しています。
Jsx// コンポーネント外でデータを取得 async function fetchUserData(userId) { const response = await fetch(`/api/users/${userId}`); return response.json(); } function UserProfile({ user }) { return ( <div> {user ? <div>{user.name}</div> : <div>ユーザーが見つかりません</div>} </div> ); } // 親コンポーネントでデータを取得して渡す function App() { const [user, setUser] = useState(null); useEffect(() => { fetchUserData(1).then(setUser); }, []); return <UserProfile user={user} />; }
この方法の利点は、非同期処理のロジックをコンポーネントの外に追い出せるため、コンポーネントが純粋なプレゼンテーションコンポーネントとして保たれる点です。また、テストが容易になるというメリットもあります。
SuspenseとReact.lazyとの組み合わせ
React 18では、SuspenseとReact.lazyを組み合わせて、非同期処理をより宣言的に扱うことができます。特に、コード分割と組み合わせて利用する場合に有効です。
Jsximport { Suspense, lazy } from 'react'; const UserProfile = lazy(() => import('./UserProfile')); function App() { return ( <Suspense fallback={<div>読み込み中...</div>}> <UserProfile userId={1} /> </Suspense> ); }
この方法の利点は、非同期処理のロジックをReactのレンダリングサイクルに組み込めるため、より宣言的なコードが書ける点です。また、ローディング状態を共通化できるため、一貫したUXを実現しやすくなります。
ハマった点やエラー解決
問題1:非同期処理のキャンセル
コンポーネントがアンマウントされた後でも非同期処理が続行され、不要な状態更新が行われる問題があります。
Jsxfunction UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetchUser(userId, signal).then(setUser); return () => { controller.abort(); // コンポーネントがアンマウントされたら処理をキャンセル }; }, [userId]); return <div>{user?.name}</div>; }
解決策として、AbortControllerを使用してリクエストをキャンセルする方法があります。これにより、コンポーネントがアンマウントされた後でも不要な状態更新を防ぎます。
問題2:古い状態に基づいた非同期処理
状態更新が非同期であるため、古い状態に基づいて非同期処理が実行されてしまう問題があります。
Jsxfunction Counter() { const [count, setCount] = useState(0); const increment = () => { setCount(count + 1); // ここでcountは古い値のまま fetchSomeData(count + 1); // 実際にはcount + 2のリクエストが送られる可能性がある }; return <button onClick={increment}>{count}</button>; }
解決策として、関数形式の状態更新を使用する方法があります。これにより、最新の状態に基づいて処理を実行できます。
Jsxfunction Counter() { const [count, setCount] = useState(0); const increment = () => { setCount(prevCount => { const newCount = prevCount + 1; fetchSomeData(newCount); return newCount; }); }; return <button onClick={increment}>{count}</button>; }
問題3:依配列の適切な設定
useEffectの依存配列に必要な値を漏らしてしまうと、意図しないタイミングで非同期処理が実行されてしまいます。
Jsxfunction UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetchUser(userId).then(setUser); // 依存配列にuserIdが含まれていないため、userIdが変更されても再実行されない }, []); // 空の配列 return <div>{user?.name}</div>; }
解決策として、useEffectの依存配列に必要な値を正しく指定します。
Jsxfunction UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetchUser(userId).then(setUser); }, [userId]); // userIdを依存配列に追加 return <div>{user?.name}</div>; }
まとめ
本記事では、React Hooksで非同期処理をuseEffect以外で行う方法とその利点・欠点を解説しました。
- イベントハンドラ内での非同期処理: ユーザーのアクションに応じてデータを取得する場合に有効
- カスタムフックの活用: 非同期処理のロジックを再利用可能にする
- React QueryやSWRのようなライブラリの利用: 非同期処理に関する多くの課題を解決
- コンポーネント外での非同期処理: コンポーネントの純粋性を保つ
- SuspenseとReact.lazyとの組み合わせ: 宣言的な非同期処理を実現
この記事を通して、React Hooksで非同期処理を扱う際の選択肢が増え、より適切なコード設計ができるようになったことと思います。今後は、プロジェクトの要件に応じて最適な方法を選択し、効率的な開発を行っていきましょう。