Reactで非同期処理を含むイベントハンドラーを使用する場合、エラーの適切な処理は、アプリケーションの安定性とユーザー体験を向上させるために極めて重要です。イベントハンドラーは、ボタンのクリックやフォームの送信など、ユーザーが行う操作に応答するコードを記述する主要な場所ですが、非同期処理が絡むと、予期せぬエラーや動作不良が発生することがあります。本記事では、非同期エラーがなぜ発生するのか、その対処方法、そしてReactでのベストプラクティスを解説します。エラーを適切にハンドリングすることで、より安全で堅牢なReactアプリケーションを構築するためのヒントを提供します。
Reactのイベントハンドラーの基本
Reactにおけるイベントハンドラーは、DOMイベントを抽象化し、React独自の仕組みを通じて管理されています。これにより、ブラウザ間での動作の一貫性が保証されます。
イベントハンドラーの設定
Reactでは、イベントハンドラーはコンポーネント内でJSX要素の属性として指定します。例えば、ボタンクリックのイベントハンドラーは次のように記述します。
function MyComponent() {
const handleClick = () => {
console.log('Button clicked!');
};
return <button onClick={handleClick}>Click me</button>;
}
onClick、onChange、onSubmitなどのイベント属性は、Reactの合成イベントシステムを通じて処理されます。
合成イベントとネイティブイベント
Reactのイベントハンドラーは、ネイティブDOMイベントを抽象化した合成イベント(SyntheticEvent)を利用します。これにより、Reactが独自にイベントのライフサイクルを管理し、パフォーマンスを最適化します。
以下は、合成イベントの例です。
function MyComponent() {
const handleEvent = (event) => {
console.log(event.type); // "click" (例)
};
return <button onClick={handleEvent}>Click me</button>;
}
合成イベントは一貫性があり、各ブラウザで同じインターフェースを提供します。
イベントハンドラーでの注意点
Reactでイベントハンドラーを扱う際には、以下の点に注意する必要があります。
this
のバインディング: クラスコンポーネントの場合、イベントハンドラーでthis
を使用するには明示的なバインディングが必要です。- メモリリーク: アンマウントされたコンポーネントに対するイベントリスナーを解除しないと、メモリリークが発生する可能性があります。
- 非同期処理との相性: 非同期処理が絡むとエラーハンドリングが複雑になるため、特別なケアが必要です。
この基礎を理解することで、Reactのイベントハンドラーを適切に設計できるようになります。次章では、非同期処理がイベントハンドラーにどのような影響を与えるかを詳しく見ていきます。
非同期処理とイベントハンドラー
非同期処理は、Reactのイベントハンドラー内でよく利用されます。非同期処理を正しく扱わないと、エラーが未処理のままアプリケーションの動作に影響を与えることがあります。ここでは、非同期処理がイベントハンドラーにどのように影響するかを解説します。
非同期処理とは
非同期処理は、時間がかかる操作(例: APIリクエストやファイル読み込み)を待機せずに進行する処理のことを指します。JavaScriptでは、非同期処理を扱うためにPromise
やasync/await
が提供されています。
例えば、次のように非同期処理をイベントハンドラーで使用するケースがあります。
function MyComponent() {
const handleClick = async () => {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error fetching data:', error);
}
};
return <button onClick={handleClick}>Fetch Data</button>;
}
この例では、APIリクエストが非同期処理として扱われています。
非同期処理の影響
非同期処理がイベントハンドラーに与える影響は以下の通りです。
1. 実行タイミングのずれ
非同期処理では、コードの実行が一時停止され、次の処理が進行します。そのため、状態管理や副作用の発生タイミングがずれる可能性があります。
function MyComponent() {
const [data, setData] = React.useState(null);
const handleClick = async () => {
console.log('Fetching data...');
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
console.log('Data set');
};
return <button onClick={handleClick}>Fetch</button>;
}
この例では、Fetching data...
とData set
のログが非同期のタイミングで出力されるため、デバッグが複雑になります。
2. エラーハンドリングの複雑化
非同期処理中に発生するエラーは、同期的なエラーとは異なる方法でキャッチする必要があります。try-catch
構文を適切に使用しないと、エラーが未処理のままになり、アプリケーションが予期せぬ動作をする可能性があります。
3. コンポーネントのアンマウント問題
非同期処理中にコンポーネントがアンマウントされると、非同期処理の結果が不要になる場合があります。この状況を無視すると、不要な状態更新が発生し、エラーが引き起こされる可能性があります。
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
const data = await fetchSomeData();
if (isMounted) {
setData(data);
}
};
fetchData();
return () => {
isMounted = false;
};
}, []);
非同期処理を考慮した設計の重要性
非同期処理を正しく設計し、エラーやタイミングのずれに対応することで、Reactアプリケーションの安定性を向上させることができます。次章では、よくある非同期エラーの発生パターンとその原因を詳しく解説します。
非同期エラーの発生原因
非同期処理を伴うReactのイベントハンドラーでは、特定の状況下でエラーが発生しやすくなります。これらのエラーを事前に把握し、適切に対処することが重要です。以下では、非同期エラーのよくある発生パターンとその原因を解説します。
1. ネットワークエラー
非同期処理で最も一般的なエラーの一つがネットワークエラーです。APIリクエストやリソースの読み込み中に接続がタイムアウトしたり、サーバーがエラーを返すことで発生します。
const handleFetch = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Network error:', error);
}
};
原因:
- サーバー側の不具合(500エラーなど)
- クライアントの接続問題(タイムアウトやDNSエラー)
- 誤ったエンドポイント指定
2. 非同期処理中の未処理エラー
非同期関数内でエラーが発生しても、それを適切にキャッチしなければアプリケーションがクラッシュする可能性があります。
const handleAction = async () => {
const data = await someAsyncFunction(); // エラーがキャッチされない
console.log(data);
};
原因:
try-catch
構文が不足しているPromise
のcatch
が実装されていない
3. 状態更新時の競合
非同期処理の完了タイミングが複数の場所で重なると、状態更新が競合し、予期しない動作を引き起こすことがあります。
const handleUpdate = async () => {
setLoading(true);
const data = await fetchData();
setData(data); // 他の状態更新と競合する可能性
setLoading(false);
};
原因:
- 複数の非同期タスクが同時に走る
- 状態が古い値を参照する(レースコンディション)
4. アンマウントされたコンポーネントでの更新
コンポーネントがアンマウントされた後に非同期処理が完了し、その結果を使用して状態を更新しようとするとエラーが発生する場合があります。
useEffect(() => {
let isMounted = true;
fetchData().then((data) => {
if (isMounted) {
setData(data); // アンマウント時に発生するエラーを防ぐ
}
});
return () => {
isMounted = false;
};
}, []);
原因:
- 非同期処理中にコンポーネントがアンマウントされる
- アンマウントされたコンポーネントの状態更新を試みる
5. 非同期関数のチェーンエラー
複数の非同期処理をチェーンで繋げた際、1つの処理でエラーが発生すると、その後の処理も影響を受けます。
const processActions = async () => {
try {
const result1 = await asyncAction1();
const result2 = await asyncAction2(result1); // result1がエラーの場合ここも失敗
console.log(result2);
} catch (error) {
console.error('Chain error:', error);
}
};
原因:
- 前段の非同期処理でのエラーが次段に影響
非同期エラーの未然防止と対策
非同期エラーの発生を防ぐには、次の対策を取ることが重要です。
- すべての非同期処理にエラーハンドリングを追加する
- 状態更新を行う際、コンポーネントのアンマウント状況を確認する
- 非同期処理が多重に実行されないように制御する
次章では、非同期エラーを防ぐ基本的な手法であるtry-catch
を用いたエラーハンドリングについて解説します。
try-catchによるエラーハンドリング
非同期処理におけるエラーハンドリングの基本は、try-catch
構文を使用してエラーを明示的に処理することです。この手法は、特にasync/await
を使用する場合に有効であり、エラーがアプリケーション全体に波及するのを防ぐために重要です。ここでは、try-catch
の基礎から実践例までを解説します。
try-catch構文の基本
try-catch
は、JavaScriptのエラーハンドリング構文で、エラーが発生する可能性のあるコードをtry
ブロック内に書き、そのエラーをcatch
ブロックで処理します。
try {
// エラーが発生する可能性のあるコード
} catch (error) {
// エラーを処理するコード
}
非同期処理の場合、await
を伴うコードも同様にtry-catch
で囲むことが推奨されます。
非同期処理での基本例
以下は、try-catch
を使用して非同期処理のエラーをキャッチする例です。
const handleFetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Fetched data:', data);
} catch (error) {
console.error('Error fetching data:', error.message);
}
};
この例では、ネットワークエラーやHTTPステータスエラーが発生した場合でも、エラーメッセージがコンソールに出力されるため、アプリケーションのクラッシュを防ぐことができます。
複数の非同期操作のハンドリング
複数の非同期操作を実行する場合、それぞれの操作でエラーが発生する可能性を考慮し、適切にエラーハンドリングを設計します。
const handleMultipleRequests = async () => {
try {
const [response1, response2] = await Promise.all([
fetch('https://api.example.com/data1'),
fetch('https://api.example.com/data2'),
]);
if (!response1.ok || !response2.ok) {
throw new Error('One or both API requests failed.');
}
const data1 = await response1.json();
const data2 = await response2.json();
console.log('Data1:', data1);
console.log('Data2:', data2);
} catch (error) {
console.error('Error in multiple requests:', error.message);
}
};
このコードは、複数の非同期リクエストが同時に行われた際のエラーを効率的に処理します。
共通エラーハンドリング関数の利用
アプリケーションの規模が大きくなると、エラーハンドリングを一元化する方が効率的です。共通のエラーハンドリング関数を作成することで、コードの再利用性を向上させられます。
const handleError = (error) => {
console.error('An error occurred:', error.message);
// 必要に応じて通知やログ保存の処理を追加
};
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
} catch (error) {
handleError(error);
}
};
非同期処理のキャンセル時の注意
非同期処理中にコンポーネントがアンマウントされた場合、処理結果を参照しようとしてエラーが発生することがあります。この問題を回避するためには、try-catch
と併せてisMounted
フラグを用いる方法が有効です。
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
if (isMounted) {
setData(data);
}
} catch (error) {
if (isMounted) {
console.error('Error fetching data:', error.message);
}
}
};
fetchData();
return () => {
isMounted = false;
};
}, []);
まとめ
try-catch
を活用することで、非同期処理のエラーを予測可能な方法で管理できます。これにより、ユーザーエクスペリエンスを損なうことなく、アプリケーションの堅牢性を向上させることが可能です。次章では、async/await
を使ったエラーハンドリングの詳細な実装例を紹介します。
async/awaitを用いたエラーハンドリングの実装
非同期処理を行う際にasync/await
を使用すると、コードをより直感的かつ同期的なスタイルで記述できます。これにより、非同期エラーの処理も簡潔かつ効果的に行えるようになります。ここでは、async/await
を活用したエラーハンドリングの詳細な実装例を解説します。
基本的なasync/awaitのエラーハンドリング
async
関数では、await
キーワードを使うことで非同期処理が完了するまで待機します。エラーが発生する可能性があるコードをtry-catch
ブロックで囲むことで、エラーを安全にキャッチできます。
const fetchUserData = async () => {
try {
const response = await fetch('https://api.example.com/user');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('User data:', data);
} catch (error) {
console.error('Failed to fetch user data:', error.message);
}
};
この例では、HTTPエラーやネットワークエラーが発生した場合でも、catch
ブロックで適切に処理できます。
複数の非同期処理の順次実行
async/await
は複数の非同期操作を順次実行するのにも適しています。それぞれの操作の結果を基に次の処理を進める場合に有効です。
const fetchUserAndPosts = async () => {
try {
const userResponse = await fetch('https://api.example.com/user');
const userData = await userResponse.json();
const postsResponse = await fetch(`https://api.example.com/posts?userId=${userData.id}`);
const postsData = await postsResponse.json();
console.log('User:', userData);
console.log('Posts:', postsData);
} catch (error) {
console.error('Error fetching user or posts:', error.message);
}
};
このように、1つの非同期操作が完了してから次の操作に進む場合、async/await
を使用することでコードが分かりやすくなります。
並行して非同期処理を実行する
Promise.all
と組み合わせることで、複数の非同期処理を並行して実行し、効率を向上させることができます。async/await
を使用することで、結果の取得やエラーの処理が容易になります。
const fetchMultipleResources = async () => {
try {
const [users, posts] = await Promise.all([
fetch('https://api.example.com/users').then((res) => res.json()),
fetch('https://api.example.com/posts').then((res) => res.json()),
]);
console.log('Users:', users);
console.log('Posts:', posts);
} catch (error) {
console.error('Error fetching resources:', error.message);
}
};
この例では、ユーザーと投稿のデータを同時に取得し、全てのリクエストが成功するまで待機します。一方で、いずれかのリクエストが失敗した場合、catch
ブロックでエラーを処理します。
非同期処理のキャンセル対応
非同期処理の結果が不要になった場合に備え、キャンセルフラグや適切なクリーンアップ処理を実装することが推奨されます。
const fetchDataWithCleanup = async () => {
let isCancelled = false;
const cancelFetch = () => {
isCancelled = true;
};
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
if (!isCancelled) {
console.log('Fetched data:', data);
}
} catch (error) {
if (!isCancelled) {
console.error('Error fetching data:', error.message);
}
}
return cancelFetch;
};
useEffect(() => {
const cancelFetch = fetchDataWithCleanup();
return () => cancelFetch();
}, []);
この例では、コンポーネントがアンマウントされた後に非同期処理の結果が使用されることを防いでいます。
エラーハンドリングの分離と再利用
async/await
を使用したエラーハンドリングは、共通化することでコードを再利用可能にし、メンテナンス性を向上させられます。
const handleApiError = (error) => {
console.error('API error:', error.message);
// エラー通知やログ記録を追加
};
const fetchData = async (url) => {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
handleApiError(error);
}
};
このようにすることで、エラー処理ロジックを一箇所にまとめ、どの非同期操作でも一貫した方法でエラーを処理できます。
まとめ
async/await
を活用することで、非同期処理を簡潔かつ効果的に記述でき、エラーハンドリングも明確に行えます。この技法は、Reactアプリケーションの信頼性を高めるために不可欠です。次章では、ReactのErrorBoundary
を活用したエラーハンドリングとその制限について詳しく解説します。
ErrorBoundaryの活用と制限
Reactには、アプリケーション内でエラーをキャッチし、ユーザー体験を改善するための仕組みとしてErrorBoundary
が提供されています。しかし、ErrorBoundary
は同期的なエラーには対応しますが、非同期処理のエラーにはそのままでは対応できません。ここでは、ErrorBoundary
の仕組みとその制限、非同期エラーへの対応方法について解説します。
ErrorBoundaryの仕組み
ErrorBoundary
は、特定の子コンポーネントで発生したエラーをキャッチして処理するコンポーネントです。エラーが発生した場合でもアプリケーション全体をクラッシュさせず、指定したフォールバックUIを表示します。
以下はErrorBoundary
の基本的な実装例です。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// エラー発生時に状態を更新
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// エラー情報をログに記録
console.error('Error caught by ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
// フォールバックUIを表示
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
このコンポーネントでラップされた部分でエラーが発生すると、getDerivedStateFromError
とcomponentDidCatch
が呼び出され、フォールバックUIを表示します。
ErrorBoundaryの制限
ErrorBoundary
は、以下のエラーをキャッチしますが、非同期処理やイベントハンドラー内で発生するエラーはキャッチできません。
- レンダリング中のエラー
- 子コンポーネントのレンダリング時に発生したエラー。
- ライフサイクルメソッド内のエラー
componentDidMount
やcomponentDidUpdate
内のエラー。
- 子コンポーネントのコンストラクタ内のエラー
以下のようなケースではエラーをキャッチできません:
- 非同期処理内のエラー(例:
fetch
のエラー)。 - イベントハンドラー内で発生したエラー。
非同期エラーへの対応
非同期処理のエラーはErrorBoundary
で直接キャッチすることができないため、次のようなアプローチを組み合わせて対応します。
1. try-catchとErrorBoundaryの併用
非同期エラーは、try-catch
で捕捉し、ErrorBoundary
のエラー処理に転送することが可能です。
class ErrorBoundary extends React.Component {
// 上記のErrorBoundaryと同様
}
const fetchDataWithErrorBoundary = async (setError) => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Fetched data:', data);
} catch (error) {
setError(error);
}
};
const App = () => {
const [error, setError] = React.useState(null);
if (error) {
throw error; // ErrorBoundaryに転送
}
return (
<ErrorBoundary>
<button onClick={() => fetchDataWithErrorBoundary(setError)}>Fetch Data</button>
</ErrorBoundary>
);
};
この例では、setError
を通じて非同期エラーをErrorBoundary
に伝える仕組みを構築しています。
2. グローバルなエラーハンドリング
非同期処理で発生するエラーをキャッチするために、window.onerror
やwindow.addEventListener
を活用する方法もあります。
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
});
ただし、これらの方法はローカルなエラー処理よりも広範囲を対象とするため、慎重に使用する必要があります。
3. 状態管理ライブラリを利用したエラーの集中管理
ReduxやZustandなどの状態管理ライブラリを使用し、エラーをアプリケーション全体で管理する方法も効果的です。
const errorSlice = createSlice({
name: 'error',
initialState: null,
reducers: {
setError: (state, action) => action.payload,
clearError: () => null,
},
});
const { setError, clearError } = errorSlice.actions;
// 非同期関数での利用
const fetchData = () => async (dispatch) => {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
dispatch(setError(error.message));
}
};
まとめ
ErrorBoundary
は、Reactアプリケーションでのエラーハンドリングに強力なツールですが、非同期エラーには直接対応できない制限があります。そのため、try-catch
の併用や状態管理、グローバルハンドラーの設定など、他の方法を組み合わせることが必要です。次章では、アプリケーション固有のエラーハンドリングロジックを設計する方法を解説します。
カスタムエラーハンドリングロジックの設計
Reactアプリケーションにおけるエラーハンドリングは、特定のユースケースや要件に応じてカスタマイズする必要があります。ここでは、アプリケーション全体で利用可能なカスタムエラーハンドリングロジックを設計する方法を解説します。
エラーハンドリング設計の基本原則
カスタムエラーハンドリングを設計する際に考慮すべき基本原則は以下の通りです:
- エラーの種類を分類する
- ネットワークエラー、ユーザー入力エラー、システムエラーなど。
- エラーの発生場所を特定する
- コンポーネント内、非同期処理中、API呼び出しなど。
- ユーザー通知を最適化する
- 適切なタイミングと方法でエラーをユーザーに通知する。
エラーハンドリングのコンテキスト設計
ReactのコンテキストAPIを使用して、アプリケーション全体でエラー状態を共有できる仕組みを作ります。
import React, { createContext, useContext, useState } from 'react';
const ErrorContext = createContext();
export const ErrorProvider = ({ children }) => {
const [error, setError] = useState(null);
const handleError = (error) => {
setError(error);
};
const clearError = () => {
setError(null);
};
return (
<ErrorContext.Provider value={{ error, handleError, clearError }}>
{children}
</ErrorContext.Provider>
);
};
export const useError = () => useContext(ErrorContext);
このErrorProvider
をルートコンポーネントにラップすることで、どのコンポーネントからでもエラーハンドリングを利用可能にします。
import { ErrorProvider } from './ErrorContext';
function App() {
return (
<ErrorProvider>
<MainComponent />
</ErrorProvider>
);
}
エラーの集中管理
コンテキストを使用することで、エラー情報を一元管理し、各コンポーネントで一貫性のある処理を実装できます。
import { useError } from './ErrorContext';
const FetchComponent = () => {
const { handleError, clearError } = useError();
const fetchData = async () => {
try {
clearError();
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
handleError(error.message);
}
};
return <button onClick={fetchData}>Fetch Data</button>;
};
エラー通知の設計
エラーが発生した際に、ユーザーに適切なフィードバックを提供するUIを設計します。以下は、エラーをモーダルで表示する例です。
import { useError } from './ErrorContext';
const ErrorModal = () => {
const { error, clearError } = useError();
if (!error) return null;
return (
<div className="error-modal">
<p>{error}</p>
<button onClick={clearError}>Close</button>
</div>
);
};
export default ErrorModal;
このErrorModal
をルートコンポーネントに追加することで、グローバルなエラー通知を実現できます。
function App() {
return (
<ErrorProvider>
<MainComponent />
<ErrorModal />
</ErrorProvider>
);
}
ログとモニタリングの組み込み
エラーの詳細を外部サービスに送信することで、運用時のトラブルシューティングが容易になります。以下は、エラーをログサーバーに送信する例です。
const logError = async (error) => {
await fetch('https://api.example.com/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error, timestamp: new Date() }),
});
};
const handleErrorWithLogging = (error) => {
handleError(error);
logError(error).catch(console.error);
};
エラーハンドリングのテスト
カスタムエラーハンドリングロジックが正しく動作することを確認するために、テストケースを作成します。React Testing Libraryを使用したテスト例を示します。
import { render, fireEvent } from '@testing-library/react';
import { ErrorProvider } from './ErrorContext';
import FetchComponent from './FetchComponent';
test('displays error message on fetch failure', async () => {
global.fetch = jest.fn(() =>
Promise.reject(new Error('Fetch failed'))
);
const { getByText, findByText } = render(
<ErrorProvider>
<FetchComponent />
</ErrorProvider>
);
fireEvent.click(getByText('Fetch Data'));
const errorMessage = await findByText('Fetch failed');
expect(errorMessage).toBeInTheDocument();
});
まとめ
カスタムエラーハンドリングロジックを設計することで、エラー処理を統一し、Reactアプリケーション全体で一貫性を持たせることができます。コンテキスト、通知UI、ログシステムを組み合わせることで、エラー管理の効率と信頼性が向上します。次章では、具体的な実践例としてフォーム入力イベントを利用した非同期処理のエラーハンドリングを紹介します。
実践例: フォーム入力イベントでの非同期処理
Reactアプリケーションでは、フォームの送信や入力イベントを処理する際に非同期処理を実装することがよくあります。ここでは、非同期処理を伴うフォーム入力イベントでのエラーハンドリングの具体例を解説します。
フォーム送信における非同期処理
以下の例は、フォームの送信ボタンをクリックした際にAPIリクエストを送信し、結果を処理する非同期処理の実装例です。
import React, { useState } from 'react';
const FormWithAsyncHandling = () => {
const [formData, setFormData] = useState({ name: '', email: '' });
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
setSuccessMessage('');
try {
const response = await fetch('https://api.example.com/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setSuccessMessage('Form submitted successfully!');
console.log('Form submission result:', result);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Name:
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
/>
</label>
</div>
<div>
<label>
Email:
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
</label>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Submitting...' : 'Submit'}
</button>
{error && <p className="error">Error: {error}</p>}
{successMessage && <p className="success">{successMessage}</p>}
</form>
);
};
export default FormWithAsyncHandling;
ポイント解説
1. フォームデータの状態管理
useState
フックを利用してフォームデータ、エラー状態、成功メッセージを管理します。
formData
: 入力データを保持。error
: エラーメッセージを保持。loading
: 処理中の状態を示すフラグ。successMessage
: 成功時のメッセージを表示。
2. エラーの捕捉と表示
非同期処理の中でエラーが発生した場合、try-catch
構文を用いてエラーをキャッチし、ユーザーにわかりやすいエラーメッセージを表示します。
try {
// APIリクエスト処理
} catch (error) {
setError(error.message);
}
フォームの下部でエラーを視覚的に通知するため、次のコードで表示を実装しています。
{error && <p className="error">Error: {error}</p>}
3. ローディング状態の管理
フォーム送信中はボタンを「Submitting…」に切り替え、二重送信を防止します。
<button type="submit" disabled={loading}>
{loading ? 'Submitting...' : 'Submit'}
</button>
4. 成功時の通知
APIリクエストが成功した場合、ユーザーに成功メッセージを通知します。
setSuccessMessage('Form submitted successfully!');
エラーケースの具体例
以下のケースを考慮してエラーハンドリングを設計しています。
- ネットワークエラー
- サーバーに接続できない場合、
catch
でエラーメッセージを設定。
- HTTPエラー
- ステータスコードが
200
以外の場合、response.ok
をチェック。
- JSONパースエラー
- APIレスポンスが無効な場合、
await response.json()
で例外をキャッチ。
拡張: バリデーションの追加
フォーム送信前に、入力データを検証するクライアントサイドバリデーションを追加することで、さらに堅牢な設計にすることができます。
const validateFormData = (data) => {
if (!data.name) {
return 'Name is required.';
}
if (!data.email || !/\S+@\S+\.\S+/.test(data.email)) {
return 'A valid email is required.';
}
return null;
};
const handleSubmit = async (e) => {
e.preventDefault();
const validationError = validateFormData(formData);
if (validationError) {
setError(validationError);
return;
}
// 非同期処理続行
};
まとめ
非同期処理を伴うフォームイベントの実装には、状態管理、エラーハンドリング、ユーザー通知の仕組みが欠かせません。このように、適切な設計を行うことで、Reactアプリケーションのユーザー体験を向上させることができます。次章では、非同期エラー処理のテスト手法について解説します。
エラーハンドリングのテスト手法
非同期処理を含むエラーハンドリングが正しく動作することを確認するためには、適切なテストを実施する必要があります。ここでは、React Testing LibraryやJestを用いた非同期エラー処理のテスト手法を解説します。
テストで考慮すべきポイント
エラーハンドリングのテストでは、以下の点を確認します:
- エラー発生時の動作
- エラーメッセージが適切に表示されるか。
- 正常系の動作
- 正常に処理が完了した場合、正しい結果が反映されるか。
- ローディング状態の管理
- 非同期処理中に正しいローディングインジケータが表示されるか。
- コンポーネントの状態更新
- エラー後、状態が正しくリセットされるか。
基本的なテスト例
以下は、React Testing Libraryを使用した非同期処理のエラーハンドリングテストの例です。
import { render, fireEvent, screen, waitFor } from '@testing-library/react';
import FormWithAsyncHandling from './FormWithAsyncHandling';
test('displays error message on API failure', async () => {
// Mock fetch API
global.fetch = jest.fn(() =>
Promise.reject(new Error('Network Error'))
);
render(<FormWithAsyncHandling />);
// ユーザーがフォームを送信
fireEvent.click(screen.getByText('Submit'));
// エラーメッセージの表示を確認
const errorMessage = await screen.findByText(/Network Error/i);
expect(errorMessage).toBeInTheDocument();
// ボタンの状態確認
expect(screen.getByText('Submit')).not.toBeDisabled();
});
ポイント解説
1. 非同期操作のモック
API呼び出しなどの非同期操作は、テストでモック化します。jest.fn()
を使用してfetch
関数をモックします。
global.fetch = jest.fn(() => Promise.reject(new Error('Network Error')));
2. DOMの状態確認
screen.findByText
を使用して、非同期処理後にエラーメッセージが正しく表示されているかを確認します。
const errorMessage = await screen.findByText(/Network Error/i);
expect(errorMessage).toBeInTheDocument();
3. ローディングとボタンの状態
フォーム送信中にボタンが無効化され、処理完了後に有効化されるかを確認します。
expect(screen.getByText('Submit')).not.toBeDisabled();
正常系テスト
次は、フォーム送信が成功した場合のテスト例です。
test('displays success message on API success', async () => {
// Mock fetch API for success
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ message: 'Success' }),
})
);
render(<FormWithAsyncHandling />);
fireEvent.click(screen.getByText('Submit'));
// 成功メッセージの表示を確認
const successMessage = await screen.findByText(/Form submitted successfully!/i);
expect(successMessage).toBeInTheDocument();
});
エラーハンドリングのカバレッジを向上させるテクニック
1. 境界ケースのテスト
ステータスコードによるエラーやJSONパースエラーなど、特定のケースをモックで再現します。
global.fetch = jest.fn(() => Promise.resolve({ ok: false, status: 500 }));
2. 状態リセットのテスト
エラー発生後にフォームがリセットされるかを確認します。
test('resets error state after successful submit', async () => {
// Mock initial failure
global.fetch = jest.fn(() => Promise.reject(new Error('Network Error')));
render(<FormWithAsyncHandling />);
fireEvent.click(screen.getByText('Submit'));
await screen.findByText(/Network Error/i);
// Mock successful retry
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ message: 'Success' }),
})
);
fireEvent.click(screen.getByText('Submit'));
const successMessage = await screen.findByText(/Form submitted successfully!/i);
expect(successMessage).toBeInTheDocument();
});
まとめ
エラーハンドリングのテストでは、非同期処理の特性を考慮し、エラーメッセージやローディング状態、フォームのリセット動作などを網羅的に確認する必要があります。これにより、Reactアプリケーションの信頼性を向上させ、予期せぬエラーに対する耐性を強化できます。次章では、この記事の内容をまとめます。
まとめ
本記事では、Reactアプリケーションにおける非同期処理を含むイベントハンドラーのエラーハンドリングについて、理論から実践まで詳しく解説しました。非同期処理は、APIリクエストやユーザー操作の処理に不可欠ですが、適切なエラーハンドリングがなければ、アプリケーションの安定性やユーザー体験が損なわれる可能性があります。
重要なポイントは以下の通りです:
- try-catchを活用したエラー処理: 基本的な非同期エラーを安全に処理するための手法。
- ErrorBoundaryの制限と対応策: ReactのErrorBoundaryが非同期エラーに対応できない場合の補完的なアプローチ。
- カスタムエラーハンドリングロジックの設計: コンテキストを利用してエラーを集中管理する方法。
- 実践例: フォーム入力イベント: フォーム送信時の非同期処理でエラーを適切に管理する実装例。
- テスト手法: 非同期処理やエラーハンドリングの動作を確認するためのテストケースの設計。
非同期処理は、エラー発生時の動作を慎重に設計しなければ、ユーザーに混乱を与える可能性があります。本記事で紹介した手法を活用し、堅牢なエラーハンドリングを設計することで、信頼性の高いReactアプリケーションを構築してください。
コメント