【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】Cognito with Amplify(Gen2)+…

  2. 【NextJS】TextField

  3. 【NextJS】Checkbox・Radio・Select

  4. 【NextJS】Redux-createAsyncThunk

  5. 【NextJS】AWS SAMを使いCLIでデプロイしたLambda関…

  6. 【NextJS】OAuth authentication with G…

最近の記事

  1. raspberrypi

制作実績一覧

  1. Checkeys