🍊

Server Actionsを利用してServer Componentsのリストをリフレッシュして表示する(App Router)

2024/03/03に公開

こんにちは!@Ryo54388667です!☺️

普段は都内でフロントエンドエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。

今回はServer Actionsを利用してServer Components(以下 SC)のリストをリフレッシュして表示する方法について紹介したいと思います。

📌 はじめに

行いたいこと

  1. テキストボックスで入力した記事(今回はtitleのみ)をDBに追加する
  2. 表示されている記事タイトル一覧が更新され、常に最新の状態にする
  3. 削除ボタンを押下する
  4. 削除後、表示されている記事タイトル一覧が更新され、常に最新の状態にする

課題

一覧の箇所がClient Components(以下 CC)の場合、実現する方法はいくつもあります。
方法の一つとして提示すると、useSWRを利用して一覧のデータを取得し、記事の追加ボタンの押下時に、マニュアルで再フェッチするもの(mutate)を利用すると実現できます。

https://swr.vercel.app/ja/docs/mutation#revalidation

一方、これをSCで実現する場合、カスタムhookは利用できないので、上記の方法は選択できません。その時、どうするのかなと思って調べたものをまとめてみました!

📌 結論と実装方法

revalidatePathでパスを指定するとリストがリフレッシュされる。

言ってしまえば、上記の一行で済んでしまう話です。。😇
検証に利用したコードを貼っておきます。今回はDBのかわりにmicroCMSを利用しています。使いやすくて助かります〜🙏一部、要点のみ抜粋したものを記載しました!スタイル等も含めたフルのコードはアコーディオンボタンを押していただければ見ることができます。

Package

name version
Next 14.0.0
React 18.2.0
microcms-js-sdk ^2.7.0

ディレクトリ構造

├── app
│   ├── articles
│   │   └── page.tsx
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components
│   ├── AddArticle.tsx
│   ├── Articles.tsx
│   └── DeleteArticle.tsx
├── lib
    ├── actions.ts
    └── microcms.ts

記事を追加し、リフレッシュさせる

記事一覧の実装 (SC)
components/Articles.tsx

//Server Components
export const Articles = async () => {
  const articles = await getArticles();
  return (
    <div>
      <h1>Articles</h1>
      <AddArticle /> {/* 追加ボタン */}
      {articles.contents.map((article: any) => (
        {/* 記事のタイトル */}
        {/* 削除ボタン */}
      ))}
    </div>
  )
}
Articles.tsxの詳細
export const Articles = async () => {
  const articles = await getArticles();
  return (
    <div>
      <h1 className="font-bold">Articles</h1>
      <AddArticle />
      {articles.contents.map((article: any) => (
        <div key={article.id} className="mt-3">
          <div className="flex justify-between">
            <h2>{article.title}</h2>
            <DeleteArticle id={article.id} />
          </div>
          <div className="bg-slate-200 h-0.5" />
        </div>
      ))}
    </div>
  )
}

記事追加ボタンの実装(CC)
components/AddArticle.tsx

"use client"

import { createArticle } from "@/lib/actions"
import { useState, useTransition } from "react"

export const AddArticle = () => {
  const [title, setTitle] = useState("")

  return (
    <div>
      <form action={createArticle}>
        <div>
          <input type="text" id="title" name="title" value={title} onChange={(e) => setTitle(e.target.value)} />
        </div>
        <button type="submit">Add</button>
      </form>
    </div>
  )
}
AddArticle.tsxの詳細
"use client"

import { createArticle } from "@/lib/actions"
import { useState, useTransition } from "react"


export const AddArticle = () => {
  const [title, setTitle] = useState("")
  const [isPending, startTransition] = useTransition();
  const onClick = () => {
    startTransition(() => {
      setTitle("")
    })
  }

  return (
    <div>
      <form className="flex m-2" action={createArticle}>
        <div>
          <input className="border m-1" type="text" id="title" name="title" value={title} onChange={(e) => setTitle(e.target.value)} />
        </div>
        <button className="bg-blue-200 rounded p-0.5 block" type="submit" onClick={onClick}>Add</button>
      </form>
      {isPending ? <p className="h-4 text-slate-400">送信中...</p> : <p className="h-4" />}
    </div>
  )
}

記事追加メソッドの実装(Server Actions)
lib/actions.ts

"use server";
import { revalidatePath } from "next/cache";

export async function createArticle(formData: FormData) {
  const rawFormData = {
    title: formData.get('title'),
  };
  await postArticle(rawFormData);
  revalidatePath("/articles");  // <== これでクライアントサイドのパスを指定する
}

lib/microcms.ts
こちらは記事のスコープ外ですが、参考までに載せておきます。

microcms.tsの詳細
import { createClient } from 'microcms-js-sdk';

const client = createClient({
  serviceDomain: process.env.NEXT_PUBLIC_MICROCMS_SERVICE_DOMAIN || "",
  apiKey: process.env.NEXT_PUBLIC_MICROCMS_API_KEY || "",
});

export const getArticles = async () => {
  return await client.get({ endpoint: 'articles', customRequestInit: { next: { revalidate: 0 } } });
}

export const postArticle = async (data: any) => {
  return await client.create({
    endpoint: 'articles',
    content: {
      title: data.title
    }
  });
}

export const deleteArticle = async (id: string) => {
  return await client.delete({
    endpoint: 'articles',
    contentId: id
  });
}

記事を削除し、リフレッシュさせる

記事削除ボタンの実装(SC)
components/DeleteArticle.tsx

import { deleteArticle } from "@/lib/actions"

export const DeleteArticle = ({ id }: Props) => {
  return (
    <>
    <form action={deleteArticle}>
      <input type="hidden" name="id" value={id} />
      <button type="submit">🗑️</button>
    </form>
    </>
  )
}
DeleteArticle.tsxの詳細
import { deleteArticle } from "@/lib/actions"

type Props = {
  id: string
}

export const DeleteArticle = ({ id }: Props) => {
  return (
    <>
    <form action={deleteArticle}>
      <input type="hidden" name="id" value={id} />
      <button className="cursor-pointer" type="submit">🗑️</button>
    </form>
    </>
  )
}

記事削除メソッドの実装(Server Actions)
lib/actions.ts

"use server";

import { revalidatePath } from "next/cache";
import { postArticle, deleteArticle as _deleteArticle } from "./microcms";

export async function deleteArticle(formData: FormData) {
  const rawFormData = {
    id: formData.get('id') || "",
  };
  await _deleteArticle(rawFormData.id as string)
  revalidatePath("/articles");
}
actions.tsの詳細
"use server";

import { revalidatePath } from "next/cache";
import { postArticle, deleteArticle as _deleteArticle } from "./microcms";

export async function createArticle(formData: FormData) {
  const rawFormData = {
    title: formData.get('title'),
  };
  await postArticle(rawFormData);
  revalidatePath("/articles");
}

export async function deleteArticle(formData: FormData) {
  const rawFormData = {
    id: formData.get('id') || "",
  };
  await _deleteArticle(rawFormData.id as string)
  revalidatePath("/articles");
}

Server Actionsではなくとも、Route Handler (旧 API Routes)経由でキックすることもできます。
https://nextjs.org/docs/app/api-reference/functions/revalidatePath#route-handler

参考
https://nextjs.org/learn/dashboard-app/mutating-data#6-revalidate-and-redirect

📌 まとめ

Server Componentsのリスト一覧を即時更新するには、Server Actions内のメソッドでrevalidatePathでクライアントパスを指定する。そうすると、リストが即時更新され、最新の状態になる。

より良い方法があれば教えてください〜

最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺
https://twitter.com/Ryo54388667/status/1733434994016862256

Discussion