昨年くらいからRemixというReactベースのフレームワークが話題になってきています。日本語の記事もあったので読んでみましたがRemixの素晴らしさが理解できませんでした。ところが最近Remix公式ページのブログData Flow in Remixを読んでビックリました!
このビックリ感は、17年前にRuby on Railsのコードを見たときに似ています。Ruby on Rails同様にコードが圧倒的に少ないのです!
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
- バックエンドから取り込むデータの型
- loaderは、バックエンドで動くデータ取得関数です
- データはJSONに変換して戻します
getScrores
関数はデータベースからジャンケン結果を取得します- actionは、バックエンドで動くデータ更新関数です
- フォーム(
form
)からバックエンドに送られた値はrequest.formData
関数で取得できます - フォームの
human
(押されたジャンケンの手の値)を数値に変換します - ジャンケンを行い、データベースに格納する
doPon
関数の呼び出します null
を戻すと、Reactコンポーネント(ここではJyanken
)が再描画されます- JyankenアプリのメインのReactコンポーネント
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} }); // ← ⑦
}
- テーブル名(≒ オブジェクトのデータ型)はJyakenですが、Scores型として再Exportしています
- getScroresは、全データの取得関数です
- PrismaのfindMany関数を使っています
- 最近の結果が先頭になるように、idの逆順でソートしています
- doPonは、ジャンケンを行い、データベースに格納する関数です
- コンピューターの手を乱数で発生します
- 勝敗を計算します
人間の手, コンピューターの手, 勝敗
を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
- ボタン毎に、formになっています
- formタグはRemixの専用の
Form
タグを使います - ボタンが押されるとhiddenタグで手の情報がバックエンドにPOSTされます
- バックエンドはformの値を受け取り
routes/jyankens.tsx
のaction関数が実行されます
- formタグはRemixの専用の
- 手の情報は、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.jsとRedwoodJSですが。
- Blitz.jsとRemixは似たコンセプトですが、Blitz.jsはより高機能をねらい。Remixはより簡単に使えるを目指してるように私には思えます
- RedwoodJSはGraphQLを使っていたり、さらに大規模なサービスの構築を目指しているように私には思えます
RemixはReactで作られたフロントエンドに、データーベース操作のコードを追加するとサービスが完成するという気軽に使えるフレームワークです。 Reactをフロントエンドに使った簡単なサービスを早急に立ち上げたい人やプロトタイプを構築するのには素晴らしいフレームワークだと思います。
Remixが今後どうのように発展していくのか、楽しみなフレームワークですね。😁