🤞

tanstack-queryとorvalのちょっとテクい書き方3選

2024/11/14に公開

エイジレスでテックリードをしているクリタです。

自社のプロダクトに表題のプラグインを導入してみた結果、ちょっと使いにくいと思っていたり、
こんな事簡単に実現できたら良いなという事がいくつかありました👀

本当にできないかなと思って、記事を漁ったり、色々試した結果、
これは他のプロダクトでも活用できるんじゃないか?と思ったので、
共有しようと思います。

1. 無限スクロールの取得処理をorvalでサクッと実装

これは、同じ会社のエンジニアが設定を見つけてくれました。マジナイスです。

orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  backend: {
    ...
    output: {
      ...
      override: {
        ...
        operations: {
          // この設定をいれると、コマンド実行時にuseGetMessagesInfiniteが作成される
          getMessages: {
            query: {
              useQuery: true,
              useInfinite: true,
              useInfiniteQueryParam: 'page'
            }
          }
        }
      }
    }
  }
});
message.tsx
const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage: isFetchingMessages
  } = useGetRoomMessagesInfinite(
    { page: '1' }, // pageは1を渡しておくと、fetchNextPageを実行した際に勝手にインクリメントしてリクエストしてくれる
    {
      query: {
        getNextPageParam: (lastPage) =>
          lastPage.messages.length < 30 ? undefined : Number(lastPage.page) + 1 // 取得したメッセージが30件以内なら最後のページとみなす
      }
    }
);

const messages = useMemo(() => data?.pages.map((page) => page.messages).flat() ?? [], [data]);

2. get処理で、dataじゃなくて実際定義されている項目名をワンライナーで書く

今まで

const { data } =
    useGetUsers();
const users = data?.users;
return users && (
  users.map((user) => (
    <div>{user.name}</div>
  );
);

こう書けた

// usersで参照可能
const { data: { users = undefined } = {} } = useGetUsers();
return users && (
  users.map((user) => (
    <div>{user.name}</div>
  );
);
// こう書けば、初期値が空配列に設定されるので、undefinedの考慮が不要になる
const { data: { users = [] } = {} } = useGetUsers();
return users.map((user) => (
    <div>{user.name}</div>
  );

3. カスタムインスタンスで、リクエストエラーのハンドリング

react-queryが、v5からget処理時のonsuccessとonerrorを廃止しています。
そのせいで、権限や条件に応じて403や404が発生した際のハンドリングをどうしたらいいか?という問題がありました。
(mutate系に関してはonerrorが残っていたので、特に気にしなくて良さそう)

そこで、カスタムインスタンスでレスポンスの検証をする形で、ハンドリングを行いました。
利用してるのはaxiosです。

import Axios, { AxiosError, AxiosRequestConfig } from 'axios';
import toast from 'react-hot-toast';

export const AXIOS_INSTANCE = Axios.create({ baseURL: '' });

export const useCustomInstance = <T>(): ((config: AxiosRequestConfig) => Promise<T>) => {
  const { getAccessTokenSilently, logout } = useAuth0();

  return async (config: AxiosRequestConfig) => {
    try {
      const token = await getToken(); // 各々の環境のアクセスキーを取得
      const source = Axios.CancelToken.source();
      const promise = AXIOS_INSTANCE({
        ...config,
        headers: {
          ...config.headers,
          ...(token ? { Authorization: `Bearer ${token}` } : undefined)
        },
        cancelToken: source.token,
        baseURL: process.env.API_PATH
      }).then(({ data, status }) => {
        return data as T;
      }).catch((error) => {
        if (error.name === 'AxiosError' && error.config.method === 'get') {
          toast.error(error.response?.data.error);
          // エラー時に画面側でハンドリングするために、nullを返す
          return null as T;
        }
        return Promise.reject(error);
      });

      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-expect-error
      promise.cancel = () => {
        source.cancel('Query was cancelled by React Query');
      };

      return promise;
    } catch (e) {
      logout();
      throw e;
    }
  };
};

export default useCustomInstance;

export type ErrorType<Error> = AxiosError<Error>;

こんな感じで、エラー時にnullを返してあげれば、既存処理に手を加えず、カスタムインスタンス内部でエラーハンドリングできました🎉

いかがでしたでしょうか?
もっと実務でいい書き方を見つけたら共有しますね😄

他にも、こんな書き方してまっせ!っていうのがあれば是非教えて欲しいです!🙏

Discussion