📉

TanStack Queryを使ってみる

2023/05/08に公開

こんにちは!
最近、お仕事の関係で、TanStack Query(旧:React Query)について学ぶ必要が出てきました。折角なので、TanStack Query を使って簡単に実装を行なってみました!

ということで、本記事では、TanStack Query について、基本的な使い方や自分が行なった実装について、まとめました!

TanStack Query とは?

TanStack Query とは、データフェッチライブラリです。
API からデータを取得したい時や更新したい時に使うことができます。また、取得したデータはキャッシュされ、キャッシュされたデータを取り出すことも可能です。

https://tanstack.com/query/latest/docs/react/overview

基本的な使い方

とりあえず、基本的な使い方を見ていきたいと思います。
useQueryuseMutationgetQueryDataについて主に取り上げていきます。

準備

まずは、TanStack Query を使用するための準備をしていきます。

インストール

インストールはnpmyarnそれぞれ次のコマンドになります。

# npm
$ npm i @tanstack/react-query

# yarn
$ yarn add @tanstack/react-query

index.tsx の修正

index.tsxの修正を行なっていきます。
index.tsxでは、TanStack Query を使うための設定を行います。

const queryClient: QueryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
});

まず、キャッシュを利用するために、QueryClientというクラスのインスタンスを作成します。
インスタンス生成の引数として、defaultOptionsプロパティを持つオブジェクトを渡すことができます。
上記のコードでは、このdefaultOptionsプロパティのqueriesプロパティに、refetchOnWindowFocus: falseと設定されています。これは、TanStack Query がデフォルトでコンポーネントにフォーカスが当たるとフェッチが作動してしまうのを無効にします。

あとは、QueryClientProviderに先ほど用意したqueryClientを渡して、アプリケーション全体で利用できるようにするだけです。

const root: ReactDOM.Root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

これで、TanStack Query を使用するための準備は完了です。

データ取得

データの取得についてです。
データ取得には、useQueryというフックを使います。

const { isLoading, data } = useQuery({
  queryKey: ['任意の文字列'] // 配列
  queryFn: exampleFetchFunc // データ取得の関数
});

useQueryフックの引数に、queryKeyqueryFnをプロパティとして持つオブジェクトを渡しています。

queryKeyは、データを再取得する時やキャッシュ管理などで利用されます。文字列の配列で渡します。
queryFnは、データを取得するための関数を渡します。実装した関数を渡すことができます。別で関数を実装せずに、アロー関数をそのまま渡すことも可能です。

dataだけでなく、isLoadingでローディング状態についても取得できます。

データ更新

続いて、データの更新です。
データの追加や変更などを行うには、useMutationフックを使います。

const mutation = useMutation({
  mutationFn: exampleMutateFunc // データ更新の関数
});

useMutationフックの引数に、mutationFnというプロパティを持つオブジェクトを渡します。

mutationFnには、データを更新する関数を入れます。

フォームから受け取ったデータを使って更新する場合があると思います。そういった、データ更新の関数実行時に引数を渡す必要がある場合には、以下のように、アロー関数で引数を渡すことになります。

const mutationWithArg = useMutation({
  mutationFn: (arg) => exampleMutateWithArgFunc(arg) // データ更新の関数
});

// データ更新実行
const onSubmit = (formData) => {
    mutationWithArg.mutate(formData);
};

キャッシュされたデータを利用する

最後に、キャッシュされたデータの利用についてです。
キャッシュされたデータの取り出しには、useQueryClientフックとgetQueryDataメソッドを使います。

データ取得useQueryを実行したコンポーネントとはまた別のコンポーネントで、取得したデータを使いたい場合などあると思います。そういった時に、このuseQueryClientフックとgetQueryDataメソッドでキャッシュされたデータを取り出してくることで、データを利用することができます。

const queryClient: QueryClient = useQueryClient();
const data = queryClient.getQueryData(['取り出したいqueryKey']);

useQueryClientフックは、その時点のQueryClient取得できます。この中に、キャッシュされたデータやdefaultOptionsの設定情報などが含まれています。

QueryClientクラスには、getQueryDataメソッドが定義されているので、それを使ってキャッシュされたデータの取得を行います。getQueryDataメソッドの引数には、取り出したいキャッシュのqueryKeyの配列を渡します。ここは、queryKeyの配列を代入した変数を別で定義して、その変数を渡すことも可能です。

API を使って実践

ここからは、先ほど説明したフックやメソッドを利用して、API からのデータ取得・更新(追加)・利用を実践してみたいと思います。

データを取得して表示する

まずは、データを取得して表示するところから進めます。

データを取得する関数

API からデータを取得する関数、getDataを実装します。
/src/ts/getData.tsを作ります。

import axiosInstance from '../axios/axiosInstance';
import { AxiosResponse } from 'axios';
import { GetResultDataType } from '../types/GetResultDataType';

const getData = async (): Promise<GetResultDataType> => {
  const result: AxiosResponse = await axiosInstance.get('/dynamoDB/getItems');
  const resultData: GetResultDataType = result.data;
  return resultData;
};

export default getData;

API 呼び出しには、axiosを利用しています。
/dynamoDB/getItemsに GET リクエストを送ることで、API からデータをレスポンスとして返してもらいます。

useQuery でデータ取得

それでは、TanStack Query のuseQueryと用意したデータを取得する関数getDataを利用して、データ取得を実装してみます。
/src/App.tsxを編集していきます。

const App = (): JSX.Element => {
  const { data }: UseQueryResult<GetResultDataType | undefined> = useQuery({
    queryKey: ['data'],
    queryFn: getData,
  });

  return (
    <div>
      <div>
        <ul>
          {data?.Items.map((item, index) => (
            <li key={index}>{item.weight}</li>
          ))}
        </ul>
        <p>現在のデータ数:{data?.Count}</p>
      </div>
    </div>
  );
};

export default App;

データ取得部分だけ取り出して見てみます。

const { data }: UseQueryResult<GetResultDataType | undefined> = useQuery({
  queryKey: ['data'],
  queryFn: getData,
});

TanStack Query のuseQueryフックを使用しています。
queryKeyには、dataという文字列を要素とする配列を渡しています。これが、取得してきたデータの再取得やキャッシュから利用する場合に使われます。
queryFnには、getDataが渡されています。このgetDataが、先ほど実装した API からのデータ取得関数getDataです。

このuseQueryフックを利用して取得したデータは、一部をmapメソッドで展開して箇条書きリストで表示しています。
また、DynamoDB のテーブルに含まれる項目数もCountというプロパティに入って返ってくるので、「現在のデータ数」として表示させています。

<ul>
  {data?.Items.map((item, index) => (
    <li key={index}>{item.weight}</li>
  ))}
</ul>

<p>現在のデータ数:{data?.Count}</p>

ブラウザで確認するとこんな感じです。

箇条書きリストで、2つのデータが表示されています。現在のデータ数にも、「2個」と表示されています。

フォーム送信してデータを追加する

それでは、フォームに値を入力して、データを追加する実装も行なっていきます。

データを追加する関数

データを追加する関数、postDataを実装します。
/src/ts/postData.tsを作ります。

import axiosInstance from '../axios/axiosInstance';
import { AxiosResponse } from 'axios';
import { FormDataType } from '../types/FormDataType';

const postData = async (formData: FormDataType): Promise<void> => {
  const result: AxiosResponse = await axiosInstance.post(
    '/dynamoDB/addItem',
    formData
  );
  console.log(result);
};

export default postData;

フォームに入力された値をリクエストボディに入れて、/dynamoDB/addItemに POST リクエストを送ることで、DynamoDB への項目(データ)追加を行います。

useMutation でデータ追加

それでは、TanStack Query のuseMutationと用意したデータを追加するための関数postDataを利用して、フォームからのデータ追加を実装してみます。
/src/RecordingForm.tsxRecordingFormコンポーネントを作っていきます。
フォーム実装には React Hook Form を、日付選択には react-datepicker を、日付の整形には date-fns を使用しています。
また、実際には、フォームのバリデーションも行なっていますが、見やすさのため削除しています。

type FormValuesType = {
  date: Date;
  weight: string;
};

const RecordingForm = (): JSX.Element => {
  const {
    register,
    handleSubmit,
    reset,
    control,
    formState: { errors },
  } = useForm<FormValuesType>();

  const queryClient: QueryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (formData: FormDataType) => postData(formData),
    onSettled: () => {
      queryClient.invalidateQueries(['data']);
    },
  });

  const onSubmit = async (data: FormValuesType): Promise<void> => {
    // 日付フォーマット
    const date: Date = new Date(data.date);
    const dataYear: number = date.getFullYear();
    const dataMonth: number = date.getMonth();
    const dataDate: number = date.getDate();
    const formattedDate: string = format(
      new Date(dataYear, dataMonth, dataDate),
      'yyyy/MM/dd'
    );

    // 体重(Stringで入ってくるので、Numberにする必要がある)
    const weightFloat: number = parseFloat(data.weight);

    // 追加するデータのオブジェクト
    const formData: FormDataType = {
      date: formattedDate,
      timestamp: date.getTime(),
      weight: weightFloat,
    };

    // DynamoDBへ登録する関数実行
    mutation.mutate(formData);

    // フォームリセット
    reset();
  };

  return (
    <div>
      <h2>フォーム</h2>
      <form>
        <div>
          <label htmlFor='date'>Date</label>
          <Controller
            name='date'
            control={control}
            render={({ field: { onChange, value } }) => (
              <DatePicker
                selected={value}
                onChange={onChange}
                locale={ja}
                id='date'
                placeholderText='日付を選択してください'
              />
            )}
          />
        </div>

        <div>
          <label htmlFor='weight'>
            Weight
          </label>
          <input
            id='weight'
            placeholder='入力してください'
            {...register('weight')}
          />
          <span>kg</span>
        </div>

        <div>
          <button onClick={handleSubmit(onSubmit)}>
            追加する
          </button>
        </div>
      </form>
    </div>
  );
};

export default RecordingForm;

「追加する」ボタン押下で、onSubmitが走るようになっています。
onSubmitにフォーカスして見てみます。

const onSubmit = async (data: FormValuesType): Promise<void> => {
  // 日付フォーマット
  const date: Date = new Date(data.date);
  const dataYear: number = date.getFullYear();
  const dataMonth: number = date.getMonth();
  const dataDate: number = date.getDate();
  const formattedDate: string = format(
    new Date(dataYear, dataMonth, dataDate),
      'yyyy/MM/dd'
  );

  const timestamp: number = date.getTime();

  // 体重(Stringで入ってくるので、Numberにする必要がある)
  const weightFloat: number = parseFloat(data.weight);

  // 追加するデータのオブジェクト
  const formData: FormDataType = {
    date: formattedDate,
    timestamp: timestamp,
    weight: weightFloat,
  };

  // データ追加処理実行
  mutation.mutate(formData);

  // フォームリセット
  reset();
};

まずはフォームに入力された値を整形するなど処理を行なっています。それらの処理を行なって、formDataというオブジェクトにしています。

続いて、TanStack Query を使ったデータの追加部分にフォーカスを当てます。

const queryClient: QueryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: (formData: FormDataType) => postData(formData),
  onSettled: () => {
    queryClient.invalidateQueries(['data']);
  }
});

TanStack Query でデータ更新を行う、useMutaionを使っています。
mutationFnには、データ更新を行う関数として、先ほど実装したデータを追加する関数postDataを渡しています。POST リクエストでリクエストボディにはフォームで入力した値に少し手を加えたオブジェクトを渡したいので、formDataを引数に渡しています。mutationFn実行後に再度データを取り直したいので、onSettledには、invalidateQueriesメソッド(引数としてqueryKeyを渡している)によってキャッシュを無効にしています。これにより、データ追加を実行する度に、データを再度取り直して最新のデータが表示できるようになります。

useMutationを実行するように定義したmutationですが、実際にはどのように実行されているのでしょうか。再び、onSubmitの中を見てみます。

// 追加するデータのオブジェクト
const formData: FormDataType = {
  date: formattedDate,
  timestamp: timestamp,
  weight: weightFloat
};

// データ追加処理実行
mutation.mutate(formData);

先ほどのmutationmutateメソッドが実行されています。オブジェクトformDataが引数として渡されています。このformDatamutationFnの関数postDataの引数として渡される、つまり、POST リクエストを送るときのリクエストボディになります。

RecordingFormコンポーネントができたので、/src/App.tsxに追加します。

return (
  <div>
    <div>
      {/* ↓追加 */}
      <RecordingForm />
      <ul>
        {data?.Items.map((item, index) => (
          <li key={index}>{item.weight}</li>
        ))}
      </ul>
      <p>現在のデータ数:{data?.Count}</p>
    </div>
  </div>
);

別のコンポーネントでデータを利用する

最後に、取得したデータを別のコンポーネントで利用する実装を行います。
取得したデータを使って、グラフを作成したいです。グラフ表示には、Recharts を利用しています。
/src/Graph.tsxGraphコンポーネントを作っていきます。

const Graph = (): JSX.Element => {
  const queryClient: QueryClient = useQueryClient();
  const queryKey: string[] = ['data'];
  const queryData: GetResultDataType | undefined =
    queryClient.getQueryData(queryKey);

  return (
    <div>
      <h2>グラフ</h2>
      {queryData?.Count === 0 ? (
        <p>データがありません。</p>
      ) : (
        <>
          <div>
            <LineChart width={900} height={300} data={queryData?.Items}>
              <CartesianGrid strokeDasharray='3 3' />
              <XAxis
                dataKey='date'
                interval={0}
                angle={-16}
                dx={-20}
                dy={8}
                tick={{
                  fontSize: 10,
                }}
              />
              <YAxis dataKey='weight' domain={[30, 50]} />
              <Tooltip />
              <Line type='monotone' strokeWidth={2} dataKey='weight' />
            </LineChart>
          </div>
          <p>X軸スクロールできます。</p>
        </>
      )}
    </div>
  );
};

export default Graph;

Graphコンポーネントは、useQueryでデータ取得したコンポーネントとは別のコンポーネントになります。そのため、コンポーネントで取得したデータを利用するためには、キャッシュからデータを取り出す必要があります。キャッシュからデータを取り出している部分にフォーカスを当てて見てみます。

const queryClient: QueryClient = useQueryClient();
const queryKey: string[] = ['data'];
const queryData: GetResultDataType | undefined =
  queryClient.getQueryData(queryKey);

まずは、useQueryClientフックで、今のQueryClientを取得しています。QueryClientには、キャッシュの情報も含まれています。
変数queryKeyには、取得したいキャッシュのqueryKeyの配列を代入しています。このqueryKeyは、データ取得時や再取得時にも渡していました。
そして、getQueryDataメソッドを実行し、キャッシュされたデータを取り出して、queryDataに代入しています。
これで、このコンポーネントでも、取得したデータを利用できる状態になりました。

それでは、グラフ実装の部分を簡単に見ていきます。

return (
  <div>
    <h2>グラフ</h2>
    {queryData?.Count === 0 ? (
      <p>データがありません。</p>
    ) : (
      <>
        <div>
          <LineChart width={900} height={300} data={queryData?.Items}>
            <CartesianGrid strokeDasharray='3 3' />
            <XAxis
              dataKey='date'
              interval={0}
              angle={-16}
              dx={-20}
              dy={8}
              tick={{
                fontSize: 10,
              }}
            />
            <YAxis dataKey='weight' domain={[30, 50]} />
            <Tooltip />
            <Line type='monotone' strokeWidth={2} dataKey='weight' />
          </LineChart>
        </div>
        <p>X軸スクロールできます。</p>
      </>
    )}
  </div>
);

キャッシュから取り出したデータqueryDataCountプロパティが0、つまり、DynamoDB のテーブルにデータが何も入っていない場合は、グラフを表示しないので、「データがありません。」と表示するようにしています。
LineChartに、dataが渡されています。このdataに渡されているのが、キャッシュから取り出したデータqueryDataItemsプロパティです。このItemsプロパティには、DynamoDB のテーブルに格納されている項目(データ)が、配列型式で入っています。data属性に表示したいデータを渡すことで、そのデータを使ったグラフを表示することができます。

最後に、Graphコンポーネントができたので、/src/App.tsxに追加します。
グラフでデータが表示できているので、箇条書きで表示していた部分は削除しています。

return (
  <div>
    <div>
      <RecordingForm />
      {/* ↓追加 */}
      <Graph />
      <p>現在のデータ数:{data?.Count}</p>
    </div>
  </div>
);

それでは、いくつかフォームからデータを追加し、ブラウザでの表示を確認してみます。

無事、フォームからのデータ追加とグラフ表示ができているようです 🎉

おわりに

データフェッチライブラリ、TanStack Query をテーマに取り上げてみました。
データ追加を行なった時に再度データ取得を実行してくれることや、Redux・Recoil のような状態管理ライブラリを使わずに別コンポーネントでも利用できることなど、API からデータを取得するような時には色々と便利なライブラリだと感じました!

お読み下さり、ありがとうございました。

参考資料

Tanstack Query 公式ドキュメント

GitHubで編集を提案

Discussion