🤍

【Next.js】Server Actionsを現場で使うテクニック

2023/12/03に公開
2

この記事は?

著者はエンジニアとしてAWS環境でのNextJSを用いた新規開発を行ってきました。その中で、ServerActionsは目を引く新技術です。本記事ではServerActionsを現場で使うテクニックを紹介します。既にAmplifyなどでも対応している内容なのでご気軽にお試しいただけます。

ServerActionsは何を解決するのか?

ServerActionsでは、form要素のactionに'use server'をつけた関数を渡すことで、JavaScriptでなくHTMLの機能だけを用いてサーバーにデータを送信することを可能にします。これによって、クライアンド側でJavaScriptがDOM処理を介する際に発生する、所謂ハイドレーションを待たずにユーザーが画面操作可能となるメリットを享受できることはServerActionsの1つの特徴です。何よりNext.jsがServerActionsに関して公式で挙げている核となるメリットとして、プログロレッシブ•エンハンスメントを可能にする観点はユーザー体験に大きく寄与する大事な観点と言えます。

ServerActionsについてよくある誤解

以下に示すような誤解は著者が実際に見聞きしたような誤解で、こういった誤解を見聞きして感じることは、誤解を解くためには自身で実際に試してみることが重要であることです。前述の通り、App RouterもNext14に対応しているため本番環境でも容易にテストが可能です。

❌Server Actionsはサーバーコンポーネントでしか使えない

→Client ComponentでもServerActionsを使用することはできる。

❌Server ActionsはAPI routesを完全に代替するものである

→それぞれServerActionsとAPI routesでユースケースは異なるので併用して使う。Server Actionsではactionの付いた要素の送信がトリガーとなるので、例えば画面遷移時に処理を走らせたい場合(例:SMS経由でのアカウント認証でユーザーが画面を開く際に、それをトリガーに認証処理を行う)などはServerActionsでなくAPI Routesを使った実装が適していると言えるでしょう。

❌Server Actionsを使えばクライアント側での状態管理は必要ない

→実際の開発では、useStateとServerActionsで状態を管理させる方法の2種類を使って開発を行います。これは前述の通り、ServerActionsは実装の手段として追加された一つの手段であって、既存の開発手法の全てを完全に覆すようなものではないからです。また、ServerActionsを使う代表的な例であるフォーム実装では、エラー表示が欠かせないため、その際はuseFormStateなどを用いてエラー情報などをクライアント側と同期させるようにします(実装詳細は後述)。

ServerActionsの使いどき

前述の通り、ServerActionsは既存のAPI routesなどの実装方式を完全に塗り替えるものではなく、共存して使っていける機能です。ここでは、よくある例としてフォームでユーザーがメールアドレス、パスワードを入力(use client) し、DBとの通信を行なってセッションをセットする(use server) までをServer Actionsで実装する例を挙げて理解を深めてみたいと思います。

・Server Actions側
Server Actionsの関数を宣言するには、文頭に'use server'をつけて宣言します。次に、引数にはクライアントサイドに表示させたいエラー情報などを含むstateと、FormData型の変数formDataを宣言します。このformDataにはビュー側のform内に囲ったinput要素にid, nameをキーとして付けて渡すことによって値が渡ってくるので、Server Actions側で使用することが可能です。

フォームのバリデーションにはzodというライブラリを使うと便利です。通常のAPI fetchの場合と同様に、非同期処理はtry catchをつけてエラーハンドリングを行いますが、returnとしてerrorを返しているのがミソで、それぞれ後述するビュー側で表示することができるものです。

export async function signin(state: State, formData: FormData) {
    'use server'
    const schema = {
        email: z.string().regex(EMAIL_REGEX, {
                message:
                    'email address is invalid'
            }),
        password: z
            .string()
            .regex(PASSWORD_REGEX, {
                message:
                    'password is invalid'
            })
    }

    const validatedFields = z.object(schema).safeParse(Object.fromEntries(formData))

    if (!validatedFields.success) {
        return { validatedErrors: validatedFields.error.flatten().fieldErrors }
    }

    const { email, password } = validatedFields.data

    try {
        const user = await prisma.user.findUnique({
            where: { email }
        })
    } catch (e) {
        return { error: 'user is not found' }
    }

    try {
        const session = await setSession(user.id)
        redirect('/')
    } catch (e) {
        return { error: 'session is not created' }
    }
}

・ビュー(クライアントコンポーネント)側
ビュー側ではstateを持つため、'use client'ファイルにコードを記述していきますが、useFormStateに第一引数としてsigninを与え、state, dispatchとすることで、stateはserver actions関数から返される状態、dispatchはformにactionとして渡す関数を得ることができます。server actionsを使うには、全体をformで囲ったのち、input要素にそれぞれキーとなるid, nameを明示し、button要素にはsubmitを付けて送信を明示することによって実装することができます。

export default function SignIn() {
    const [state, dispatch] = useFormState(signin, initialState)

    return (
        <form action={dispatch}>
            <div className="flex-col">
                <input
                    label="メールアドレス"
                    id="email"
                    name="email"
                    type="email"
                    autoComplete="email"
                    placeholder="メールアドレス"
                />

                <input
                    label="パスワード"
                    id="password"
                    name="password"
                    placeholder="パスワード"
                />
                <SubmitButton type="submit" />
                <ErrorMessages error={state.error} validationErrors={state.validationErrors} />
            </div>
	</form>

おまけ(hidden属性

また、明示的に画面に入力部分を表示させたくないが、serverActions内で処理するパラメータとして使用したいものがある場合、input要素にhiddenを付けて実装することが可能です。

 <form action="dispatch">
        <input type="hidden" name="a" defaultValue={a} />
        <input type="hidden" name="b" defaultValue={d} />
	<button type="submit" />
</form>

Discussion

ishikoishiko

私はあなたの素晴らしい記事を読みました。zhihuであなたの記事を共有してもよろしいですか?許可をいただけますか?
お返事お待ちしております。よろしくお願いいたします。

rión_devopsrión_devops

コメントありがとうございます。記事に関して、出典を示していただければ問題ないです!