useQueryについて

react-query

・サーバーからのデータ取得回数を減らす(fetch)
 (キャッシュに格納する)
・不要な再レンダリングを無くす

従来のstate管理

●State management(useState, redux store)
・Server data(REST APIから取得したデータ)
・React app state(ローカルのstate、isOpenなどのbooleanなど)
→Reduxのstoreに全てまとめて状態管理されていた

●Reactのアプリケーション性能を高める、状態管理をより分かりやすくするためには Server dataの取り扱いとReact app stateの取り扱いを分けて考える必要がある
React app stateは、従来のReduxやuseContextを使う
Server dataは、キャッシュ機構を活用することができるreact-queryを使用する
キャッシュには他のコンポーネントから自由にアクセスすることができるため、
ReduxやuseContextを使わずサーバーから取得したデータは
react-queryのキャッシュ機構を使って他のコンポーネントから自由にアクセスできる

キャッシュのメカニズムをうまく活用することで
・他のコンポーネントからアクセス可能
・Fetch回数の最適化

●Redux(useContext + useState)
コンポーネントAからfetchでアクセスし取得したデータを
dispatchを使用してstoreに保存
こうすることでコンポーネントBやコンポーネントCから
取得したデータに自由にアクセスできるようになっていた

●React-query
コンポーネントAからREST APIへuseQueryを使用してアクセスする
データを新規作成・更新、削除したりする場合は、useMutationを使用する
取得したデータは自動的にキャッシュに保存してくれる
キャッシュに保存されたデータにコンポーネントBやコンポーネントCなど
他のコンポーネントから自由にアクセスが可能になる
(ReduxやuseContextなどと同じようにグローバルに管理できる)

利点

1、Fetch回数の最適化
従来:
あるコンポーネントにuseEffectを導入
useEffectの中でREST APIにfetchするための処理を記述
例えば、第二引数を空の[ ]とすることで
コンポーネントがマウントされる度にfetchが実行されサーバから最新のデータを取得
別のコンポーネントへページを遷移して戻ってきたとき
再度マウントされるためfetchが毎回実行される

useQuery:
あるコンポーネントにuseQueryを導入
そのコンポーネントがマウントされた時にREST APIにアクセスし
取得したデータをキャッシュの機構に保存
react-queryではstaleTimeというパラメータを設定することができる
これによりfetchの回数を調整することができる
例えばstaleTimeを10秒(staleTime: 10000[ms])に設定した場合、
マウントしてから10秒間は、保存したキャッシュを最新のもの(freshなcache)と見なすようになる
10秒後にキャッシュの内容をstale(古いcache)と見なすようになる
そのため、今回の場合だと10秒間に再度マウントしたとしても
useQueryを実行したとしてもサーバへのfetchを行わないようにすることができる
また、取得するデータのエンドポイント毎にstaleTimeを設定することができる
サーバのデータがほとんど変わらないデータの場合、毎回fetchをするのではなく、
staleTimeを長めにとっておくことで無駄なfetchをなくすことが出来る

2、Better UX by Stale While Revalidation
従来:
あるコンポーネントにuseEffectを導入
マウント時にfetchの処理が実行されREST APIからデータを取得
非同期通信のため、データを取得するまでに少し時間がかかる
データ取得できるまでは何も表示されない
コンポーネントがマウントされる度にloadingのstateがユーザに見えてしまう形になる

useQuery:
あるコンポーネントにuseQueryを導入
初回はキャッシュが存在しないのでREST APIからデータを取得するため
従来と同じようにloadingの画面が表示される
2回目以降マウントされた際は既にキャッシュに何らかの値が保存されているため
キャッシュからデータをすぐに返してくれる
キャッシュのデータが古い可能性もあるがとりあえずキャッシュにあるデータを返してくれる
返している間に最新のデータを評価していく形になる
データが取得でき次第、最新のデータでstale dataを書き換えてくれる

3、コード量を少なくして処理をシンプルにすることが出来る
従来:
・fetch処理
・通信状況処理(データを取得できたか通信が終わったのか、エラーが発生したのかなど)
・グローバルState化(dispatch、useContextを使うなど)

import { useState, useEffect } from "react";
import { useStateContext } from "../context/StateProvider";
import axios from "axios";

export const useClassicFetch = () => {
  const { tasks, setTasks } = useStateContext();
  const { isLoading, setLoading } = useState(false);
  const { isError, setError } = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setError(false);
      setLoading(false);
      try {
        const res = await axios('API');
        setTasks(res.data);
      } catch (error) {
        setError(true);
      }
      setLoading(false);
    };
    fetchData();
  }, [setTasks]);

  return { tasks, isLoading, isError };
};

取得したデータをuseContextを使って
グローバル化する必要があるためProviderの設定が必要

import { useContext, useState, createContext } from "react";
import { Tasks } from "../types/types";

interface StateContextType {
  tasks: Task[] | null;
  dark: boolean;
  setTasks: React.Dispatch<React.SetStateAction<Task[] | null>>;
  setDark: React.Dispatch<React.SetStateAction<boolean>>;
}

const StateContext = createContext({} as StateContextType);

export const StateProvider: React.FC = ({ children }) => {
  const [ tasks, setTasks ] = useState<Task[] | null>(null);
  const [ dark, setDark ] = useState(false);

  return (
    <StateContext.Provider value={{ tasks, setTasks, dark, setDark }}>
      {children}
    </StateContext.Provider>
  )
}
export const useStateContext = (): StateContextType => useContext(StateContext)

useQuery:
下記で上記のようなことと同じようなことが出来る

const { isLoading, error, data } = useQuery('repoDate', () =>
  fetch('API').then(res =>
    res.json()
  )
)

Next.jsを使用する場合は、SWR(Vercel)を採用する方が良いかもしれない。
APOLLOクライアントは標準でキャッシュ機構が備わっている。