🧞‍♂️

【React Query】queryFnでundefinedをreturnしてはいけない理由と解決策

に公開

【React Query】queryFnでundefinedをreturnしてはいけない理由と解決策

React Queryは非常に便利なデータフェッチングライブラリですが、queryFnundefinedを返すと様々な問題が発生します。

この記事では、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なのにデータが使えない状況

適切な対策として:

  1. 明示的にnullやデフォルト値を返す
  2. enabledオプションで条件付き実行を制御する
  3. 適切なエラーハンドリングを実装する
  4. TypeScriptの型定義を正しく行う

これらのポイントを意識することで、より安全で予測可能なReact Queryアプリケーションを構築できます。undefinedの罠にハマらないよう、ぜひこの記事で紹介した解決策を活用してみてください!

コミュニティオ テックブログ

Discussion