React Queryを使ったキャッシュ更新によるステート更新

記事の概要

フロントエンドの開発をやっていると、「バックエンドのデータ更新した後のフロントのStateの更新どうしよう…?」って思うことありませんか?私はあります。めっちゃあります。そして気がついたら「あっちのページでは更新後に再フェッチしてるのに、こっちのページはフロントだけでState書き換えてるじゃん・・・」みたいなことになります 😇

最近のフロントエンドのデータフェッチング系ライブラリは、この辺の問題に解決策を与えてくれるようなので、この記事ではそれを実践してみたいと思います。気持ちよく開発できることを期待!!

目的

  • 最近のフロントエンド開発は面白いね!って思って欲しい
  • フロントエンドのデータの取り回しに苦労している人がいたら参考にして欲しい
  • 便利な機能がもっと世に広まって欲しい

この記事はこういう方にオススメ

  • フロントエンド開発に興味がある・している人
  • reactを使っている・使おうとしている人
  • ネットオンのフロントエンド開発に興味がある人 🔥

使用する技術

以下、今回使用する技術スタックです。Reactでもよかったのですが、バックエンドとのやりとりが発生するので、Next.jsでバックエンドもまとめて実装します。TypeScriptは。。。。もうないと生きていけません。React Queryは今回のメインになる、データフェッチライブラリです。

  • Next.js
  • TypeScript
  • React Query

最終的に作りたいもの

まずは準備していく

プロジェクト作成

最初に環境を準備していきます。Next.jsの環境をゼロから作っていきます。create-next-appで一撃!プロジェクト名はneton-blogにしました。

npx create-next-app --typescript

... (いろいろログが出る)


cd neton-blog && npm run dev

ページの作成

最初に出来上がるサンプルファイルたちはもうそのまま置いておいて、新しくページを作成します。Next.jsはダイナミックルーティングなのでファイルを作成するだけで、新しいルーティングが追加されます。

ということで。pages/task.tsxに以下のファイルを作成します。npm run buildで起動して、http://localhost:3000/taskにアクセスすると、タスク一覧ページが表示されます。

import React from "react";

/* タスク一覧ページ */
const Task: React.FC = () => {
  const tasks = [
    { id: 1, name: "翌日の仕事の段取りをする", status: "未" },
    { id: 2, name: "LTの資料を作成する", status: "未" },
    { id: 3, name: "ブログを書く", status: "済" },
    { id: 4, name: "このブログに応援コメントを書く", status: "未" },
  ];
  return (
    <div>
      <h1>タスク一覧</h1>
      <table>
        <thead>
          <tr>
            <th>ID</th>
            <th>なまえ</th>
            <th>状態</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {tasks.map((task) => (
            <tr key={task.id}>
              <td>{task.id}</td>
              <td>{task.name}</td>
              <td>{task.status}</td>
              <td>
                <button>Done!</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default Task;

React Queryを使ってAPIからデータを取得

先程のページはテストデータをページの中に記述していました。この部分を少し現実味のある実装に置き換えていきます。実際に外部のデータにアクセスするのも面倒なので、next.jsのAPI Serverを利用して実装していきます。

データを取得するロジック自体は本筋ではないので、サラッと以下のように記述していきます。data/tasks.jsonにデータを配置して、src/pages/api/tasks.tsからこのJSONファイルにアクセスします。

[
  { "id": 1, "name": "翌日の仕事の段取りをする", "status": "未" },
  { "id": 2, "name": "LTの資料を作成する", "status": "未" },
  { "id": 3, "name": "ブログを書く", "status": "済" },
  { "id": 4, "name": "このブログに応援コメントを書く", "status": "未" }
]
import { NextApiRequest, NextApiResponse } from 'next'
import tasks from '../../data/tasks.json'

export type Task = {
  id: number,
  name: string,
  status: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Task[]>
) {
  res.status(200).json(tasks)
}

次に、先程のpages/task.tsxの中から、タスクのデータを取得するように変更します。

-import React from "react";
+import React, { useState } from "react";
+import { Task } from './api/tasks'

 /* タスク一覧ページ */
 const Task: React.FC = () => {
-  const tasks = [
-    { id: 1, name: "翌日の仕事の段取りをする", status: "未" },
-    { id: 2, name: "LTの資料を作成する", status: "未" },
-    { id: 3, name: "ブログを書く", status: "済" },
-    { id: 4, name: "このブログに応援コメントを書く", status: "未" },
-  ];
+  const [tasks, setTasks] = useState<Task[]>()
+  fetch("/api/tasks", { method: "GET" }).then(async (response) => {
+    const data = await response.json()
+    setTasks(data)
+  })
+
   return (
     <div>
       <h1>タスク一覧</h1>
@@ -21,7 +22,7 @@ const Task: React.FC = () => {
           </tr>
         </thead>
         <tbody>
-          {tasks.map((task) => (
+          {tasks?.map((task) => (
             <tr key={task.id}>
               <td>{task.id}</td>
               <td>{task.name}</td>

そしてここにreacty-queryを被せていきます。まずはインストールです。

npm install react-query

次に、先程のデータ取得部分を非同期関数として切り出して、React QueryのuseQueryの引数に渡します。React Queryでは、非同期関数の実行値をラッピングして、戻り値をキャッシュに保存することができます。また、useQueryを利用する場合は、親階層でQueryClientProviderタグで囲っておく必要があります。Contextを利用してClientを供給していくためです。

 import "../styles/globals.css";
 import type { AppProps } from "next/app";
+import { QueryClientProvider, QueryClient } from "react-query";
 
 function MyApp({ Component, pageProps }: AppProps) {
-  return <Component {...pageProps} />;
+  const client = new QueryClient();
+  return (
+    <QueryClientProvider client={client}>
+      <Component {...pageProps} />;
+    </QueryClientProvider>
+  );
 }
 
 export default MyApp;
 import React, { useState } from "react";
-import { Task } from './api/tasks'
+import { useQuery } from "react-query";
+import { Task } from "./api/tasks";
+
+/* 非同期でタスクを取得する関数 */
+const fetchTasks: () => Promise<Task[]> = async () => {
+  const response = await fetch("/api/tasks", { method: "GET" });
+  return await response.json();
+};
 
 /* タスク一覧ページ */
 const Task: React.FC = () => {
-  const [tasks, setTasks] = useState<Task[]>()
-  fetch("/api/tasks", { method: "GET" }).then(async (response) => {
-    const data = await response.json()
-    setTasks(data)
-  })
-
+  // useQueryの第一引数はキャッシュデータのキーとして利用される
+  const { data: tasks, isLoading } = useQuery("tasks", fetchTasks);
+  if (isLoading) return <h1>Loading...</h1>;
   return (
     <div>
       <h1>タスク一覧</h1>

useQueryの戻り値にはデータ以外にも、isLoadingのような非同期関数の実行状態を取得するための値も用意されています。今回、試しにisLoadingを入れてみました。こういう小回りを効かせるのが簡単にできてよいですね。

※ わかりやすくするために上記画像ではLoading状態で1秒待機させています

タスクの状態更新のAPIを実装

タスク一覧ページにはDone!ボタンがありますが、何も処理を実装していません。このボタンをクリックした際に、データを更新するようにしていきます。src/pages/done.tsにJSONファイルを更新する処理を書いて、クリックイベントで発火するようにします。

import { NextApiRequest, NextApiResponse } from "next";
import tasks from "../../data/tasks.json";
import * as fs from 'fs'
import * as path from 'path'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const { id } = req.body;
  const savedTasks = tasks.map((task) => {
    if (task.id === id) task.status = "済";
    return task
  });
  // とても雑だけど..
  fs.writeFileSync(path.resolve(__dirname, '../../../../data/tasks.json'), JSON.stringify(savedTasks))
  res.status(200).json("OK")
}
@@ -8,6 +8,14 @@ const fetchTasks: () => Promise<Task[]> = async () => {
   return await response.json();
 };
 
+const doneTask = async (taskId: Task["id"]) => {
+  await fetch("/api/done", {
+    method: "POST",
+    body: JSON.stringify({ id: taskId }),
+    headers: { "Content-Type": "application/json" },
+  });
+};
+
 /* タスク一覧ページ */
 const Task: React.FC = () => {
   const { data: tasks, isLoading } = useQuery("tasks", fetchTasks);
@@ -31,7 +39,7 @@ const Task: React.FC = () => {
               <td>{task.name}</td>
               <td>{task.status}</td>
               <td>
-                <button onClick={() => mutation.mutate(task.id)}>Done!</button>
+                <button onClick={() => doneTask(task.id)}>Done!</button>
               </td>
             </tr>
           ))}

これでDone!ボタンを押下すると、JSONファイルが更新されるようになりました。しかし、画面のタスク一覧には変化はありません。コンポーネント内部のStateを更新していないので、仕方ありません。

React Queryを使ってキャッシュ更新 —> コンポーネントState更新

ようやく、この記事で書きたかった点に辿り着きました…. React Queryを利用すると、以下のことが簡単に実現できます。早速実装していってみます。pages/tasks.tsxに改修を加えます。

  • useMutationを使って、POST成功時にコールバック関数を実行
  • invalidateQueriesを使って、キャッシュの更新
diff --git a/pages/task.tsx b/pages/task.tsx
index 730af2c..90fedb1 100644
--- a/pages/task.tsx
+++ b/pages/task.tsx
@@ -18,7 +18,13 @@ const doneTask = async (taskId: Task["id"]) => {
 
 /* タスク一覧ページ */
 const Task: React.FC = () => {
+  const queryClient = useQueryClient();
   const { data: tasks, isLoading } = useQuery("tasks", fetchTasks);
+  const mutation = useMutation(doneTask, {
+    onSuccess: () => {
+      queryClient.invalidateQueries("tasks");
+    },
+  });
   if (isLoading) return <h1>Loading...</h1>;
   return (
     <div>
@@ -39,7 +45,7 @@ const Task: React.FC = () => {
               <td>{task.name}</td>
               <td>{task.status}</td>
               <td>
-                <button onClick={() => doneTask(task.id)}>Done!</button>
+                <button onClick={() => mutation.mutate(task.id)}>Done!</button>
               </td>
             </tr>
           ))}

invalidateQueriesを利用すると、「このキャッシュの情報が正しくなくなった(invalidate)」と教えることができます。それを受けて、React Queryはもう一度クエリを実行して、キャッシュを最新の値に書き換えてくれます。そしてこのキャッシュの値をコンポーネントのStateとして利用できているので、画面上でも変化が発生します。この連動感、気持ちいい!!

さいごに

今回はReact Queryを使って、「バックエンドのデータ更新した後のフロントのStateの更新どうしよう…?」問題に対応してみました。React Queryを使うと、invalidateQueriesを利用したデータ再取得だけでなく、キャッシュの値を直接更新するような方法もあったり、幅が広いです。一定時間経過後にデータ再取得を実施するポーリングのような処理も簡単に実装できます。メジャーなライブラリなので活用されている方も多いかと思いますが、まだ使ったことのない方も是非、使ってみてください!