ottijp blog

Next.jsのServer Actionsでサーバサイドのメソッドをクライアントコンポーネントから透過的に呼び出す

  • 2025-05-28

環境

  • Next.js: 15.3
  • Node.js: 22.16

モチベーション

Webアプリケーションにおいて,テキストフィールドに入力された値に応じて,サーバで検索されたデータを動的に画面に表示する実装をしたいと考えました. こんな感じです.

この際,WebAPIを作らずに,サーバサイドで行ったデータフェッチの結果を透過的にクライアントで描画したいです.

はじめはRSC (React Server Components) を使うことを考えましたが,以下の理由により諦めました.

  • 動的なクライアントコンポーネントにimportする形でRSCを使うことはできない.(クライアントコンポーネントを動的にしたいので,この方法この方法はたぶんNG.)
  • クエリパラメタにクエリをセットしてRouterでページ遷移することで実現はできるが,クエリパラメタにクエリは入れたくない.

そこで,Server Actionsを使って実装してみました.

cf. Data Fetching: Server Actions and Mutations | Next.js

実装

コード全体はこちらにあります.

ottijp/nextjs-server-actions-demo

データベースもどき

/usr/share/dict/wordsからクエリ文字列でgrepする以下のようなデータベースもどきを用意しました.

lib/repository.js
import util from 'util'
import { execFile } from 'child_process'
const execFileAsync = util.promisify(execFile)

export async function searchWords(query) {
  if (!query || query.trim() === '') {
    return []
  }
  // /usr/share/dict/words からqueryにマッチする単語をで検索して最大100件を返す
  const result = await execFileAsync('grep', ['-i', '-m', '100', query, '/usr/share/dict/words'], { encoding: 'utf-8' })
  return result.stdout.split('\n').filter(word => word.trim() !== '')
}

サーバアクション

これを使うサーバアクションがこちらです. use serverとすることでサーバサイドで実行されます.(console.log()の出力はサーバ側に出力されます.)

app/search/actions.js
'use server'

import { searchWords } from '../../lib/repository.js'

export async function searchAction(query) {
  // データベースを検索して結果を返す
  console.log(`Searching for: ${query}`)
  return await searchWords(query)
}

ページコンポーネント

ページのコンポーネントは次のようにしました. use clientとすることで,クライアントサイドで実行されonChageなどが使えるようになります.

app/search/page.js
'use client'

import React, { useState } from "react"
import { searchAction } from './actions'
import styles from './page.module.css'

export default function Page() {

  const [query, setQuery] = useState('')
  const [result, setResult] = useState([])
  const onQueryChange = async (event) => {
    console.log(`Query changed: ${event.target.value}`)
    setQuery(event.target.value)
    // サーバアクションを呼び出して検索結果を更新
    setResult(await searchAction(event.target.value))
  }

  return (
    <div>
      <input value={ query } onChange={ onQueryChange } placeholder="Type something..." />
      <ul>
        {result.map((word, index) => (
          <li key={index} className={ styles.resultList }>{word}</li>
        ))}
      </ul>
    </div>
  )
}

Server Actionsの理解

テキストフィールドに文字を入力するたびに,次のようなやり取りがクライアントとサーバで行われます. Next-Actionヘッダでターゲットのサーバアクションを特定し,リクエストボディで引数を渡しレスポンスボディで返り値を渡すことでRPCしているようです.

リクエスト
curl 'http://localhost:3000/search' \
  -H 'Accept: text/x-component' \
  -H 'Accept-Language: ja,en-US;q=0.9,en;q=0.8' \
  -H 'Connection: keep-alive' \
  -H 'Content-Type: text/plain;charset=UTF-8' \
  -b '__next_hmr_refresh_hash__=162' \
  -H 'Next-Action: 40ecdafa8564c6c94999f03d6b2cccf83c2ae25f17' \
  -H 'Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22search%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2C%22%2Fsearch%22%2C%22refresh%22%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D' \
  -H 'Origin: http://localhost:3000' \
  -H 'Referer: http://localhost:3000/search' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: same-origin' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --data-raw '["japane"]'
レスポンス
0:{"a":"$@1","f":"","b":"development"}
1:["Japanee","Japanese","Japanesque","Japanesquely","Japanesquery","Japanesy"]

注意点

RSCと違い,初期ロード時はServer Actionsが走った結果が返るわけではないので,SEO対策に有効ではないです. また,Server ActionsはNext.jsの内部用なので公開用WebAPIとしては使えません.


© 2025, ottijp