NextjsのSever Actionsを掲示板のCRUDで学ぶ


今回簡易的な掲示板アプリを作成して、投稿のCRUD機能を実装することによってNext.js14で正式リリースされたServer Actions機能のキャッチアップを行いましたので学んだことをアウトプットしたいと思います。

#### ブログをまとめる際にコードが長くなりすぎてしまうので、省略して記載しています。

## 目次
[使用技術](#使用技術)
[Server Actionsとは](#server-actionsとは)
[CRUD処理](#crud処理)
[今後](#今後)
[参考文献](#参考文献)

## 使用技術
Typescript
Nextjs
supabase
Prisma
yarn

## Server Actionsとは
formの送信の際にfetch関数などを利用してAPIエンドポイントを経由することなく、サーバ上でデータの更新を行うことができる機能です。
これによりブラウザで読み込むJavaScriptコードを削減することができます。
JavaScriptが使用できない環境下でもフォームの送信を行うことができます。

### API RoutesとSeverActionsの違い
Server Actionsを使用すると
```
	メリット
	・簡単にDBを操作できる。
	・コンポーネント内でデータを直接取得できる。
	httpリクエストを削減でき、レイテンシを短縮できる
	デメリット
	・使用する場面が限定的
	・複雑な処理や長時間かかる処理には適していません。
```

### 併用することが最適な使用方法
簡単な処理等はSever Actionsを使用することによって、アプリケーションの速度を少しでも高速にすることが可能になります。


## CRUD処理


Create（新規作成）
ここでは新規投稿をする`postBB()`についてまとめています。

フロントエンド側
```create.tsx
"use client";
//中略
import { postBB } from "@/app/actions/postAction";

//zod周り省略

const CreatePage = () => {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      title: "",
      content: "",
    },
  });

  async function onSubmit(value: z.infer<typeof formSchema>) {
    const { username, title, content } = value;
    postBB({ username, title, content });
  }

  return (
    <div className="mt-10">
      <h1 className="text-4xl font-bold text-center py-6">新規投稿</h1>
      <Form {...form}>
        <form
          onSubmit={form.handleSubmit(onSubmit)}
          className="space-y-3 w-1/2 px-7 mx-auto"
        >
        
          //入力部分省略
        
          <Button type="submit">投稿</Button>
        </form>
      </Form>
    </div>
  );
};

export default CreatePage;
```

バックエンド側
```postActions.ts
//新規投稿
export const postBB = async ({
  username,
  title,
  content,
}: z.infer<typeof formSchema>) => {
  await prisma.post.create({
    data: {
      username,
      title,
      content,
    },
  });

  revalidatePath("/");
  redirect("/");
};
```

Read（読込機能）
ここでは記事の一覧を表示する時に使う`getAllData（）`と記事の詳細を取得する`getDetailData()`をまとめました。

フロントエンド側
```page.tsx
//全記事読み込み
import CardList from "../components/ui/CardList";
import { getAllData } from "./actions/postAction";

export default async function Home() {
  const bbData = await getAllData();
  return (
    <main>
      <CardList bbData={bbData}/>
    </main>
  );
}
```
```[id]/page.tsx
"use client";
//中略
import { getDetailData } from "@/app/actions/postAction";
import { deleteBB } from "@/app/actions/postAction";

//記事詳細を取得する処理
const DetailPage = ({ params }: { params: { Id: number } }) => {
  const [bbDetailData, setBbDetailData] = useState<BBDataType | null>(null);
  useEffect(() => {
    const fetchData = async () => {
      const data = await getDetailData(params.Id);
      setBbDetailData(data);
    };
    fetchData();
  }, [params.Id]);
  
  const { id, title, content, username } = bbDetailData;

  return (
    <div className="mx-auto max-w-4xl p-10 border border-gray-300 bg-white mt-10">
      <div className="mb-8">
        <h1 className="text-2xl font-bold">{title}</h1>
        <p className="text-gray-700">{username}</p>
      </div>

      <div className="mb-8">
        <p className="text-gray-900">{content}</p>
      </div>
    </div>
  );
};

export default DetailPage;

```
バックエンド側
`getAllDate()`でデータを取得する際に`findMany()`を使用しているので、データ量が多くなった時に毎回DBを全て検索していると処理が大きくなってしまいます。ここでは行っていませんが、DBに変更が加わった時だけ処理されるようにするなど何らかの対策をする必要があります。
```postActions.ts
//全記事取得
export const getAllData = async () => {
  const bbData: BBDataType[] = await prisma.post.findMany();
  return bbData;
};

//投稿詳細取得
export const getDetailData = async (id: any) => {
  const bbDetailData = await prisma.post.findUnique({
    where: {
      id: parseInt(id),
    },
  });
  return bbDetailData;
};
```

Update（編集機能）
CRUDの中で一番手間がかかった処理です。
投稿を編集する際に、元の値を入力フォームの初期値に設定するためのデータの扱いと、編集結果をバックエンドに送る処理がうまくいかず苦戦しました。

フロントエンド側
```edit/[Id]/pagge.tsx
"use client";
//中略
import { editBB, getDetailData } from "@/app/actions/postAction";

//zod周り省略

const EditPage = ({ params }: { params: { Id: number } }) => {
  const [bbDetailData, setBbDetailData] = useState<BBDataType>();
  //form初期化
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
      title: "",
      content: "",
    },
  });

  //記事のデータを取得する処理（省略）

  // Form初期値を設定
  useEffect(() => {
    if (bbDetailData) {
      form.reset({
        username: bbDetailData.username,
        title: bbDetailData.title,
        content: bbDetailData.content,
      });
    }
  }, [bbDetailData, form]);

  //Loding画面（省略）

  //ボタンが押された時に入力値をeditBBに渡しバックエンドで処理を行う
  async function onSubmit(value: z.infer<typeof formSchema>) {
    const { username, title, content } = value;
    const editId = params.Id;
    console.log(editId, username, title, content);
    editBB(editId, { username, title, content });
  }

  return (
    <div className="mt-10">
      <h1 className="text-4xl font-bold text-center py-6">編集画面</h1>
      <Form {...form}>
        <form
          onSubmit={form.handleSubmit(onSubmit)}
          className="space-y-3 w-full lg:w-1/2 px-7 mx-auto"
        >
        
          //入力部分省略
          
          <div className="text-right">
            <Button type="submit" className="bg-green-600">
              保存
            </Button>
          </div>
        </form>
      </Form>
    </div>
  );
};

export default EditPage;

```
バックエンド側
```postActions.ts
//編集処理
export const editBB = async (
  editId: any,
  { username, title, content }: z.infer<typeof formSchema>
) => {
  try {
    await prisma.post.update({
      where: {
        id: parseInt(editId),
      },
      data: {
        username: username,
        title: title,
        content: content,
      },
    });
    console.log("log: edit done");
  } catch (error) {
    console.log(editId, { username, title, content });
    console.error("log: edit error");
  }

  revalidatePath("/");
  redirect("/");
};
```

Delete（削除機能）
フロントエンド側
```delete.tsx
"use client";
//中略
import { deleteBB } from "@/app/actions/postAction";

//投稿詳細読込部分省略（前記）

  const { id, title, content, username } = bbDetailData;

  return (
    <div className="mx-auto max-w-4xl p-10 border border-gray-300 bg-white mt-10">
    
    //投稿表示部分省略
    
      <div className="w-full flex text-white">
        <div className="w-full text-right">
          <Link href={`/edit/${id}`}>
            <Button className="w-1/4">編集</Button>
          </Link>
          <Button
            onClick={() => deleteBB(params.Id)}
            className="ml-8 w-1/4 bg-red-600 border border-slate-500"
          >
            削除
          </Button>
        </div>
      </div>
    </div>
  );
};

export default DetailPage;

```
バックエンド側
```postActions.ts
//削除処理
export const deleteBB = async (id: any) => {
  try {
    await prisma.post.delete({
      where: {
        id: parseInt(id),
      },
    });
    console.log("log: delete done");
  } catch (error) {
    console.error("log: delete error");
  }
  revalidatePath("/");
  redirect("/");
};
```


## 詳細はGithubにコードをあげているのでご覧ください

https://github.com/kengo2003/bb-nextjs-app


## 今後
今後はログイン機能やUIの改善を行っていきたいと思います。

## 参考文献
Nextjs公式ドキュメント
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations


[一番上へ](#目次)