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. 参考
- Docs | Next.js (nextjs.org)
- Quick Start – React
- Getting Started with Redux | Redux
- Getting Started with React Redux | React Redux (react-redux.js.org)
- Material UI: React components based on Material Design (mui.com)
投稿者プロフィール
-
開発好きなシステムエンジニアです。
卓球にハマってます。