Blog
ブログ

2024年12月09日

Remixのactionとloaderの実践 – SOHOBB AI/BI Advent Calendar 2024

こんにちは。AI/BI部の塚本です。

今回は、Advent Calendarでの2回目の投稿となります。

 

前回、Remixについての簡単な紹介をしましたので、そちらを先にご覧いただくと、より今回の記事も楽しめるかと思います。

参考: https://www.sohobb.jp/event/remixintroduction-sohobb-ai-bi-advent-calendar-2024/

 

今回は、前回紹介したRemixのactionとloaderについてより理解するための実践編となります。

それでは、今回もどうぞよろしくお願いします!

 

Prismaを使ってデータベースに接続

本題に入る前にPrismaについて触れておきます。

PrismaはNode.jsでのサーバーサイドの開発の際に、手軽にデータベース操作ができるようにするためのORMフレームワークです。

ORMとは、「Object Relational Mapping」の略で、メソッドの実行をするようにデータベース操作をできるようにするものです。

SQLのクエリ文を文字列として持っておいて…というようなことをしないで済むのでソースコードの管理が断然楽になります。

 

例えば、Prismaの場合、Userというテーブルからすべてのレコードを取得したい場合は下記のように記述します。

import { PrismaClient } from '@prisma/client'

// Prismaのデータベース接続のためのクライアントのインスタンスを生成
const prisma = new PrismaClient()

// SELECT * FROM User;に相当する処理
const users = await prisma.user.findMany()

 

今回は、このPrismaを利用してPostgreSQLデータベースを操作します。

Prisma + PostgreSQLについては、今回のメインではないので詳しくは触れませんがどちらもよく見る技術スタックなので、調べてみて損はないと思います。

参考:

今回の環境について

まずは、ソースコードを書いていく前に、環境について触れておきたいと思います。

 

今回は、Remixのactionとloaderの雰囲気をつかむことがメインとなるため、環境構築の詳細は省きますが、各種公式ドキュメントのリンクを貼りますので、自分も試してみたいという方はそちらの導入に関する記事を参考にしてみてください。

なお、PostgreSQLは、Dockerで起動するのがお手軽かと思いますので、Docker Imageのリンクを貼りつけておきます。

 

今回使った各種バージョンを共有するため、package.jsonのdependeneciesの部分を参考までに貼り付けておきます。

"dependencies": {
    "@prisma/client": "^5.21.1",
    "@remix-run/node": "^2.13.1",
    "@remix-run/react": "^2.13.1",
    "@remix-run/serve": "^2.13.1",
    "isbot": "^4.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "tsx": "^4.19.2"
 },

 

参考:

データベースの下準備

それでは、さっそく実践に入っていきます。

 

最初はデータベースの下ごしらえです。

 

prisma initコマンドで/prismaというディレクトリとその下に/prisma/shema.prismaというファイルが作成されます。

 

yarn prisma init

 

今回は、より簡素にするため、ユーザー名とe-mailアドレスを登録していくだけの仕組みを作ってみることにします。

 

今回は、shema.prisamファイルに下記のように設定してみました。


generator client {
    provider = "prisma-client-js"
  }
 
  datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
  }
 
  model User {
    id    Int     @id @default(autoincrement())
    email String  @unique
    name  String
  }

 

PostgreSQL上にUserというテーブルが作成されるような設定です。

  • id: 自動でインクリメントされるレコードのID
  • email: 重複を許さないString型(PostgreSQLでいうtext)のデータ
  • name: String型のデータ

 

shema.prismaでPrismaの設定をしたら、下記の二つのコマンドを実行しておきましょう。

 

Prismaのクライアント(データベースとのやり取りをするもの)を作成するコマンド

yarn prisma generate

 

Prismaのデータベースのマイグレーション(スキーマに従ってDBを構成すること)を実行するコマンド


yarn prisma migrate dev --name init

 

また、Prismaを利用できるように、TypescriptのソースコードでもPrismaClientの設定を記述しておきます。

今回は、/app/lib/prisma.server.tsというファイルを作成し、そこに下記の通り記述しておきました。

既にPrismaClientインスタンスが存在すればそれを使って、存在しない場合は新しく生成しています。ローカルの開発環境でリロードのたびにインスタンスが大量に生成されるのを防ぐための対応策となっています。

import { PrismaClient } from "@prisma/client";


const prisma = (global as any).prisma || new PrismaClient();


if (process.env.NODE_ENV !== "production") {
  (global as any).prisma = prisma;
}


export default prisma;

 

actionとloaderを実装するページの下準備

actionとloaderを実装するために、Usersというページコンポーネントを作成します。

 

まずは、下記のようなソースコードを用意してみました。

/app/routes/blogusers/route.tsx

import { Form } from "@remix-run/react";


const Users = () => {
  return (
    <>
      <p>ユーザー登録</p>
      <Form method="post">
        <div>
          <label>name: </label>
          <input type="text" name="name" id="name" />
        </div>
        <div>
          <label>email: </label>
          <input type="email" name="email" id="email" required />
        </div>
        <button type="submit">Submit</button>
      </Form>
      <p>ユーザー一覧</p>
      <ul>
        <li>username: user@email.com</li>
      </ul>
    </>
  );
};


export default Users;

 

この段階の見た目は下画像のようになっています。

{root}/blogusersというURLのページになっていますね。

 

remix-action-loader-image-001

 

Remixでは、ソースコードのファイルの構成自体がページURLとなるような仕組みになっています。

/app/routesというディレクトリ以下に作成されたファイルやディレクトリがURLとなるため、どのようなページ構成になるのかが、ソースコードを見ればわかるのはとても分かりやすいです。

 

私個人としては、大きくコンテキストが変わるページはディレクトリを作成して、そのページにのみ使うコンポーネントなどは、そのディレクトリ内にファイルを作成して実装すると分かりやすそうだと思いました。

 

actionの実装

まずは、登録しなければ話が進まないので、ユーザー登録の実装をしてきましょう。

 

今回は、下記のようなaction関数を実装してみました。

import { Prisma } from "@prisma/client";
import { ActionFunctionArgs } from "@remix-run/node";
import prisma from "~/lib/prisma.server";


export const action = async ({ request }: ActionFunctionArgs) => {
  const body = await request.formData();


  const name = body.get("name");
  const email = body.get("email");


  if (typeof name !== "string" || typeof email !== "string") {
    throw new Error("Invalid form data");
  }


  const userData: Prisma.UserCreateInput = {
    name: name,
    email: email,
  };


  const user = await prisma.user.create({
    data: userData,
  });


  console.log(user);
  return null;
};

 

とりあえず、動作検証してみましょう。

 

まず、nameとemailを入力して、Submitボタンをクリックしてみます。

 

remix-action-loader-image-002

 

ちゃんと、データベースに保存されたデータが表示されています。

※ この時点では、loaderを実装していないので、ブラウザ上に追加したデータは反映されません。

 

remix-action-loader-image-003

 

ここで一つ注意しておきたいのが、action関数内で記述したconsole.log()の内容がブラウザのコンソールではなくて、Remixを起動したローカルのコンソール上に表示されている点です。

action関数の処理はサーバー側で実行されていることが分かります。

 

action関連の解説

action関数の中身を少し解説しておきます。

 

まずは下記の部分ですが、actionでは、request.formData()で簡単にFormの各inputの内容を取得できます。

export const action = async ({ request }: ActionFunctionArgs) => {
    const body = await request.formData();
 
    const name = body.get("name");
    const email = body.get("email");
 
    // 略
  };

 

もう一か所、Prismaを利用してUserレコードを作成している部分も確認しておきましょう。

Prisma.{テーブル名}CreateInputという型の変数に作成したデータを格納しています。

その後、prisma.user.create()で実際にレコードを作成しています。

export const action = async ({ request }: ActionFunctionArgs) => {
    // 略
 
    const userData: Prisma.UserCreateInput = {
      name: name,
      email: email,
    };
 
    const user = await prisma.user.create({
      data: userData,
    });
     
      // 略
  };

 

loaderの実装

次は登録したレコードを表示するためのloaderを実装してみます。

 

今回は単純にloaderを使ってユーザーをすべて持ってくる実装にしてみました。

import { Prisma, User } from "@prisma/client";
import { ActionFunctionArgs } from "@remix-run/node";
import { Form, json, useLoaderData } from "@remix-run/react";
import prisma from "~/lib/prisma.server";


export const loader = async () => {
  const users = await prisma.user.findMany();
  return json(users);
};


export const action = async () => {
  // actionの実装内容
};


const Users = () => {
  const users = useLoaderData<User[]>();
  return (
    <>
      <p>ユーザー登録</p>
      <Form method="post">
        <div>
          <label>name: </label>
          <input type="text" name="name" id="name" />
        </div>
        <div>
          <label>email: </label>
          <input type="email" name="email" id="email" required />
        </div>
        <button type="submit">Submit</button>
      </Form>
      <p>ユーザー一覧</p>
      <ul>
        {users.map((user, index) => {
          return (
            <li key={index}>
              {user.name}: {user.email}
            </li>
          );
        })}
      </ul>
    </>
  );
};


export default Users;

 

実行してみた状態が下画像です。

 

remix-action-loader-image-004

 

さきほど、登録した際にidが2となっていましたが、事前確認していたname=””, email=”a@a”というデータも表示されています。

では、この状態でさらにデータを追加してみます。

name=”tsukamoto”, email=”tsukamoto@test.com”にして、追加してみます。

 

remix-action-loader-image-005

 

ちゃんと新規追加したデータも表示されました!

 

remix-action-loader-image-006

 

loader関連の解説

今回作成したloader関数はとても単純で、すべてのレコードを取得しているだけのものになります。

Remixが提供しているjson関数を利用することで、json形式で取得したUserレコードの一覧を利用することができます。

import { json } from "@remix-run/react";


export const loader = async () => {
  const users = await prisma.user.findMany();
  return json(users);
};

 

loaderで取得したデータを実際に利用する際には、Remixのフックである、useLoaderData()を利用します。

さらに言うと、@prisma/clientがshema.prismaで定義したmodelの型を持っているので、ソースコード上でわざわざ再度User型を定義する必要はありません。便利!

import { User } from "@prisma/client";
import { useLoaderData } from "@remix-run/react";


// 略


const Users = () => {
  const users = useLoaderData<User[]>();
 
  return (
    <>
      <p>ユーザー登録</p>
        // 略
      <p>ユーザー一覧</p>
      <ul>
        {users.map((user, index) => {
          return (
            <li key={index}>
              {user.name}: {user.email}
            </li>
          );
        })}
      </ul>
    </>
  );
};


export default Users;

 

UXを少しだけ改善

Remixの機能紹介もかねて、少しUXを改善してみます。

 

今回は、フォームを送信した後、inputの中身が残ってしまっている部分を改善してみましょう。

以下のようなソースコードにすることで改善ができます。

import { Prisma, User } from "@prisma/client";
import { ActionFunctionArgs } from "@remix-run/node";
import { Form, json, useLoaderData, useNavigation } from "@remix-run/react";
import { useEffect, useRef } from "react";
import prisma from "~/lib/prisma.server";


export const loader = async () => {
    // loaderの実装内容
}


export const action = async () => {
    // actionの実装内容
}


const Users = () => {
  const navigation = useNavigation();
  const users = useLoaderData<User[]>();
  const isAdding = navigation.state === "submitting";
  const formRef = useRef<HTMLFormElement>(null);


  useEffect(() => {
    if (!isAdding) {
      formRef.current?.reset();
    }
  }, [isAdding]);


  return (
    <>
      <p>ユーザー登録</p>
      <Form ref={formRef} method="post">
        // formの中身は同じ
      </Form>
      <p>ユーザー一覧</p>
      <ul>
        // ユーザー一覧の中身は同じ
      </ul>
    </>
  );
};

 

Remixでは、useNavigation()というフックを利用することで、そのページの状態を手軽に確認することができます。

navigationの状態を監視して、状態変化を監視しているのが下のコードです。

ReactのフックであるuserEffect()を利用して、isAddingという変数を監視して、変更が発生した場場合に、処理が発生するようにしています。

const Users = () => {
    const navigation = useNavigation();
    const isAdding = navigation.state === "submitting";
 
    useEffect(() => {
      if (!isAdding) {
          // submit中以外になったときの処理
      }
    }, [isAdding]);
  };

 

今回の実装コード全体像

import { Prisma, User } from "@prisma/client";
import { ActionFunctionArgs } from "@remix-run/node";
import { Form, json, useLoaderData, useNavigation } from "@remix-run/react";
import { useEffect, useRef } from "react";
import prisma from "~/lib/prisma.server";


export const loader = async () => {
  const users = await prisma.user.findMany();
  return json(users);
};


export const action = async ({ request }: ActionFunctionArgs) => {
  const body = await request.formData();


  const name = body.get("name");
  const email = body.get("email");


  if (typeof name !== "string" || typeof email !== "string") {
    throw new Error("Invalid form data");
  }


  const userData: Prisma.UserCreateInput = {
    name: name,
    email: email,
  };


  const user = await prisma.user.create({
    data: userData,
  });


  console.log(user);
  return null;
};


const Users = () => {
  const navigation = useNavigation();
  const users = useLoaderData<User[]>();
  const isAdding = navigation.state === "submitting";
  const formRef = useRef<HTMLFormElement>(null);


  useEffect(() => {
    if (!isAdding) {
      formRef.current?.reset();
    }
  }, [isAdding]);


  return (
    <>
      <p>ユーザー登録</p>
      <Form ref={formRef} method="post">
        <div>
          <label>name: </label>
          <input type="text" name="name" id="name" />
        </div>
        <div>
          <label>email: </label>
          <input type="email" name="email" id="email" required />
        </div>
        <button type="submit">Submit</button>
      </Form>
      <p>ユーザー一覧</p>
      <ul>
        {users.map((user, index) => {
          return (
            <li key={index}>
              {user.name}: {user.email}
            </li>
          );
        })}
      </ul>
    </>
  );
};


export default Users;

 

おわりに

今回はactionとloaderを実際に実装して、動作を確認してみました。

何となくでも雰囲気が伝わって、自分でも試してみたいと思ってもらえたらうれしいです。

 

私もまだまだRemix触りたてなので、今後もいろいろ勉強していきたいと思います。

 

次回は、Remixとは別にBackendを作成するパターンを紹介したいと思います。Backendの実装では、Go言語のGinというフレームワークを利用してみようと思います。

次回もどうぞよろしくお願いします!

このページの先頭へ