記事は後ほど書きます。
振り返り用にメモ
✅メモリリーク対策追加(対応)
✅注意:asyncFunctionが変更されるたびにexecute関数が再作成され、意図しない動作が起こる可能性あり(対応)
import { useState, useCallback, useRef, useEffect } from "react";
/**
* 非同期アクションフック
* - 連打防止
* - ローディング状態管理
* - エラーハンドリング
*
* @param asyncFunction 非同期関数
* @returns [実行関数, isLoading]
*
* @example
* const [handleSubmit, isLoading] = useAsyncAction(async () => {
* await api.submit();
* });
*/
export function useAsyncAction<T extends (...args: any[]) => Promise<any>>(
asyncFunction: T
): [T, boolean] {
const [isLoading, setIsLoading] = useState(false);
const isProcessing = useRef(false);
//asyncFunction が変わった場合に対応するための ref
const asyncFunctionRef = useRef(asyncFunction);
// マウント状態を追跡する ref
const isMountedRef = useRef(true);
// asyncFunction が変更されたら ref を更新
useEffect(() => {
asyncFunctionRef.current = asyncFunction;
}, [asyncFunction]);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
const execute = useCallback(
(async (...args: Parameters<T>) => {
// アンマウント済みなら何もしない
if (!isMountedRef.current) {
if (__DEV__) {
console.log(
"コンポーネントがアンマウント済みのため、処理をスキップ"
);
}
return;
}
// 連打防止
if (isProcessing.current) {
if (__DEV__) {
console.log("処理中のため、クリックを無視");
}
return;
}
// 実行開始
isProcessing.current = true;
setIsLoading(true);
try {
const result = await asyncFunctionRef.current(...args);
return result;
} catch (error) {
// エラー
throw error;
} finally {
// 実行完了
// マウント済みの場合のみ状態更新
if (isMountedRef.current) {
setIsLoading(false);
}
isProcessing.current = false;
}
}) as T,
[]
);
return [execute, isLoading];
}