空雲 Blog

Eye catchRemix 3 を実際に動かしてみる

publication: 2025/10/13
update:2025/10/14

Remix3 とは

Remix 3 は、React から脱却し、Web 標準に基づいた新しいフルスタック Web フレームワークとして再設計されました。Remix 3 は鋭意開発中ですが、この記事では React 代替に相当する箇所を部分的に動かして、実際に動作を検証してみます。

サンプルコードのリポジトリ

https://github.com/SoraKumo001/remix3-sample01

環境の準備

SPA アプリケーションとして動作させるための環境を用意します。今回はビルドに rolldown を使用します。

rolldown の設定

普通にビルドすれば依存パッケージのバンドルが行われるのですが、react/jsx-runtimeに関しては@remix-run/dom/jsx-runtimeに変換する設定が必要になります。

  • rolldown.config.ts

1import { defineConfig } from "rolldown";
2
3export default defineConfig({
4 input: "src/index.tsx",
5 output: {
6 file: "public/bundle.js",
7 },
8 resolve: {
9 alias: {
10 "react/jsx-runtime": "@remix-run/dom/jsx-runtime",
11 "react/jsx-dev-runtime": "@remix-run/dom/jsx-dev-runtime",
12 },
13 },
14});

typescript の設定

Remix3 の jsx の形式を有効にするため、jsxImportSource@remix-run/domにする必要があります。

  • tsconfig.json

1{
2 "compilerOptions": {
3 "strict": true,
4 "lib": ["ES2024", "DOM", "DOM.Iterable"],
5 "module": "ES2022",
6 "moduleResolution": "Bundler",
7 "target": "ESNext",
8 "allowImportingTsExtensions": true,
9 "rewriteRelativeImportExtensions": true,
10 "verbatimModuleSyntax": true,
11 "skipLibCheck": true,
12 "jsx": "react-jsx",
13 "jsxImportSource": "@remix-run/dom"
14 }
15}

サンプルプログラム

カウントを回すだけ

まずはボタンを押して、カウントを表示するプログラムです。
これを見るとわかりますが、React との違いはステートが存在しないということです。React を初期から使っている人なら気がつくと思いますが、クラスコンポーネントのような動作を関数で行っています。再レンダリングの指示はthis.render()で能動的に呼び出します。
そして Remix3 ではイベントはすべてonに集約されており、そこでイベントを受け取ることになります。pressという、あらかじめ定義されているものを使っていますが、自分で定義することも可能です。

1import { createRoot, type Remix } from "@remix-run/dom";
2import { press } from "@remix-run/events/press";
3
4function App(this: Remix.Handle) {
5 let count = 0;
6 return () => (
7 <button
8 on={press(() => {
9 count++;
10 this.render();
11 })}
12 >
13 Count: {count}
14 </button>
15 );
16}
17
18createRoot(document.body).render(<App />);

マウント、アンマウントの使い方

動作確認用に Test というコンポーネントを作って、ボタンを押したら消滅するようにします。
これを動かすと、マウント時にconnectが、アンマウント時にdisconnectが呼び出されます。
そして現時点で未実装なのか、ref が動作していません。

1import { createRoot, type Remix } from "@remix-run/dom";
2import { press } from "@remix-run/events/press";
3import { connect, disconnect } from "@remix-run/dom";
4
5function Test() {
6 return (
7 <div
8 ref={(n) => {
9 console.log(n);
10 }}
11 on={[
12 connect(() => {
13 console.log("mount");
14 }),
15 disconnect(() => {
16 console.log("unmount");
17 }),
18 ]}
19 >
20 Test
21 </div>
22 );
23}
24
25function App(this: Remix.Handle) {
26 let count = 0;
27 return () => (
28 <>
29 <button
30 on={press(() => {
31 count++;
32 this.render();
33 })}
34 >
35 Count: {count}
36 </button>
37 {count === 0 && <Test />}
38 </>
39 );
40}
41
42createRoot(document.body).render(<App />);

その他のイベントの使い方

その他のイベントの使い方です。ここでの注意点ですが、Test コンポーネントは関数を戻しています。こうしないと、再レンダリングごとにmouseStateが初期化されてしまうので気をつけてください。

1import { createRoot, type Remix } from "@remix-run/dom";
2import { press } from "@remix-run/events/press";
3import { dom } from "@remix-run/events";
4
5function Test(this: Remix.Handle) {
6 let mouseState = "mouseOut";
7 return ({ value }: { value: string }) => (
8 <div
9 on={[
10 dom.mouseover(() => {
11 mouseState = "mouseOver";
12 this.render();
13 }),
14 dom.mouseout(() => {
15 mouseState = "mouseOut";
16 this.render();
17 }),
18 ]}
19 >
20 {value}:{mouseState}
21 </div>
22 );
23}
24
25function App(this: Remix.Handle) {
26 let count = 0;
27
28 return () => (
29 <>
30 <button
31 on={[
32 press(() => {
33 count++;
34 this.render();
35 }),
36 ]}
37 >
38 Count: {count}
39 </button>
40 <Test value="test" />
41 </>
42 );
43}
44
45createRoot(document.body).render(<App />);

まとめ

取り急ぎ、動くものを作って動作を確認してみました。興味のある人は是非いじってみてください。