Next.jsのServer Actionsについて調べてみた
概要
前回、新しいNext.jsのAPIでフォームのデータの送受信について調べてみました。
調べていたら、新機能としてNext.jsの13.4で新たに追加された機能としてServer Actionsという機能があるのをしりました。
まだアルファ版という事ですが、これがまた凄いなという事で調べてみた。
Server Actionsについて
これまでフォームからデータの追加、削除などをNext.jsで実現するには、apiフォルダにAPIを用意してfetch
などでデータを送ったりなどクライアント側で処理をしてました。
ただ、Server Actionsの登場で、フォームの送信やボタンクリックしてfetch
を利用したクライアント側の処理の必要がなくなり、サーバーコンポーネント内でフォームのデータを受けとり、データ更新(作成、更新、削除)がサーバーサイドで実行されるようになる。
この機能を使うには、pages RouterではなApp Routerでないと使えません。
公式サイトはこちら。
Server Actionsを利用する為の設定
Server Actionsを利用するには、next.config.js
にexperimental
の項目にserverActions: true
の設定を追加する必要がある。
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
serverActions:true //これを追加する
},
}
module.exports = nextConfig
Server Actionsのサンプル
まずは余計なコードを省いたシンプルな全体像。
Server Actionsを利用するにはサーバーコンポーネントでないと利用できない点に注意してください。
またServer Actionsを実装するには3つの方法があるらしい。
とりあえず今回は2つの方法は似てるので、2つを紹介。
action属性にサーバーで実行したい関数を指定
まずは1つ目の方法として<form>
タグのaction
属性にサーバーで実行したい関数を指定する。
export default async function Home() {
return (
<form action={addPost}>
<input type='text' name='text' />
<button>送信</button>
</form>
)
}
formAction属性にサーバーで実行したい関数を指定
2つ目の方法として<button>
タグにformAction
属性を追加してサーバーで実行したい関数を指定する方法。
formActionですが、<form>
タグで括られてないと実行されないので注意が必要である。
export default async function Home() {
return (
<form>
<input type='text' name='text' />
<button formAction="addPost">送信</button>
</form>
)
}
Server Actionsの関数
フォームに指定したサーバーで実行したい関数です。
const addPost = async (formData: FormData) => {
"use server"
const textdata = formData.get('text')
console.log(textdata)
}
関数は非同期処理としてasync
を指定します。
指定しないとServer actions must be async functionと表示されます。
またサーバー側で実行するにはuse server
を関数内で宣言する必要がある。
この設定をしないとエラーがでます。
フォームの入力内容は関数の引数から受けてることができる。データの型はFromDataとなる。
変数名はformData
としてるが何でもいい。
このFromData型にget
メソッドがあるので、引数に受け取りたいフォームのname
属性を指定すれば取得できる。
あとは、MySQLやMongoDB、Supabaseなど利用してるデータベースなどに追加するそれぞれのメソッドで追加することができる。
実装
簡単な例としてSupabaseを利用してフォームに入力したテキストの追加と削除、一覧表示をやってみたいと思う。
今回のDEMOはこちら
実装にあたって、以下のディレクトリー構成にする。
- app/page.tsx
- app/libs/supabaseClient.ts
Supabaseの設定もろもろは今回省きます。Supabaseの設定は下記。
-
todos
:テーブル名 -
id
:デフォルトでprimarykeyとして設定されていて、自動的にID番号が振られる -
text
:入力フォームのテキストを保存する -
created_at
:こちらも初期値で自動的に投稿日が保存
Supabaseの設定もろもろ
まずは、Supabseでテーブルなど作成しておきます。
APIの設定からProject URLとProject API keysに記されてるANONが必要になる。(下記画像、赤枠の箇所)
プロジェクトにSupbaseが使えるようにパッケージをインストールします。
npm install @supabase/supabase-js
KeyをもとにSupbaseのデータベースにアクセスするためのフィルをsupbaseClient.ts
として別ファイルにしておきます。
Keyに関しては、ルートディレクトリに.env
ファイルを作成してNEXT_PUBLIC_SUPABASE_URLとNEXT_PUBLIC_SUPABASE_ANON_KEYとして読み込んでます。
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseKey)
このcreateClient
に引数をURLとKeyを渡してsupabase
をエクスポートしておきます。
こちらをインポートする事でSupabaseの追加、削除、更新などのメソッドを利用することができる。今回は追加(insert
)と削除(delete
)メソッドのみ利用する。
全体像
まずはファイルの全体像。
import { Inter } from 'next/font/google'
import { supabase } from './libs/supabaseClient'
import { PostgrestSingleResponse } from '@supabase/supabase-js'
import { revalidatePath } from 'next/cache'
type Todo = {
id: number,
title: string
}
const inter = Inter({ subsets: ['latin'] })
export default async function Home() {
//Supabaseからselect文でデータを読み込む
const todos: PostgrestSingleResponse<Todo[]> = await supabase.from('todos').select("*");
//Supabaseにデータを追加する
const addPost = async (formData: FormData) => {
"use server"
//フォームのデータを取得
const textdata = formData.get('text')
//supabaseにデータを追加
await supabase.from('todos').insert({ text: textdata })
//ページをリロード
revalidatePath('/')
}
//削除ボタンを押したときの処理
const deletePost = async (formData: FormData) => {
"use server"
//削除したいIDを取得
const id = formData.get('id')
try {
//Supabaseのdeleteメソッドで該当するIDを削除する
const { error } = await supabase.from('todos').delete().eq('id', id);
if (error) {
throw error;
}
//ページをリロード
revalidatePath('/')
} catch (error) {
console.error('Error deleting data:', error);
}
}
if (!todos) {
return (
<main>
<form action={addPost}>
<input type='text' name='text' />
<button>送信</button>
</form>
<h2>Todo一覧</h2>
<p>投稿がありません。</p>
</main>
)
}
return (
<main>
<form action={addPost}>
<input type='text' name='text' />
<button>送信</button>
</form>
<h2 >Todo一覧</h2>
//Supabaseから取得した一覧を表示する
<ul>
{todos.data?.map((todo: any) => (
<li key={todo.id} >
<span>{todo.text}</span>
<form>
<input type='hidden' name='id' value={todo.id} />
<button formAction={deletePost}>削除</button>
</form>
</li>
)
)}
</ul>
</main>
)
}
すべてを説明すると長いので、所々説明。
先に作成したsupabaseClient.ts
をインポートする。
Supabaseからselect文でデータを読み込む
まずはSupabaseからデータを取得。サーバーコンポーネントなので、useEffect
とか利用しない。
const todos: PostgrestSingleResponse<Todo[]> = await supabase.from('todos').select("*");
取得したデータがない場合は、「投稿がありません。」としてデータがあった場合は、map()
関数で一覧を出力します。
Supabaseにデータを追加する
今回の目的であるServer Acitonsの設定部分です。
<form>
タグのaction="addPost"
という関数を設定しています。
その関数の部分を抜粋。
const addPost = async (formData: FormData) => {
"use server"
//フォームのデータを取得
const textdata = formData.get('text')
//supabaseにデータを追加
await supabase.from('todos').insert({ text: textdata })
//ページを検証する
revalidatePath('/')
}
ServerActionsの関数内でuse server
を宣言。
formData
から入力されたフォームの値を取得。
Supabaseにinsert
文を利用してデータを追加する。
その後、revalidatePath
でページを検証しています。
こちらについては後述します。
データを削除してみる
削除自体はformAction
で削除するのでdeletePpst
という関数を設定。
各データをSupabaseから削除するにはID番号が必要となるので、一覧表示させるときに<input>
タグのtype="hidden"
にIDを設定しておく。
先に述べたように<form>
タグで括る必要があるので以下のように記述。
<ul>
{todos.data?.map((todo: any) => (
<li key={todo.id} >
<span>{todo.text}</span>
<form>
<input type='hidden' name='id' value={todo.id} />
<button formAction={deletePost}>削除</button>
</form>
</li>
)
)}
</ul>
削除は、Supabaseのdelete
メソッドでIDを指定すればいい。
const deletePost = async (formData: FormData) => {
"use server"
//削除したいIDを取得
const id = formData.get('id')
const { error } = await supabase.from('todos').delete().eq('id', id);
}
ページを更新させる(revalidatePat)
通常、フォームなどを入力してデータを追加した後はブラウザのリロードなどをしないと一覧は更新されない。Supabaseにはデータベースの変更をリアルタイムに検知する事ができる機能もあるが、、ただこれを解決してくれる機能としてrevalidatePath
とrevalidateTag
いうのがある。(今回はrevalidateTag
については省く)
const addPost = async (formData: FormData) => {
"use server"
//ページを検証する
revalidatePath('/')
}
このrevalidatePath
をaddPost
やdeletePost
でのデータの追加・削除が完了した後に追加しておけば、データを検証して更新されていれば最新に反映されるようになる。
引数にはパスを指定するのだがパスに紐づくデータを再検証するようです。
例えば、投稿一覧のページのURLが/posts
としてパスを下記のようにする追加したデータと一覧した場合に更新されるが、関係のないパス、、、/hogehoge
としたら更新されない。
//更新される
revalidatePath('/posts')
//更新されない
revalidatePath('/hogehoge')
ただ、投稿一覧のページのURLが/posts
でパスをルートパスを指定するとルートパス配下は更新される??模様。
//更新される
revalidatePath('/')
他にもrevalidateTag
というのもあります。
詳しくはこちらの記事が参考になりまくります。一部引用させていただくと以下のようです。
Server Actions と revalidatePath revalidateTag を組み合わせると、いま現在表示している画面に対して revalidate に応じた更新を行うことができます。
公式サイトはこちら
参考サイト
今回、2つのパターンを調べてみたが長くなりそうなので、3つ目のパターンは別にします。
Discussion