EY-Office ブログ

RemixはReact界のRuby on Railsか?

昨年くらいからRemixというReactベースのフレームワークが話題になってきています。日本語の記事もあったので読んでみましたがRemixの素晴らしさが理解できませんでした。ところが最近Remix公式ページのブログData Flow in Remixを読んでビックリました!

このビックリ感は、17年前にRuby on Railsのコードを見たときに似ています。Ruby on Rails同様にコードが圧倒的に少ないのです!

Remix Remix Blog “Data Flow in Remix” より

Data Flow in Remixブログのざっくりした解説

Remix公式ページのブログData Flow in Remixを読んでもらうのが良いのですが、ざっくりと内容を知りたい方向けに、ざっくりとした解説を書きます。

1. Action→State→View

ReactやVueのようなフロントエンド用ライブライリーでは、ボタンを押したなどのActionでState(状態やデータ)が変更されると、View(画面)がVirtual DOM等の技術を使い自動的に更新されるという、一方向のサイクルで処理が進みます。これにより、プログラマーが苦労してコードを書かなくても良い宣言的UIが実現されています。😁

さらに、フロントエンドではRedux等の高度なState管理ライブラリーが生まれプログラマーの苦労は減りました。

Remix Blog “Data Flow in Remix” より

2. バックエンドとのやりとりは?

しかし、ほとんどのアプリ(サービス)のデータ(状態)はバックエンド(サーバー)に保存・管理されています。したがってプログラマーが通信や、フロントエンドとバックエンドの同期を行うコードを書く必要があります。

通常、フロントエンドとバックエンドは別のプログラムとして開発されています。開発メンバーも違うかもしれません、それらの間でStateやデータの同期を行うには、API設計や通信コードの実装など面倒です。

Remix Blog “Data Flow in Remix” より

3. Remixはフルスタック・フレームワーク

以下はRemixのコードの例です、コードは商品情報を編集する画面のようです。

  • 真ん中のRoute関数は編集画面を表示するReactのコードです。当然フロントエンド(ブラウザー)で動きます
  • 上のlodaer関数は変更対象のデータをデーターベースから取得するコードです。このコードはバックエンドで実行されます
  • 下のaction関数はSave(submit)ボタンが押された後に動く関数です。これもバックエンドで動きます

このコードは1つのファイルに書かれています! Remixはフロントエンドとバックエンド両方を含むフルスタックのフレームワークです。通信のコードを書く必要はありません!

Remix Blog “Data Flow in Remix” より

4. Remixが解決するもの

Remixを使うとフロントエンドとバックエンドを含めたAction→State→Viewの流れが簡単に書けます。

ReactフロントエンドのAction→State→Viewの流れが、ブログの最初画像のAction→Loader→Componentというフロントエンド・バックエンドを統合した流れで実現されています。

Remix Blog “Data Flow in Remix” より

Remixを使って簡単なコードを書いてみました

Remixに強い興味を持ったので、まず公式ページのQuickstart(Get Started)を試してみました。あまり親切に書かれてい部分もあり躓きましたが、何とか書かれているコードを動かす事ができました。

これだけでは、腑に落ちないことも多かったので、独自のコードを書いてみる事にしました。 このブログの良く出てくるジャンケンのアプリを作る事にしました。ただし、Remixはフルスタックのフレームワークなので、ジャンケンの結果はバックエンドのデータベースに格納するようにしました。

作り方は、公式ページのJokes App Tutorialを参考にしました。create-remixの選択肢は、

  • TypeScript
    • JavaScriptも選択できますが。。。
  • バックエンドはExpress
    • 選択肢には独自サーバー、Express, AWS Lambda, Cloudflareなどのエッジコンピューティング・サービスなどがあります
    • 独自サービスはブラックボックス感が高いので馴染みのExpressにしました
  • データーベースはPrisma + SQLite3
    • Remixはデーターベースにはあまり依存してないようです?(深く調べていません)

フォルダー構成

今回はRemixの詳細な解説ではないので、appフォルダーのみ説明します。

app
├── components            表示用のReactコンポーネント
│   ├── JyankenBox.tsx      グー・チョキ・パー ボタン
│   └── ScoreBox.tsx        結果表示
├── db.server.ts          DB(Prisma)初期化コード
├── entry.client.tsx      フロントエンドの起動時に動く最初のコード
├── entry.server.tsx      バックエンドへのリクエスト毎に動く最初コード
├── models                Model(MVCのM)
│   ├── jyanken.server.ts   バックエンドで動くModel、DB操作やロジック
│   └── jyanken.ts          フロント。バックエンド両方で使う型定義
├── root.tsx              フロントエンド(React)のトップレベル
└── routes                パスに関連したReact(Remix)のコード
    ├── index.tsx           / のReactコード
    └── jyankens.tsx        /jyanken のReact(Remix)のコード

主要ファイル

routes/index.tsx

/パスに対応するページのReactコード

  • Linkタグでリンクaタグが生成されます
import { Link } from "@remix-run/react";

export default function Index() {
  return (
    <div>
      <Link to="/jyankens">Jyanken</Link>
    </div>
  );
}
routes/jyankens.tsx

/jyankenパスに対応するページのReact(Remix)のコード。今回のメインです!

import React from 'react'
import { ActionFunction, LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

import JyankenBox from '../components/JyankenBox'
import ScoreBox from '../components/ScoreBox'
import { Scores, doPon, getScrores } from '../models/jyanken.server';

type LoaderData = { scores: Array<Scores> };                   // ← ①

export const loader: LoaderFunction = async () => {            // ← ②
  return json({                                                // ← ③
    scores: await getScrores()                                 // ← ④
  });
};

export const action: ActionFunction = async ({ request }) => { // ← ⑤
  const formData = await request.formData();                   // ← ⑥
  const human = Number(formData.get("human"));                 // ← ⑦

  await doPon(human)                                           // ← ⑧
  return null                                                  // ← ⑨
}

const Jyanken: React.FC = () => {                              // ← ⑩
  const { scores } = useLoaderData<LoaderData>();              // ← ⑪

  return (
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox />
      <ScoreBox scores={scores} />
    </>
   )
}

export default Jyanken
  1. バックエンドから取り込むデータの型
  2. loaderは、バックエンドで動くデータ取得関数です
  3. データはJSONに変換して戻します
  4. getScrores関数はデータベースからジャンケン結果を取得します
  5. actionは、バックエンドで動くデータ更新関数です
  6. フォーム(form)からバックエンドに送られた値はrequest.formData関数で取得できます
  7. フォームのhuman(押されたジャンケンの手の値)を数値に変換します
  8. ジャンケンを行い、データベースに格納するdoPon関数の呼び出します
  9. nullを戻すと、Reactコンポーネント(ここではJyanken)が再描画されます
  10. JyankenアプリのメインのReactコンポーネント
  11. useLoaderDataはデータの取得してくれるホックです
    • useEfect + useState + データ取得関数のような感じでしょうか
    • このホックの中でバックエンドへのリクエストが発生し、バックエンド上で②のloaderが実行され、レスポンスがホックの戻り値になります
models/jyanken.ts

フロントエンド、バックエンド両方で使う型定義

export enum Te { Guu = 0, Choki, Paa};
export enum Judgment { Draw = 0, Win, Lose };
models/jyanken.server.ts

バックエンドで動く、モデル(主にデーターベース操作)

  • データベースはPrisma ORMを使ってアクセスしています
import { db } from "../db.server";
import type { Jyanken as Scores } from "@prisma/client";  // ← ①
import { Judgment, Te } from "./jyanken";
export type { Scores };                                   // ← ①

export const getScrores = async () => {                   // ← ②
  return db.jyanken.findMany({orderBy: {id: 'desc'}});    // ← ③
}

export const doPon = async (human: number) => {           // ← ④
  const computer: Te = Math.floor(Math.random() * 3);     // ← ⑤
  const judgment: Judgment = (computer - human + 3) % 3;  // ← ⑥

  return db.jyanken.create({ data: {human, computer, judgment} }); // ← ⑦
}
  1. テーブル名(≒ オブジェクトのデータ型)はJyakenですが、Scores型として再Exportしています
  2. getScroresは、全データの取得関数です
  3. PrismaのfindMany関数を使っています
    • 最近の結果が先頭になるように、idの逆順でソートしています
  4. doPonは、ジャンケンを行い、データベースに格納する関数です
  5. コンピューターの手を乱数で発生します
  6. 勝敗を計算します
  7. 人間の手, コンピューターの手, 勝敗をPrismaのcreate関数でデーターベースに格納します
components/JyankenBox.tsx

グー・チョキ・パー ボタンのReactコンポーネント

import React from 'react'
import { Form } from "@remix-run/react";
import { Te } from '../models/jyanken';

const JyankenBox: React.FC = () => {
  const divStyle: React.CSSProperties = {margin: "0 20px"}
  const formStyle: React.CSSProperties = {display: "inline-block", margin: "0 10px", fontSize: 14}
  return (
    <div style={divStyle}>
      <Form  method="post" style={formStyle}>                 // ← ①
        <input type="hidden" name="human" value={Te.Guu} />   // ← ②
        <input type="submit" value="グー" />
      </Form>
      <Form method="post" style={formStyle}>
        <input type="hidden" name="human" value={Te.Choki} />
        <input type="submit" value="チョキ" />
      </Form>
      <Form method="post" style={formStyle}>
        <input type="hidden" name="human" value={Te.Paa} />
        <input type="submit" value="パー" />
      </Form>
     </div>
  )
}

export default JyankenBox
  1. ボタン毎に、formになっています
    • formタグはRemixの専用のFormタグを使います
    • ボタンが押されるとhiddenタグで手の情報がバックエンドにPOSTされます
    • バックエンドはformの値を受け取りroutes/jyankens.tsxのaction関数が実行されます
  2. 手の情報は、hiddenタグでバックエンドに伝えます
components/ScoreBox.tsx

結果表示のReactコンポーネント

import React from 'react'
import { Scores } from "../models/jyanken.server"

type ScoreListProps = {
  scores: Scores[]
}
const ScoreBox: React.FC<ScoreListProps> = ({scores}) => {
  const teString = ["グー","チョキ", "パー"]
  const judgmentString = ["引き分け","勝ち", "負け"]

  const tableStyle: React.CSSProperties = {marginTop: 20, borderCollapse: "collapse"}
  const thStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px"}
  const tdStyle: React.CSSProperties = {border: "solid 1px #888", padding: "3px 15px", textAlign: "center"}
  return (
    <table style={tableStyle}>
      <thead>
        <tr>
          <th style={thStyle}>あなた</th>
          <th style={thStyle}>コンピュター</th>
          <th style={thStyle}>勝敗</th>
        </tr>
      </thead>
      <tbody>
        {scores.map((jyanken, ix) =>
          <tr key={ix}>
            <td style={tdStyle}>{teString[jyanken.human]}</td>
            <td style={tdStyle}>{teString[jyanken.computer]}</td>
            <td style={tdStyle}>{judgmentString[jyanken.judgment]}</td>
          </tr>
        )}
      </tbody>
    </table>
  )
}

export default ScoreBox
prisma/schema.prisma

Prisma ORMの設定とJyankenテーブル(オブジェクト)の定義

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Jyanken {
  id         Int      @id @default(autoincrement())
  createdAt  DateTime @default(now())
  computer   Int
  human      Int
  judgment   Int
}

ハマったこと

やはり何度かハマりました。ハマることで理解が深まりますね😅

1. loaderやaction関数を書くファイルは決まっている

最初、loader関数とuseLoaderDataホックをroutes/ScoreBox.tsxに書きましたが、バックエンドからデータを取得できませんでした、loader関数が起動されていませんでした。同じくaction関数をroutes/JyankenBox.tsxに書いたところ405-Method Not Allowedエラーになってしまいました。

loader関数、useLoaderDataホック、action関数をroutes/jyankens.tsxに移したところ動きました。URL(path)に関連付けされたコンポーネントのファイル内にに書く必要があります。

2. フロントエンド用ファイル、バックエンド用ファイル

最初models/jyanken.tsに書かれている型定義は、最初コンポーネントにありました。途中でこの型定義をmodels/jyanken.server.tsに移したところ、フロントエンドでTe.Guu undefinedのようなエラーが発生してしまいました。

調べてみるとRemixでは.server.tsはバックエンド専用、.client.tsはフロントエンド専用、それ以外は両用になるようです。しかしcomponents/ScoreBox.tsx../models/jyanken.serverを参照しているのはエラーにはなりません。
これはcomponents/ScoreBox.tsxで使われているScoresは型名なので、TypeScriptのコンパイルしJavaScriptになると消えてしまうので問題にならないようです。

Te.Guuがエラーになるのは、enumの性質 定数列挙型(Const Enums) が原因のようです。ということで現在のようにmodels/jyanken.tsに書き対応しました。

まとめ

RemixはReactを使ったフロントエンドと、それ専用のバックエンドを簡単に作れるフレームワークです。コードの解説で見たようにフロントエンドとバックエンドの通信部分はまったくありません、通信はフレームワークが行っています。

Ruby on RailsではGET /books/1リクエストは、BooksControllerクラスのshowメソッドで実行され、Bookモデルでbooksデータベースのid=1のレコードが取得され、HTML作成用テンプレートview/books/show.html.erbが実行されブラウザーにID=1の本の情報が表示されます。RailsでもMVC(Model View Controller)の関連はフレームワークが行っていてコードにはほとんど書かれていません。
Remixも同様にフレームワークが規約(Conventions)に基づきいろいろな事を行ってくれるフレームワークです。

以前書いたJamstack用Fullstack Frameworkを試してみたけど時期尚早だと思ったで取り上げたBlitz.jsRedwoodJSですが。

  • Blitz.jsRemixは似たコンセプトですが、Blitz.jsはより高機能をねらい。Remixはより簡単に使えるを目指してるように私には思えます
  • RedwoodJSはGraphQLを使っていたり、さらに大規模なサービスの構築を目指しているように私には思えます

RemixはReactで作られたフロントエンドに、データーベース操作のコードを追加するとサービスが完成するという気軽に使えるフレームワークです。 Reactをフロントエンドに使った簡単なサービスを早急に立ち上げたい人やプロトタイプを構築するのには素晴らしいフレームワークだと思います。

Remixが今後どうのように発展していくのか、楽しみなフレームワークですね。😁

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ