Vite+ + React¶
Vite+ pre-1.0 SPA React 19
ReScript と Vite+ ツールチェーンで動作するシングルページ React アプリケーション。Vite+(vite-plus)は Vite・Vitest・Oxlint・Oxfmt・Rolldown を統合したラッパーであり、1 つの CLI(vp)が dev・build・preview・test を処理する。テンプレートはオフラインフォールバックを備えた動作する greet フォーム (入力 + useState + fetch)と、vp test のスモークスイートを同梱する。
モダンな統合ツールチェーンで React SPA を組みたい場合に本テンプレートを選択する。.res + JSX のプロジェクトを ブラウザで動かす最速の手段である。Vite+ で詰まった場合(pre-1.0 のため)も、README と本ページに vanilla Vite への クリーンなフォールバック手順が記載されている — ロックインされることはない。
生成内容¶
my-project/
├── rescript.json # JSX mode "automatic", @rescript/react in bs-deps
├── package.json # ESM, "private": true, "vp" scripts
├── index.html # <script type="module" src="/src/Main.res.mjs">
├── vite.config.mjs # imports defineConfig from "vite-plus" + react()
├── src/
│ ├── Main.res # ReactDOM.Client.createRoot + <App />
│ ├── App.res # form + useState + fetch + Validation
│ ├── Api.res # fetch("/api/greet") + offline fallback
│ ├── Validation.res # zod or sury — selected in the wizard
│ └── __tests__/
│ └── App.test.mjs # smoke test that App is a function component
├── README.md # script docs + About Vite+ section
├── LICENSE # MIT, holder = project name
├── .nvmrc # Node 24
├── .gitignore # node_modules + ReScript build + dist/ + .vite/
├── .editorconfig # 2-space indent, LF line endings
└── .github/
├── dependabot.yml # weekly npm updates
└── workflows/ci.yml # install + rescript build + vp test
ウィザードオプション¶
オプション |
効果 |
|---|---|
Project name |
npm の |
Package manager |
npm / pnpm / yarn / bun。 |
Validation library |
|
主要な依存¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ |
|
|
標準ライブラリ |
|
|
コンパイル後の |
|
|
React バインディング(フック・コンポーネント・イベント) |
|
|
React 19 ランタイム |
|
|
フォーム入力のバリデーション(ウィザードで選択) |
|
|
Vite/Vitest/Oxlint を統合し |
|
|
Vite+ のコアエンジン |
|
|
ドキュメント化されている Vite+ → Vite フォールバックが依存を変更せずに機能するよう、直接依存として追加されている |
|
|
React Fast Refresh + JSX |
|
|
スモークテストランナー( |
|
|
|
|
主要なファイル¶
rescript.json¶
本テンプレート固有の 3 つのポイント:
bs-dependenciesに@rescript/reactが含まれているため JSX が型チェックされるjsx.mode = "automatic"により、JSX はReact.createElementではなくreact/jsx-runtimeに展開されるウィザードでの選択に応じて、バリデーションライブラリ(
@rescript/zodシムまたはrescript-sury)が追記される
これらに手を加える必要はほとんどない — いずれもモダンな React + ReScript の規約と 1:1 で対応している。
index.html¶
Vite のエントリドキュメント。/src/Main.res.mjs をモジュールとして読み込む — Vite はこのパスを dev サーバ経由で解決する(dev の場合)か、ハッシュ付きのバンドルへ書き換える(build の場合)。
src/Main.res¶
ブートストラップ。3 行で構成される: #root を検索し、React ルートを作成し、<App /> をレンダリングする:
switch ReactDOM.querySelector("#root") {
| Some(rootEl) =>
ReactDOM.Client.Root.render(ReactDOM.Client.createRoot(rootEl), <App />)
| None => Console.error("Could not find root element")
}
src/App.res¶
インタラクティブなデモ。制御された <input> + submit ボタン + 4 つの useState (name, greeting, error, loading)から成る。submit ハンドラは Validation.parseGreetForm で入力をバリデートしてから Api.greet を呼び出す:
let handleSubmit = async event => {
ReactEvent.Form.preventDefault(event)
switch Validation.parseGreetForm(name) {
| Error(message) =>
setError(_ => Some(message))
setGreeting(_ => None)
| Ok({name: validated}) =>
setError(_ => None)
setLoading(_ => true)
try {
let message = await Api.greet(validated)
setGreeting(_ => Some(message))
} catch {
| JsExn(err) =>
setGreeting(_ => Some("Error: " ++ err->JsExn.message->Option.getOr("unknown")))
}
setLoading(_ => false)
}
}
validate してから fetch する順序が重要である: バリデーションは安価かつ同期的なので、ネットワークラウンドトリップの コストを払う前にチェックする。エラーはインライン(<p style={{color: "crimson"}}>)で描画され、リクエスト処理中は submit ボタンが無効化される。
src/Api.res¶
/api/greet を対象とした薄い fetch ラッパー。注目すべき点は オフラインフォールバック である: バックエンドが利用できなくても関数は使える挨拶を返すため、初回実行時にフォームが壊れて見えることがない。
let greet = async (name: string): string => {
try {
let response = await fetch("/api/greet", {...})
if response->ok {
let body = await response->json
body["message"]
} else {
`Hello, ${name}! (offline fallback — no backend at /api/greet)`
}
} catch {
| _ => `Hello, ${name}! (offline fallback — fetch failed)`
}
}
実際のバックエンドを接続する場合(Full-Stack や Monorepo に移行するか、Hono (REST) テンプレートを立ち上げる場合)には、フォールバック分岐を削除すること。
src/Validation.res¶
parseGreetForm: string => result<greetForm, string> — fetch 呼び出しの前に実行される。zod 版・sury 版のいずれも、トリム後の name が空でなく、80 文字以下であることをチェックする。不正な入力はインラインのエラーメッセージを点灯させ、妥当な入力は Api.greet へ進む。
src/__tests__/App.test.mjs¶
スモークテストは コンパイル後 の App.res.mjs をインポートし、エクスポートされたコンポーネントを検証する:
import { describe, expect, it } from "vitest";
import { make as App } from "../App.res.mjs";
describe("App", () => {
it("is a function component", () => {
expect(typeof App).toBe("function");
});
});
コンポーネントが到達可能であることを確認する最小限のサニティチェックである。フォーム自体はレンダリングしない。DOM の挙動を検証するには、@testing-library/react と JSDOM 環境を dev dependencies に追加し、<App /> をレンダリングして submit ボタンをクリックするテストを記述する。
vite.config.mjs¶
ボイラープレートに加えて 2 行:
import { defineConfig } from "vite-plus";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
defineConfig のインポート元が vite ではなく vite-plus である点に注意。vanilla Vite に フォールバックする場合はこのインポートを差し替える(下記の Notes を参照)。
npm スクリプト¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
推奨される開発ワークフローは 2 ターミナル構成である: 一方で pnpm res:dev を走らせ .res.mjs 出力を 最新に保ち、もう一方で pnpm dev を走らせて Vite+ の HMR サーバを動かす。Vite+ はファイルウォッチャ経由で 再コンパイルされたモジュールを拾う。
vanilla Vite へのフォールバック¶
pre-1.0 の Vite+ が誤動作する場合(よくあるのは vp build 中の vite/internal の解決失敗)、Vite+ から移行するのは 2 箇所の編集だけで、新しい依存は不要である:
vite.config.mjsのインポートを差し替える:- import { defineConfig } from "vite-plus"; + import { defineConfig } from "vite";
package.jsonでvpスクリプトを vanilla 相当のものに置き換える:"scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "test": "vitest run", "test:coverage": "vitest run --coverage" }
vite と vitest のパッケージは既に devDependencies に含まれている。vite-plus と @voidzero-dev/vite-plus-core はインストールしたまま残しておいてもよいし(設定からインポートされなければ 何もしない)、pnpm remove vite-plus @voidzero-dev/vite-plus-core で削除してもよい。
Vite+ の安定リリースが出たら、戻すのは同じ編集の逆操作で済む — 典型的な React プロジェクトでは、両者の間で vite.config の構文に違いはない。
2日目以降のレシピ¶
React コンポーネントの作成 — 新しい ReScript React コンポーネントを 追加して
App.resに組み込むTypeScript からの変換 — 既存の
.tsxコンポーネントを.resコンポーネントに移植するImport の最適化 — SPA が成長しても
App.resとMain.resを整然と保つデッドコードの検出 —
reanalyzeを実行して未使用コンポーネントを検出する
プロジェクトを開いた後の ReScript 側のエディタワークフローについては、機能概要 を参照すること。
補足¶
Vite+ は pre-1.0 である。
TemplateVersions.VITE_PLUSは0.1.xレンジに固定している。安定リリース前に API・デフォルト値・CLI フラグが変わる可能性がある。バンプの際は Vite+ のチェンジログ を追跡すること。既知の非互換性: 現在の pre-1.0 Vite+ は
@vitejs/plugin-reactと組み合わせた際にvite/internalを常にクリーンに解決できるとは限らないため、vp buildが失敗することがある。フォールバックは 1 箇所の編集である:vite.config.mjsでimport { defineConfig } from "vite-plus"をimport { defineConfig } from "vite"に置き換え、npm スクリプト(dev/build/preview/test)をvpからvite/vite build/vite preview/vitest runに差し替える。テンプレートは既に直接のvite依存を宣言しているため、この差し替えにnpm installは不要である。デフォルトは React 19 である。
@rescript/reactと@types/react系はTemplateVersions.ktで対応するメジャーに固定されている。一緒にバンプすること。rescript.jsonのjsx.modeは"automatic"に設定されている。"classic"に戻す場合は、JSX を使うすべての.resファイルの先頭でReactをインポートする必要もある。Vitest のスモークテストは
import { make as App } from "../App.res.mjs"が関数を 返すことだけを確認する。フォームを実際にレンダリングはしない。DOM のアサーションが必要になったら@testing-library/reactと JSDOM 環境を追加すること。フォームは意図的にネットワーク呼び出しの 前 に
Validationを呼ぶ。これにより契約がシンプルに保たれる: ネットワークは既にローカルチェックを通過した入力しか見ない。サーバ側のバリデーションは依然として必須である — 対応するバックエンドパターンについては Hono (REST) または Full-Stack テンプレートを参照すること。.gitignoreは ReScript のデフォルトに加えてdist/と.vite/を追加しており、ビルドアーティファクトや Vite のローカルキャッシュがコミットされないようになっている。package.jsonは"private": trueを宣言している。SPA そのものを実際に npm に公開したい場合に限り、falseに切り替え(同時に正式なversionとlicenseを追加する)こと — エンドユーザ向けアプリでは珍しい。vpCLI はvp testにおいては Vitest の薄いラッパーである。Vite+ が公開しない Vitest 固有のフラグが 必要になったら、直接vitest runを実行すること —vitestと@vitest/coverage-v8の依存は 既にインストール済みである。