【NextJS】Server Actions with MySQL

1. 概要

前回はSuspenseの使い方についてでした。今回はServer Actionsの使い方についてです。

API endpointsを作成せずClient側のForm actionから直接Server側のFunctionを呼び出す内容になります。

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

2. nodeのインストール

こちらを参考

3. プロジェクトを作成

こちらを参考

4. 必要なライブラリをインストール

こちらを参考

npm install mysql2

5. ソースコード

※前回より差分のみを記載

5-1-1. next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    serverActions: true,
  },
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.dog.ceo",
        port: "",
        pathname: "/breeds/**",
      },
    ],
  },
};

module.exports = nextConfig;

5-1-2. .env.local

DB_HOST=localhost
DB_USER=user
DB_PASSWORD=password
DB_NAME=test

5-1-3. src/lib/common/db/pool-config.ts

const poolConfig = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  connectionLimit: 10,
  maxIdle: 10, // max idle connections, the default value is the same as `connectionLimit`
  idleTimeout: 60000, // idle connections timeout, in milliseconds, the default value 60000
  queueLimit: 0,
  enableKeepAlive: true,
  keepAliveInitialDelay: 0,
};

export default poolConfig;

5-1-4. src/lib/common/db/pool.ts

import * as Mysql from "mysql2/promise";
import poolConfig from "./pool-config";

type MyResultSet =
  | Mysql.RowDataPacket[][]
  | Mysql.RowDataPacket[]
  | Mysql.OkPacket
  | Mysql.OkPacket[]
  | Mysql.ResultSetHeader;

const pool: Mysql.Pool = Mysql.createPool(poolConfig);

const SelectQuery = async <T extends MyResultSet>(
  sql: string,
  param: any[] = []
): Promise<T extends MyResultSet ? T : MyResultSet> => {
  try {
    const [rows] = await pool.query<T extends MyResultSet ? T : MyResultSet>(
      sql,
      param
    );
    return Promise.resolve(rows);
  } catch (error) {
    console.error(JSON.stringify(error));
    return Promise.reject(error);
  }
};

const UpdateQuery = async (sqls: string[], params: (string | number)[][]) => {
  const connection: Mysql.PoolConnection = await pool.getConnection();

  try {
    await connection.beginTransaction();

    for (let i = 0; i < sqls.length; i++) {
      const sql: string = sqls[i];
      const param: (string | number)[] = params[i];
      await connection.execute(sql, param);
    }

    await connection.commit();
  } catch (err) {
    console.error(err);
    await connection.rollback();
  } finally {
    await pool.releaseConnection(connection);
  }
};

export { SelectQuery, UpdateQuery };

5-1-5. src/app/nextjs/nextjs04/actions.ts

"use server";

import { revalidatePath } from "next/cache";
import { RowDataPacket } from "mysql2";
import { UpdateQuery, SelectQuery } from "@/lib/common/db/pool";

export interface ISample extends RowDataPacket {
  id: number;
  name: string;
}

const registerAction = async (formData: FormData) => {
  const name: FormDataEntryValue | null = formData.get("name");
  const sql1 = `INSERT INTO sample (name) VALUES (?)`;
  const sql2 = `UPDATE sample SET name = concat(name, ?) WHERE id = last_insert_id()`;
  const sqls: string[] = [sql1, sql2];
  const params: (string | number)[][] = [[name!.toString()], [" changed"]];
  await UpdateQuery(sqls, params);
  revalidatePath("/");
};

const selectAction = async () => {
  const sql: string = "SELECT id, name FROM sample ORDER BY id DESC LIMIT ?";
  return await SelectQuery<ISample[]>(sql, [5]);
};

export { registerAction, selectAction };

5-1-6. src/app/nextjs/nextjs04/register-form.tsx

"use client";

import { useRef } from "react";
import { experimental_useFormStatus as useFormStatus } from "react-dom";

import TextField from "@mui/material/TextField";
import { Button } from "@mui/material";
import SendIcon from "@mui/icons-material/Send";

import { registerAction } from "./actions";

const RegisterForm = () => {
  const formRef = useRef<HTMLFormElement>(null);
  const { pending } = useFormStatus();

  return (
    <form
      ref={formRef}
      action={async (formData) => {
        await registerAction(formData);
        formRef.current!.reset();
      }}
    >
      <TextField name="name" fullWidth label="Hello" color="secondary" />
      <Button
        type="submit"
        disabled={pending}
        variant="contained"
        endIcon={<SendIcon />}
      >
        Register
      </Button>
      {pending && <p>Submitting...</p>}
    </form>
  );
};

export default RegisterForm;

5-1-7. src/app/nextjs/nextjs04/list.tsx

import { ISample, selectAction } from "./actions";

const List = async () => {
  const list: ISample[] = await selectAction();

  return (
    <ul>
      {list.map((row: ISample) => (
        <li key={row.id}>{row.name}</li>
      ))}
    </ul>
  );
};

export default List;

5-1-8. src/app/nextjs/nextjs04/page.module.scss

.component {
  color: blue;
  & ul {
    margin-left: 20px;
    & li {
      list-style: disc;
    }
  }
}

5-1-9. src/app/nextjs/nextjs04/page.tsx

import Box from "@mui/material/Box";

import GoBack from "@/lib/components/go-back";
import scss from "./page.module.scss";
import RegisterForm from "./register-form";
import List from "./list";

const Nextjs04 = () => {
  return (
    <div className={scss.component}>
      <GoBack />
      <br />
      <br />
      <ul>
        <li>Server Actions</li>
        <ul>
          <li>No endpoints</li>
        </ul>
      </ul>
      <br />
      <Box
        sx={{
          width: "100%",
          p: 2,
          border: "1px dashed grey",
          borderRadius: "20px",
          "&:hover": {
            backgroundColor: "pink",
            opacity: [0.9, 0.8, 0.7],
          },
        }}
      >
        <RegisterForm />
      </Box>
      <br />
      <ul>
        <li>Data list</li>
        <List />
      </ul>
    </div>
  );
};

export default Nextjs04;

5-1-10. src/app/nextjs/page.tsx

"use client";

import React from "react";
import { Link } from "@mui/material";
import scss from "./page.module.scss";

const Nextjs = () => {
  return (
    <div className={scss.components}>
      <ul>
        <li>
          <Link href="/nextjs/nextjs01" underline="hover">
            Nextjs01
          </Link>
        </li>
        <li>
          <Link href="/nextjs/nextjs02" underline="hover">
            Nextjs02
          </Link>
        </li>
        <li>
          <Link href="/nextjs/nextjs03" underline="hover">
            Nextjs03
          </Link>
        </li>
        <li>
          <Link href="/nextjs/nextjs04" underline="hover">
            Nextjs04
          </Link>
        </li>
      </ul>
    </div>
  );
};

export default Nextjs;

6. サーバーを起動

npm run dev

※MySQLを起動

7. ブラウザで確認

  • http://localhost:3000

8. ディレクトリの構造

.
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│   ├── js
│   │   └── script.js
│   ├── next.svg
│   └── vercel.svg
├── src
│   ├── app
│   │   ├── components
│   │   │   ├── component01
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── component02
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── component03
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── component04
│   │   │   │   ├── checkbox-demo.tsx
│   │   │   │   ├── page.module.scss
│   │   │   │   ├── page.tsx
│   │   │   │   ├── radio-demo.tsx
│   │   │   │   └── select-demo.tsx
│   │   │   ├── page.module.scss
│   │   │   └── page.tsx
│   │   ├── events
│   │   │   ├── event01
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── page.module.scss
│   │   │   └── page.tsx
│   │   ├── favicon.ico
│   │   ├── globals.css
│   │   ├── globals.scss
│   │   ├── hooks
│   │   │   ├── hook01
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── hook02
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── hook03
│   │   │   │   ├── child.tsx
│   │   │   │   ├── counter-provider.tsx
│   │   │   │   ├── grandchild.tsx
│   │   │   │   ├── myself.tsx
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── hook04
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── hook05
│   │   │   │   ├── child.tsx
│   │   │   │   ├── counter-provider.tsx
│   │   │   │   ├── grandchild.tsx
│   │   │   │   ├── myself.tsx
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── hook06
│   │   │   │   ├── child.tsx
│   │   │   │   ├── page.module.scss
│   │   │   │   ├── page.tsx
│   │   │   │   ├── play-provider.tsx
│   │   │   │   ├── text-box.tsx
│   │   │   │   └── video-player.tsx
│   │   │   ├── page.module.scss
│   │   │   └── page.tsx
│   │   ├── layout.module.scss
│   │   ├── layout.tsx
│   │   ├── nextjs
│   │   │   ├── nextjs01
│   │   │   │   ├── child
│   │   │   │   │   ├── client-page.tsx
│   │   │   │   │   ├── metadata.ts
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── nextjs02
│   │   │   │   ├── [...slug]
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── page.module.scss
│   │   │   │   ├── page.tsx
│   │   │   │   └── shop
│   │   │   │       └── [id]
│   │   │   │           └── page.tsx
│   │   │   ├── nextjs03
│   │   │   │   ├── fetch-image.tsx
│   │   │   │   ├── get-image.tsx
│   │   │   │   ├── loading.tsx
│   │   │   │   ├── page.module.scss
│   │   │   │   └── page.tsx
│   │   │   ├── nextjs04
│   │   │   │   ├── actions.ts
│   │   │   │   ├── list.tsx
│   │   │   │   ├── page.module.scss
│   │   │   │   ├── page.tsx
│   │   │   │   └── register-form.tsx
│   │   │   ├── page.module.scss
│   │   │   └── page.tsx
│   │   ├── page.module.scss
│   │   ├── page.tsx
│   │   └── redux
│   │       ├── page.module.scss
│   │       ├── page.tsx
│   │       ├── redux01
│   │       │   ├── child.tsx
│   │       │   ├── counter-slice.ts
│   │       │   ├── grandchild.tsx
│   │       │   ├── hooks.ts
│   │       │   ├── myself.tsx
│   │       │   ├── page.module.scss
│   │       │   ├── page.tsx
│   │       │   └── store.ts
│   │       └── redux02
│   │           ├── child.tsx
│   │           ├── hooks.ts
│   │           ├── image-box.tsx
│   │           ├── image-slice.ts
│   │           ├── page.module.scss
│   │           ├── page.tsx
│   │           ├── store.ts
│   │           ├── text-box.tsx
│   │           └── text-slice.ts
│   ├── lib
│   │   ├── common
│   │   │   ├── db
│   │   │   │   ├── pool-config.ts
│   │   │   │   └── pool.ts
│   │   │   ├── definitions.ts
│   │   │   └── sidebar-links.tsx
│   │   ├── components
│   │   │   ├── alert-snackbar.tsx
│   │   │   ├── go-back.tsx
│   │   │   └── spacer.tsx
│   │   ├── footer.tsx
│   │   ├── header.tsx
│   │   ├── sidebar.tsx
│   │   └── utils
│   │       └── util.ts
│   └── scss
│       └── common
│           ├── _index.scss
│           ├── _mixin.scss
│           ├── _mq.scss
│           └── _variables.scss
├── tailwind.config.ts
└── tsconfig.json

37 directories, 116 files

9. 備考

今回はServer Actionsの使い方についてでした。

10. 参考

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. 【NextJS】Metadata(Head,Title)・Script…

  2. 【NextJS】Error Handling

  3. 【NextJS】Canvasを使い、図形を描画

  4. 【NextJS】Redux

  5. 【NextJS】TextField

  6. 【NextJS】Events

最近の記事

制作実績一覧

  1. Checkeys