最新記事公開時にプッシュ通知します
2026年2月16日
![Remix v3をサンプルコードで体験する。React非依存で“明示的に”UI制御できるコア機能3つ[レバテックLAB]](https://levtech.jp/media/wp-content/uploads/2026/02/260209remixv303.jpg)

執筆
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)に所属するテクニカルライター。新潟県上越市出身。会津大学コンピュータ理工学部卒業後、現在は新潟市に在住。ReactやAndroidを軸に、モバイルアプリ開発やWebサイト制作、Webメディア編集部の業務改善や、プログラミング技術記事の執筆等に携わっている。著書に「たった1日で基本が身に付く! Androidアプリ開発超入門」(技術評論社)、「基礎から学ぶ React Native入門」(翔泳社)他。

監修
静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。
主な著書に「独習」シリーズ、「これからはじめるReact実践入門」、「改訂3版 JavaScript本格入門」他、著書多数。
前回は、Remix v3が目指す「薄いフレームワーク」という理念を解説しました。ランタイム非依存、React非依存、手動リアクティビティという3つの特徴を通じて、Web標準に回帰するフレームワークの姿を描き出しました。
今回は、実際にコードを書きながらRemix v3の特徴を体験します。概念として理解することと、手を動かして体感することは異なります。本稿では、配布サンプルコードを用いて、手動リアクティビティ、型安全ルーティング、合成イベントシステムの3つの機能を実践的に学んでいきましょう。
なお、本稿執筆時点でRemix v3はまだexperimental段階にあり、APIが変更される可能性があります。特にパッケージ名について、前回紹介した@remix-run/eventsは、執筆時点では@remix-run/interactionに変更されています。最新の情報は公式リポジトリで確認してください。
本稿で利用しているサンプルコードは次のリポジトリで公開しています。
本稿は、レイアウトの都合上、解説に必要な箇所のみコードを掲載しています。実際に動作するプロジェクト(完全なコード)を確認したい場合には、ダウンロードサンプルを参照してください。
Remix v3の開発を始めるには、いくつかのパッケージが必要です。目的に応じて、以下のパッケージを組み合わせて使用します。
@remix-run/dom - JSXレンダリング(サーバ/クライアント両方) @remix-run/interaction - 合成イベント(press, longPress等) @remix-run/fetch-router - ルーティング @remix-run/route-pattern - 型安全なURLパターン
これらのパッケージは、それぞれが独立した単一目的のライブラリとして設計されています。すべてを使う必要はなく、プロジェクトの要件に応じて必要なものだけを選択できます。この「単一目的で置き換え可能な抽象化」という原則は、Remix v3を薄いフレームワークとして成立させている重要な要素です。
本稿では、クライアントサイドの機能に焦点を当て、@remix-run/interactionと@remix-run/route-patternを使った実装を解説します。サーバーサイドレンダリングやハイドレーションについては、次回以降で詳しく扱います。
前回、Remix v3の手動リアクティビティについて概念的に説明しました。this.update()を呼び出して明示的に再描画をトリガーするという設計思想です。今回は、この手動リアクティビティを支えるイベント管理の仕組みを見ていきます。
@remix-run/interactionパッケージは、DOMイベントリスナーを効率的に管理するためのユーティリティを提供します。その中心となるのが、createContainer関数とset()メソッドです。createContainerは、DOM要素に対するイベントリスナーを管理するコンテナを作成します。
// manual-reactivity/entry.ts
import { createContainer } from '@remix-run/interaction'
const button = document.getElementById('button')!
const container = createContainer(button)
container.set({
click(event) {
console.log('clicked')
},
})
// クリーンアップのタイミングで呼び出す
container.dispose()
このコードで重要なのは、set()が何度でも呼び出せるという点です。リスナーを動的に更新する際、前のリスナーを手動で解除する必要がありません。set()が内部で差分更新を行い、変更があったリスナーだけを効率的に付け替えます。今回は click イベントのみを扱いましたが、 input や focus といった他のDOMイベントも同様に管理できます。
より簡潔に書きたい場合は、on関数を使います。
import { on } from '@remix-run/interaction'
// button要素の取得は省略
const dispose = on(button, {
click(event) {
console.log('clicked')
},
})
// クリーンアップ
dispose()
onはcreateContainerのショートハンドで、戻り値の関数を呼び出すことでリスナーを解除できます。リスナーの動的更新が不要なシンプルなケースではこちらが便利です。
@remix-run/interactionの強力な機能の一つが、非同期リスナーの重複実行対策です。リスナー関数の第二引数には、AbortSignalが提供されます。これは非同期処理のキャンセルを通知するためのWeb標準の仕組みで、Fetch APIでも利用されています。
on(button, {
async click(event, signal) {
try {
// クリックを連打した場合、signal経由でキャンセル通知が送られ、
// 前回のリクエストは自動的に中断される
let response = await fetch('/api/data', { signal })
let data = await response.json()
updateUI(data)
} catch (err) {
if (err.name === 'AbortError') {
// キャンセルされた場合は何もしない(次のリクエストに任せる)
return
}
throw err
}
},
})
リクエストがキャンセルされると、fetchはAbortErrorというDOMExceptionを throw します。上記のようにerr.nameで判定することで、キャンセルと他のエラーを区別できます。
この設計により、ボタンの連打による重複リクエストや、非同期処理の競合状態を、フレームワークレベルで対処できます。AbortControllerの生成やライフサイクル管理を自前で行う必要がなく、リスナーの引数を2つにするだけでこの機能が有効になります。
従来のRemixやNext.jsでは、ファイルシステムのディレクトリ構造がそのままルート定義になる「ファイルベースルーティング」が主流でした。
Remix v3では、これとは異なるアプローチを採用しています。ルートをコード上で明示的に定義する方式に移行し、その中核となるユーティリティがRoutePatternクラスです。RoutePatternはルーティングフレームワーク全体ではなく、URLパターンのマッチングと生成に特化した軽量なユーティリティです。このクラスは、URLパターンの定義、マッチング、URL生成を型安全に行えます。
// type-safe-routing/entry.ts
import { RoutePattern } from '@remix-run/route-pattern'
const bookShow = new RoutePattern('/books/:slug')
// URL生成(型安全)
bookShow.href({ slug: 'remix-guide' })
// → "/books/remix-guide"
// URLマッチング
const result = bookShow.match('/books/remix-guide')
// → { params: { slug: 'remix-guide' }, url: URL }
href()メソッドの引数は、パターンから自動的に型が推論されます。:slugというパラメータを含むパターンでは、{ slug: string }が必須になります。パラメータの指定漏れや型の不一致は、TypeScriptがコンパイル時に検出します。
実践的なアプリケーションでは、route()関数を使って階層的なルートを定義できます。
import { route, get, post, form, resources } from 'remix'
let routes = route({
home: '/',
about: '/about',
books: {
index: '/books',
show: '/books/:slug',
genre: '/books/genre/:genre',
},
auth: {
login: form('login'),
logout: post('logout'),
},
})
// 型安全なURL生成
routes.books.show.href({ slug: 'hello-world' })
routes.books.genre.href({ genre: 'tech' })
この設計の利点は、URLの構造がコードとして表現され、型システムによって保護されることです。文字列リテラルでURLを組み立てる従来の方法では、タイポや構造の変更がバグの原因になりやすいですが、型安全ルーティングならそのリスクを大幅に減らせます。
前回、press()という合成イベントを紹介しました。今回は、このイベントシステムの詳細を解説します。
合成イベントは、複数のネイティブイベントを抽象化し、より意味的な操作として扱えるようにする機能です。@remix-run/interaction/pressパッケージは、以下の表のイベントを提供します。
| イベント | 説明 |
|---|---|
press 押下 | 解放の完全なサイクル |
pressDown | 押下開始 |
pressUp | 押下解放 |
longPress | 500ms以上の長押し |
pressCancel | 押下のキャンセル(要素外での解放、Escapeキー) |
@remix-run/interaction/pressパッケージが提供する合成イベント
これらのイベントは、マウスクリック、タッチ、キーボード(Enter/Space)を統一的に扱います。なお、シンプルなボタンクリックだけが必要な場合は、ネイティブのclickイベントでも十分です。clickはボタンやリンクなどフォーカス可能な要素であれば、マウス、タッチ、キーボードのいずれでも発火します。
合成イベントが真価を発揮するのは、longPressやpressDownのような細かい操作の区別が必要な場合です。長押しでコンテキストメニューを表示する、押下中に視覚フィードバックを与えるといった操作は、ネイティブイベントだけでは実装が煩雑になります。合成イベントシステムはこれらを標準機能として提供します。
// synthetic-events/entry.ts
import { on } from '@remix-run/interaction'
import { press, longPress } from '@remix-run/interaction/press'
on(target, {
[pressDown](event) {
// 押下開始時の視覚フィードバック
target.classList.add('pressing')
},
[longPress](event) {
event.preventDefault() // 後続のpress/pressUpをキャンセル
showContextMenu()
},
[press](event) {
handleClick()
},
[pressCancel](event) {
target.classList.remove('pressing')
},
})
longPressイベントでevent.preventDefault()を呼び出すと、後続のpressとpressUpイベントがキャンセルされます。これにより、長押しとタップを明確に区別でき、モバイルアプリでよく見られるコンテキストメニューのような機能を実装できます。
合成イベントが提供するPressEventには、clientXとclientYプロパティが含まれており、ポインターの位置を取得できます。キーボード操作の場合は(0, 0)になりますが、これによりポインター系とキーボード系のイベントを同じハンドラで処理できます。
本稿では、Remix v3の3つの主要な機能を実践的に解説しました。
createContainerとonによる手動リアクティビティは、イベントリスナーの管理をシンプルにし、非同期処理の再入力保護を自動的に提供します。RoutePatternによる型安全ルーティングは、URLの構造をコードとして表現し、タイポや構造変更によるバグを防ぎます。合成イベントシステムは、デバイスの違いを吸収し、より意味的なインタラクションの記述を可能にします。
これらの機能に共通するのは、「明示的であること」と「薄い抽象化であること」です。何が起きているかがコードから明確に読み取れ、フレームワーク独自の魔法に頼らない。必要な機能だけを選んで組み合わせられる。これがRemix v3の目指す「薄いフレームワーク」の具体的な姿です。
次回は、フォーム送信の段階的強化、AbortSignalを活用した競合制御、ミドルウェア設計など、より実践的なパターンを解説します。Remix v3をプロダクションで使う際の技術選定の視点についても触れる予定です。
配布サンプルコードには、本稿で解説した3つの機能のデモが含まれています。npm run devで起動し、ぜひご自身の環境で試してみてください。
関連記事



人気記事