【React Query】queryFnでundefinedをreturnしてはいけない理由と解決策
【React Query】queryFnでundefinedをreturnしてはいけない理由と解決策
React Queryは非常に便利なデータフェッチングライブラリですが、queryFn
でundefined
を返すと様々な問題が発生します。
この記事では、queryFnで沼った私の屍を超えてもらえるように解説します。
何気なくqueryFnでundefinedを返してしまい、「なんで動かないんだ...?」と数時間悩みました。
isSuccessはtrueなのにdataが使えない、キャッシュがおかしい、TypeScriptで型エラーが出る...。
同じ沼にハマる人を一人でも減らすために、なぜqueryFnでundefinedを返してはいけないのか、どのような問題が発生するのか、そして適切な解決策について詳しく解説します。
なぜundefinedを返してはいけないのか?
React QueryのqueryFn
でundefinedを返すことは、複数の深刻な問題を引き起こします。その理由を詳しく見ていきましょう。
1. 型安全性の問題
TypeScriptを使用している場合、React Queryの型定義ではqueryFn
の戻り値にundefined
は含まれていません:
// React Queryの型定義(簡略版)
type QueryFunction<T = unknown> = (context: QueryFunctionContext) => T | Promise<T>
この型定義により、undefinedを返そうとすると型エラーが発生します:
// ❌ TypeScriptエラーが発生
const fetchData = (): Promise<string | undefined> => {
// Type 'Promise<string | undefined>' is not assignable to type 'Promise<string>'
return Promise.resolve(undefined);
};
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData, // 型エラー!
});
仕様としてundefinedが禁止されていることが、React Queryの型定義からも明確に分かります。
2. 予期しない動作の発生
React Queryはundefined
を有効なデータとして扱わず、以下のような問題が発生します:
// undefinedが返されると、isSuccessがfalseになる可能性
const { data, isSuccess, isError } = useQuery({
queryKey: ['data'],
queryFn: () => undefined, // 成功とみなされない
});
console.log(isSuccess); // false になることがある
console.log(data); // undefined
3. キャッシュの問題
undefined
が返されると、React Queryのキャッシュメカニズムが正常に機能しない場合があります:
// キャッシュが適切に更新されない例
const fetchUser = async (id: string) => {
if (!id) return; // undefinedを返す
const response = await fetch(`/api/users/${id}`);
return response.json();
};
// 初回: id = "" でundefinedが返される
// 2回目: id = "123" でも古いキャッシュの影響で期待通りに動作しない可能性
よくある間違いのパターン
以下のようなコードを書いてしまうことがあります:
import { useQuery } from '@tanstack/react-query';
// ❌ 悪い例:条件によってはundefinedが返される
const fetchUser = async (userId?: string) => {
if (!userId) {
return; // これがundefinedを返してしまう
}
const response = await fetch(`/api/users/${userId}`);
return response.json();
};
function UserProfile({ userId }: { userId?: string }) {
const { data, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
// エラーが発生する可能性がある
return <div>{data?.name}</div>;
}
問題となる具体的なケース
ケース1:条件分岐でundefinedが返される
// ❌ 問題のあるコード
const fetchUserData = async (isLoggedIn: boolean) => {
if (!isLoggedIn) {
return; // undefinedが返される
}
const response = await fetch('/api/user');
return response.json();
};
ケース2:APIレスポンスがnullやundefinedの場合
// ❌ 問題のあるコード
const fetchProfile = async () => {
const response = await fetch('/api/profile');
const data = await response.json();
// dataがnullの場合、undefinedを返してしまう
return data?.profile; // profileが存在しない場合undefined
};
ケース3:TypeScriptの型エラーを無視している場合
// ❌ 型エラーを無視している
const fetchItems = async (): Promise<Item[] | undefined> => {
try {
const response = await fetch('/api/items');
return response.json();
} catch (error) {
return; // undefinedを返している
}
};
正しい解決策
解決策1:明示的にnullやデフォルト値を返す
// ✅ 良い例:明示的にnullを返す
const fetchUser = async (userId?: string) => {
if (!userId) {
return null; // nullを明示的に返す
}
const response = await fetch(`/api/users/${userId}`);
return response.json();
};
function UserProfile({ userId }: { userId?: string }) {
const { data, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId, // userIdがある場合のみクエリを実行
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error occurred</div>;
if (!data) return <div>No user data</div>;
return <div>{data.name}</div>;
}
解決策2:enabledオプションを活用する
// ✅ enabledオプションで条件付き実行
function UserDashboard({ userId }: { userId?: string }) {
const { data, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
// この時点でuserIdは必ず存在する
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
enabled: !!userId, // userIdが存在する場合のみ実行
});
return (
<div>
{isLoading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
{data && <div>Welcome, {data.name}!</div>}
</div>
);
}
解決策3:エラーハンドリングを適切に行う
// ✅ 適切なエラーハンドリング
const fetchProfile = async () => {
try {
const response = await fetch('/api/profile');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// データが存在しない場合は空のオブジェクトを返す
return data?.profile || {};
} catch (error) {
// エラーを再スローして、React Queryに処理を委ねる
throw error;
}
};
解決策4:TypeScriptの型を正しく定義する
// ✅ 型定義を明確にする
interface User {
id: string;
name: string;
email: string;
}
// undefinedではなくnullまたはエラーを返すことを明示
const fetchUser = async (userId: string): Promise<User | null> => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('User not found');
}
const userData = await response.json();
return userData || null;
};
ベストプラクティス
1. 常に明示的な値を返す
// ✅ 推奨パターン
const queryFn = async () => {
// データの種類に応じて適切なデフォルト値を返す
return data || null || [] || {};
};
// 配列の場合の具体例
const fetchItems = async (): Promise<Item[]> => {
const response = await fetch('/api/items');
const data = await response.json();
return data.items || []; // 必ず配列を返す
};
2. enabledオプションを積極的に使う
// ✅ 条件付きクエリの正しい書き方
const { data } = useQuery({
queryKey: ['data', id],
queryFn: () => fetchData(id),
enabled: !!id && someCondition,
});
3. エラーは適切にスローする
// ✅ エラーハンドリング
const fetchData = async () => {
try {
const result = await apiCall();
return result;
} catch (error) {
// React Queryがエラー状態を管理できるようにスローする
throw new Error('Failed to fetch data');
}
};
まとめ
React QueryのqueryFn
でundefinedを返すことは、以下の問題を引き起こす可能性があります:
- 型安全性の喪失:TypeScriptの型定義に反する
- 予期しないデータ状態:成功扱いだがデータがundefinedになる
- キャッシュの汚染:undefinedがキャッシュに保存されて後続クエリに影響
- デバッグの困難さ:isSuccessがtrueなのにデータが使えない状況
適切な対策として:
- 明示的にnullやデフォルト値を返す
- enabledオプションで条件付き実行を制御する
- 適切なエラーハンドリングを実装する
- TypeScriptの型定義を正しく行う
これらのポイントを意識することで、より安全で予測可能なReact Queryアプリケーションを構築できます。undefinedの罠にハマらないよう、ぜひこの記事で紹介した解決策を活用してみてください!
Discussion