---
title: "Remix 3のコンポーネントとは？handle・mixの書き方と3.2の変更点、@remix-run/ui"
url: "https://www.issoh.co.jp/tech/details/10504/"
published: 2026-01-07
updated: 2026-11-10
categories: ["フレームワーク"]
publisher: "株式会社一創"
---

# Remix 3のコンポーネントとは？handle・mixの書き方と3.2の変更点、@remix-run/ui

Remix 3は、Reactを使わない独自のコンポーネントモデルを持つWebフレームワークです。2026年10月1日に3.0.0が正式版になり、10月23日に3.1.0、11月4日に3.2.0が出ています。この記事では、コンポーネントの書き方（`handle`と`mix`）、状態の持ち方、`remix new`で作る雛形、3.1・3.2で変わった挙動、ヘッドレスUIの`@remix-run/ui`を、3.2.0でビルドして動かしたコードで説明します。

2025年10月のRemix Jamで公開されたデモのAPI（`this.update()`や`pressDown`）は、正式版では書き方が変わっています。英語圏の解説記事にも当時の書き方のままのものが多いため、読み替えの表も載せます。

## まとめ：Remix 3のコンポーネントで押さえる要点

- 最新は`remix` 3.2.0（2026年11月4日）で、コンポーネント本体は`@remix-run/component` 1.2.0です。ReactにもPreactにも依存しません。
- コンポーネントは「`handle`を受け取り、描画関数を返す関数」です。状態はふつうの変数で持ち、変えたら`handle.update()`を呼びます。代入だけでは再描画されません。
- イベントは`mix={on('click', ...)}`で付けます。振る舞いの使い回しは`createMixin()`で作ります。
- 仮想DOMの差分更新はあります。Reactとの違いは、描画のきっかけを開発者が`update()`で出す点です。
- 3.2では`css()`の数値の扱いが変わり、`flex: 2`が`2px`でなく伸長比になりました。3.1以前の出力に合わせたCSSは見直しが必要です。
- UI部品の`@remix-run/ui`は0.13.1の不安定版で、`remix`パッケージには含まれません。0.13.0では`toggle`の書き方が変わりました。

## Remix 3の現在地：3.0正式版から3.2までのリリースとパッケージ構成

Remix 3は2025年5月28日の公式ブログ[「Wake up, Remix!」](https://remix.run/blog/wake-up-remix)で「Reactに依存しない」方針が示され、2025年10月のRemix Jamで動くデモが公開されました。ベータ、リリース候補を経て、[公式CHANGELOG](https://github.com/remix-run/remix/blob/main/packages/remix/CHANGELOG.md)の3.0.0の項に「First stable release of Remix 3.」と書かれた正式版が出ています。表の日付はnpmの公開日時（UTC）です。

| 公開日        | 版                    | コンポーネントに関わる主な変更                       |
| ---------- | -------------------- | ------------------------------------- |
| 2025-10-02 | @remix-run/dom（Jam用） | 実験版。this.update()・pressDown           |
| 2026-10-01 | remix 3.0.0          | 正式版。component 1.0.0                   |
| 2026-10-23 | remix 3.1.0          | ssr:falseのmixin、frame.reload、prefetch |
| 2026-11-04 | remix 3.2.0          | csp()、css()の数値の扱い、props検証             |

npmの`remix` 3.2.0は依存パッケージが49個あり、すべて`@remix-run/`で始まる自社パッケージです。`react`は含まれていません。動作環境は`engines`で`node >=24.3.0`と指定されています。`remix`パッケージはルーター、ミドルウェア、セッション、DBアクセス（`remix/data-table`）、アセット配信まで含む全部入りで、コンポーネントはそのうちの画面を描く層（`remix/component`）にあたります。

名前が同じでも、Remix v2の後継はReact Routerです。npmの`react-router`は2026年10月27日に8.5.1（v7系は7.18.5）が出ており、Remix 3とは別の系統として更新が続いています。Remix v2から続く系統の経緯は[PESPAとReact Router v8・Remix 3での実装](/tech/details/2279/)にまとめています。

## remix newで始める：雛形の構成と起動

新しいアプリは`npx remix new my-remix-app`で作ります（[同梱ガイドのCLIの章](https://github.com/remix-run/remix/blob/main/packages/remix/guides/14-cli-and-tooling.md)）。3.2.0で生成したところ、18個のファイルができ、実行時の依存は`remix`だけでした。

```
npx remix@3.2.0 new my-remix-app
cd my-remix-app
npm install
npm run dev    # http://localhost:44100
```

| ファイル                                 | 役割                       |
| ------------------------------------ | ------------------------ |
| server.ts                            | Nodeのhttpサーバー。既定ポート44100 |
| app/routes.ts・router.ts              | URLの定義とミドルウェア            |
| app/actions/controller.tsx           | URLごとの処理と描画              |
| app/actions/public/prompt-button.tsx | ブラウザで動く唯一の部品             |
| app/actions/public/entry.ts          | ブラウザ側の起動（run()）          |
| AGENTS.md・.agents/skills/remix       | AIコーディング支援向けの説明          |

`package.json`のスクリプトは`dev`・`hmr`・`start`・`test`・`typecheck`の5つで、TypeScriptは`node --import remix/node-tsx`でビルドせずに実行します。本番相当の起動でトップページは200を返し、ページ内でハイドレーションされるのはコピー用ボタン（`PromptButton`）だけでした。同梱のテスト（`remix test`）は1件成功し、`tsc --noEmit`もエラーなしです。

## Reactとの違い：setupとrenderの2段構造と明示的な更新

### setupとrenderの実行回数

コンポーネント関数そのものは最初に1回だけ呼ばれ（setup）、戻り値の描画関数が更新のたびに呼ばれます（render）。[@remix-run/componentのREADME](https://github.com/remix-run/remix/blob/main/packages/component/README.md)は「It runs once per component instance」「The render function runs on the first render and again on every update」と説明しています。状態はsetupで宣言したローカル変数に置けば、クロージャとしてコンポーネントの寿命のあいだ残ります。useStateも、Hooksの呼び出し順のルールもありません。propsは`handle.props`から読み、更新のたびに最新の値に差し替わります。

### ローカル状態の変更とhandle.update()による再描画

変数を書き換えても、Remixはそれを検知しません。自分の状態を変えたコンポーネントは、`handle.update()`を呼んで描画を予約します。もう1つの再描画の契機は親の更新で、親が`update()`すると子の描画関数も新しいpropsで呼ばれます。jsdomで試すと、子は自分の`update()`を呼ばなくても親のクリックに合わせて表示が変わりました。

`handle.update()`は、更新が反映された後に`AbortSignal`で解決するPromiseを返します。`await`すれば、DOMに反映された後にフォーカス移動などの処理を続けられます。

### 仮想DOMによる差分更新とDOMの再利用

「Remix 3には仮想DOMが無い」という説明を英語圏の解説でも見かけますが、ソースには`src/runtime/vdom.ts`と`reconcile.ts`があり、仮想ノードを比較して必要な箇所だけDOMを更新しています。READMEも`root.render()`を「Renders a tree, reusing existing DOM and component instances.」と説明しています。Reactとの違いは差分更新の有無ではなく、状態setterで更新を予約するか、変数を変更した後に開発者が`update()`で出すかです。

| 観点       | React             | Remix 3（component 1.2）        |
| -------- | ----------------- | ----------------------------- |
| 状態の置き場   | useState などのHooks | setup内のふつうの変数                 |
| 再描画のきっかけ | setState で自動      | handle.update() を明示的に呼ぶ       |
| イベント     | onClick（合成イベント）   | mix={on(‘click’, …)}（DOMイベント） |
| 振る舞いの再利用 | カスタムフック           | createMixin() で作るmixin        |
| 後片付け     | useEffect の戻り値    | handle.signal の abort         |

Reactのコンポーネントや、Reactに依存するUIライブラリはそのままでは使えません。Reactからの距離を小さく保ちたい場合は、互換レイヤーを持つ[Preact](/tech/details/16011/)のほうが移行の負担は軽くなります。

## 基本の書き方：Counterとmix={on()}

最小の例はクリックで数を増やすボタンです。次のコードをremix 3.2.0とnode v26.5.0でビルドし、jsdomで2回クリックして「Count: 2」になること、サーバー描画で`<button type="button">Count: 2</button>`を出力すること（初期値2を渡した場合）を確認しました。

```
import { on, type Handle } from 'remix/component'

export function Counter(handle: Handle<{ initialCount?: number }>) {
  let count = handle.props.initialCount ?? 0

  return () => (
    <button
      type="button"
      mix={on('click', () => {
        count++
        handle.update()
      })}
    >
      Count: {count}
    </button>
  )
}
```

JSXの変換先は`remix/component`です。`remix new`の雛形には設定済みですが、手で組む場合は`tsconfig.json`に次の2行を入れます。TypeScript 7.0.2の`--strict`で型エラーが出ないことも確認しました。

```
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "remix/component"
  }
}
```

### Remix Jam 2025のデモから変わった書き方

2025年10月時点の実験版と正式版では、同じカウンターでも書き方が違います。Jam時点の記事やサンプルを参考にする場合は、次の表に沿って読み替えてください。

| 項目        | Jam時点（2025-10）        | 正式版（3.0以降）           |
| --------- | --------------------- | -------------------- |
| ハンドルの受け取り | this: Remix.Handle    | 第1引数 handle: Handle  |
| 更新        | this.update()         | handle.update()      |
| イベント      | on={pressDown(…)}     | mix={on(‘click’, …)} |
| 独自の操作の定義  | createInteraction     | createMixin          |
| パッケージ     | @remix-run/dom・events | remix/component      |

Jam時点の`@remix-run/dom`はREADMEの冒頭に「DO NOT USE UNDER CONSTRUCTION」と書かれた実験版です。`createInteraction`は3.2.0の`remix`と`@remix-run/*`のどのパッケージにも含まれておらず、独自の振る舞いは後述の`createMixin()`で書きます。また、`remix/ui`だった入口は、正式版の数時間前に出た3.0.0-rc.5で`remix/component`へ改名されました。

## ToDoリストの実装：配列の状態管理とフォームイベント

状態が配列になっても考え方は同じです。配列をsetupの変数に置き、フォームの`submit`で要素を足してから`handle.update()`を呼びます。入力値はフォーム要素から`FormData`で読むので、入力のたびに状態を更新する必要はありません。

```
import { on, type Handle } from 'remix/component'

export function TodoApp(handle: Handle) {
  let todos: { id: number; text: string; done: boolean }[] = []
  let nextId = 1

  return () => (
    <section>
      <form
        mix={on('submit', (event) => {
          event.preventDefault()
          let form = event.currentTarget
          let text = String(new FormData(form).get('text') ?? '').trim()
          if (!text) return
          todos.push({ id: nextId++, text, done: false })
          form.reset()
          handle.update()
        })}
      >
        <input name="text" aria-label="新しいタスク" />
        <button type="submit">追加</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <label>
              <input
                type="checkbox"
                checked={todo.done}
                mix={on('change', (event) => {
                  todo.done = event.currentTarget.checked
                  handle.update()
                })}
              />
              {todo.text}
            </label>
          </li>
        ))}
      </ul>
      <p>残り {todos.filter((t) => !t.done).length} 件</p>
    </section>
  )
}
```

jsdom上で「牛乳を買う」「請求書を送る」の2件を追加すると「残り 2 件」、1件目のチェックボックスを押すと「残り 1 件」になり、送信後の入力欄が空に戻ることを3.2.0でも確認しました。`todo.done`を直接書き換えているのは、Remixが値の不変性を要求しないためです。

## 非同期処理と中断：AbortSignalの3つの入口

Remix 3は後片付けと競合の回避を、ブラウザ標準の`AbortSignal`にそろえています。入口は3つあり、用途で使い分けます。

| 入口                         | abortされるとき          | 主な用途               |
| -------------------------- | ------------------- | ------------------ |
| handle.signal              | コンポーネントが外れたとき       | タイマーやwindowのリスナー解除 |
| on() の第2引数 signal          | 同じハンドラが再入したとき・外れたとき | 検索入力のfetch取り消し     |
| await handle.update() の戻り値 | 次の描画が始まったとき・外れたとき   | 読み込み表示を出した後のfetch  |

検索欄のように入力が続く場面では、イベントハンドラが受け取る`signal`を`fetch`へ渡すだけで、前の要求が取り消されます。READMEは「Remix doesn’t handle a rejected async event handler」とし、中断された`fetch`は例外になると明記しているので、`try`で受けて、中断による失敗は黙って抜けるようにします。

```
import { on, type Handle } from 'remix/component'

function SearchInput(handle: Handle) {
  let results: string[] = []

  return () => (
    <div>
      <input
        type="search"
        aria-label="検索語"
        mix={on('input', async (event, signal) => {
          try {
            let q = encodeURIComponent(event.currentTarget.value)
            let res = await fetch(`/search?q=${q}`, { signal })
            if (!res.ok) throw new Error(`HTTP ${res.status}`)
            let data = await res.json()
            if (signal.aborted) return
            results = data.results
            handle.update()
          } catch (error) {
            if (signal.aborted) return
            console.error(error)
          }
        })}
      />
      <ul>{results.map((r) => <li>{r}</li>)}</ul>
    </div>
  )
}
```

`window`の`resize`や外部のストアのように要素の外にあるイベントは、`on()`では付けられません。setupから`handle.queueTask()`でブラウザ専用処理を予約し、その中で`window.addEventListener`を呼び、オプションに`{ signal: handle.signal }`を渡しておけば、コンポーネントが外れたときにリスナーも外れます。

## createMixinによる振る舞いの再利用

`mix`に渡す`on()`や`css()`は「mixin」と呼ばれる部品で、`createMixin()`を使えば自分でも作れます。Reactのカスタムフックにあたる使い回しの単位ですが、コンポーネントではなく要素に付けます。次の例は、要素がDOMに入った時点で入力欄にフォーカスを移すmixinです。

```
import { createMixin, type Handle } from 'remix/component'

export const focusOnInsert = createMixin<HTMLElement>(
  (handle) => {
    handle.addEventListener('insert', ({ node }) => node.focus())
  },
  { ssr: false },
)

export function SearchBox(handle: Handle) {
  return () => <input type="search" name="q" aria-label="検索語" mix={focusOnInsert()} />
}
```

第2引数の`{ ssr: false }`は3.1.0で入った指定で、サーバー描画ではそのmixinのsetupを飛ばします。試すと、サーバーの出力は`<input type="search" name="q" aria-label="検索語" />`のままで、ブラウザ側ではフォーカスが入力欄に移りました。比較用に`ssr`を指定しないmixinを並べると、そちらのsetupはサーバーで1回呼ばれていました。HTMLに何も足さない振る舞い（フォーカス、外側クリックの検知など）にだけ使い、属性やスタイルを付けるmixinには付けません。組み込みの`on()`と`ref()`も3.1からこの指定で動いています。

## 3.1・3.2で変わったコンポーネントの挙動

3.0.0の時点で書いたコードは、3.2.0でスタイルやARIA属性などの出力が変わります。[componentのCHANGELOG](https://github.com/remix-run/remix/blob/main/packages/component/CHANGELOG.md)で「Behavior change」とされた変更と、知っておくと設計が変わる追加を並べます。

### css()の数値と単位の扱い

3.2.0（component 1.2.0）から、`css()`と`style`は単位なしの数値を受け付けるプロパティで`px`を付けなくなりました。`css({ flex: 2, marginTop: 8, tabSize: 4, '--gapSize': '8px' })`をサーバー描画すると、出力は`flex: 2`・`margin-top: 8px`・`tab-size: 4`・`--gapSize: 8px`でした。以前は`flex: 2px`になり伸長比として効いていなかったので、数値の`flex`を使っていた画面はレイアウトが変わります。キャメルケースで書いたカスタムプロパティはケバブケースに変換されなくなったため、`var(--gap-size)`で読んでいる場合は定義側も`'--gap-size'`に直します。生成されるクラス名（`rmxc-*`）も変わるので、テストやセレクタで決め打ちしている箇所は書き換えが必要です。

### 3.1・3.2のSSR性能・Frame操作・CSPの変更

| 版     | 変更                                   | 影響                                |
| ----- | ------------------------------------ | --------------------------------- |
| 3.1.0 | サーバー描画の高速化                           | 48商品：513→244µs（Node 24.18・M5 Pro） |
| 3.1.0 | frame.reload({ src, method, body })  | 履歴を変えずにFrameへ送信                   |
| 3.1.0 | data-rmx-prefetch=”intent\|viewport” | リンク先のFrameを先読み                    |
| 3.2.0 | csp()ミドルウェア                          | nonce付きのCSPを自動で付与                 |
| 3.2.0 | aria-\*の真偽値                          | サーバーでも”true”・”false”を出力           |
| 3.2.0 | createRoot系のみ使用                      | Frame・run()未使用でruntime約40%減       |

真偽値の変更は、`<a aria-current={true} aria-hidden={false}>`のサーバー出力が`aria-current="true" aria-hidden="false"`になることで確かめました。以前は`aria-current`だけが値なしで出力され、支援技術には「false」として読まれていました。

## @remix-run/ui：Remix 3のヘッドレスUIプリミティブ

Remix 3向けのUI部品は、別パッケージの`@remix-run/ui`にあります。2026年11月時点の最新は0.13.1で、[README](https://github.com/remix-run/remix/blob/main/packages/ui/README.md)には「currently unstable and versioned independently. It is not available through the remix package.」と書かれています。完成済みの見た目を持たないヘッドレスな部品で、キーボード操作やARIA属性の付与に加え、位置合わせやアニメーションなども提供します。

| 読み込み先                                 | 役割                 |
| ------------------------------------- | ------------------ |
| @remix-run/ui/menu                    | メニュー               |
| @remix-run/ui/popover                 | ポップオーバー            |
| @remix-run/ui/select・listbox・combobox | 選択リスト・入力補完         |
| @remix-run/ui/tabs・accordion・toggle   | タブ・開閉・切り替え         |
| @remix-run/ui/anchor                  | 要素の位置合わせ           |
| @remix-run/ui/animation               | 出入りのアニメーション・spring |

振る舞いの大半は`mix`に渡すmixinとして提供され、部品どうしの状態を共有する範囲だけを`popover.Context`のようなコンポーネントで囲みます。ポップオーバーの例では、ボタンに`popover.anchor()`、パネルに`popover.surface({ open, onHide })`を付け、開閉状態は自分の変数と`handle.update()`で管理します。次のコードは0.13.1でも型検査を通りました。実行前に`npm install @remix-run/ui@0.13.1`で別パッケージを追加してください。

```
import { css, on, type Handle } from 'remix/component'
import * as popover from '@remix-run/ui/popover'

function ViewOptions(handle: Handle) {
  let open = false

  return () => (
    <popover.Context>
      <button
        type="button"
        mix={[
          popover.anchor({ placement: 'bottom-end' }),
          popover.focusOnHide(),
          on('click', () => {
            open = true
            handle.update()
          }),
        ]}
      >
        表示オプション
      </button>
      <div
        mix={[
          css({ background: 'white', padding: '8px' }),
          popover.surface({
            open,
            onHide() {
              open = false
              handle.update()
            },
          }),
        ]}
      >
        パネルの内容
      </div>
    </popover.Context>
  )
}
```

0.13.0では、toggleの改名に加え、削除済みの装飾付きコンポーネントの型やAccordionの未使用プロパティなども削除されました。切り替えのmixinは`toggle.control({ defaultChecked: true })`から`toggle({ defaultChecked: true })`に変わり、読み込みも`import { toggle } from '@remix-run/ui/toggle'`になっています。同じ版で、動きを減らす設定を読む`prefersReducedMotion()`が`@remix-run/ui/animation`に加わりました。サーバー描画では`false`を返すので、描画関数の中から呼べます。

`css()`で付けたスタイルは`rmx`というカスケードレイヤーに入ります。同じオリジンの通常宣言では、どのレイヤーにも属さないCSSのほうが優先されるため、既存のリセットCSSを素のまま置くと部品のスタイルが上書きされます。componentのREADMEの手順どおり、`@layer base, rmx;`で順序を宣言したうえで、リセットCSS自体を`@layer base { ... }`の中へ移してください。順序の宣言だけでは既存のCSSはレイヤーに入りません。

## SSRと部分ハイドレーション：clientEntryとFrame

### clientEntryによる部分ハイドレーション

Remix 3はサーバーでHTMLを描画し、ブラウザでは`clientEntry`で指定した部品だけを動かします。READMEの説明は「hydrate selected components of a server-rendered page, leaving the rest as plain HTML」で、[アイランドアーキテクチャ](/tech/details/2423/)と同じく静的な部分にはJavaScriptを送りません。

```
import { clientEntry, on, type Handle } from 'remix/component'

export const Counter = clientEntry(
  import.meta.url,
  function Counter(handle: Handle<{ initialCount?: number }>) {
    let count = handle.props.initialCount ?? 0
    return () => (
      <button type="button" mix={on('click', () => { count++; handle.update() })}>
        {count}
      </button>
    )
  },
)
```

第1引数は`moduleUrl#ExportName`の形の識別子で、`remix new`の雛形とREADMEは`import.meta.url`を渡しています。`#ExportName`を省くと関数名が使われるため、関数名とエクスポート名をそろえます。ブラウザ側では`run({ loadModule })`がページ内の目印を探してモジュールを読み込みます。

propsはページに書き出されてブラウザへ渡るので、文字列・数値・プレーンなオブジェクト・JSX要素など直列化できる値に限られます。関数をpropsに持たせると、TypeScriptの段階で`never`型のエラー（TS2345）になりました。JSX要素に付けられるmixinも`css()`だけです。イベントハンドラを親から渡す設計はここで行き詰まるので、対話の単位ごとにclientEntryを切ります。3.2.0からは、JSONから読んだだけの「要素の形をしたオブジェクト」は描画されません。`{"$rmx":true,"type":"div",...}`を子に渡すと、サーバーの出力は空の`<div></div>`で、ブラウザ側では例外になりました。DBやAPIの値でマークアップを差し込まれる経路が塞がれています。

### Frameによる部分描画とソフトナビゲーション

`<Frame src="/sidebar">`はサーバーで描画したHTMLをページの一部に流し込む部品です。`fallback`を渡すと初回HTMLは読み込み表示で先に返り、本体は後から流れてきます。渡さないと、その中身がそろうまで待ちます。3.1.0からは`frame.reload({ src, method, body })`で、ブラウザの履歴を変えずにFrameへフォームの内容を送り、返ってきたHTMLで差し替えられます。リンクに`data-rmx-prefetch="intent"`を付ければ、ホバーやフォーカスの時点で遷移先を先読みします。

また`run()`を呼ぶと、同一オリジンのリンクとフォームはNavigation API経由でHTMLを取り直し、ページ全体を読み込み直さずに差し替えます。READMEによると、この挙動には`window.navigation`と`NavigateEvent.sourceElement`の両方が必要で、どちらかが無いブラウザでは通常のページ遷移になります。特定のリンクだけ通常遷移にしたいときは`data-rmx-document`属性を付けます。

サーバーとクライアントの描画の分担そのものは[レンダリングとCSR・SSR・SSGの違い](/tech/details/13578/)で整理しています。HTMLを部分的に差し替えるという点では[htmx](/tech/details/15371/)やHotwireのTurbo Framesと発想が近く、違いはクライアント側の部品をJSXで書ける点にあります。

## 採用判断：Remix 3を選ぶ場面と避ける場面

Remix 3は3.2まで来ましたが、新規の業務アプリで既定の選択肢にするのはまだ早いと判断します。理由は、UI部品が不安定版のままで、Reactの部品資産を使えないためです。3.2.0でも`css()`の出力が変わったように、マイナー版で挙動が動く段階でもあります。一方、次の条件がそろうなら試す価値があります。

- 画面の大半がサーバー描画の静的HTMLで、対話する部品が限られている
- ルーティング、セッション、DBアクセス、UIまでの提供元を1つにそろえたい
- Node.js 24.3以上を本番で使える

反対に、次のどれかに当てはまる場合は避けるべきです。

- MUIやshadcn/uiなどReact向けUIライブラリを前提にした画面が多い（Remix 3のコンポーネントとして動かない）
- Remix v2やReact Routerの既存アプリを移したい（Remix 3は別系統で、コンポーネントは書き直しになる）
- デザインシステムの部品を長期に固定したい（@remix-run/uiは0.x系で、0.13.0でもtoggleの書き方が変わった）

既存のRemix v2アプリを持っているなら、先にReact Router v7・v8への移行を検討してください。クライアントだけで完結させたい場合の制約は[Remix SPAモードの解説](/tech/details/3840/)に、Remix 3のサーバー側でgRPCを扱う例は[gRPC×Connect-ESをRemixで使う実装](/tech/details/7063/)にまとめています。

### update()の呼び忘れとsetup中の呼び出しによる失敗

状態を変えたのに画面が変わらないときは、まず`handle.update()`の呼び忘れを疑います。Counterから`handle.update()`だけを消した版をjsdomで2回クリックすると、変数は2になっていても表示は「Count: 0」のままでした。エラーも警告も出ないため、気づきにくい不具合です。

逆に、setupの中で`handle.update()`を呼ぶと、「Ignored handle.update() while SetupUpdate is running its setup function.」という警告が出て無視されます（SetupUpdateは試験用に付けたコンポーネント名）。初回の描画はsetupの後に必ず行われるため、setup中の更新呼び出しは不要です。READMEは、描画関数の中で呼ぶと例外になること、サーバー描画中の`update()`も例外になることを明記しています。DOMができてから実行したい処理は`handle.queueTask()`に渡します。

## よくある質問

### Remix 3はReactを使っていますか？

使っていません。`remix` 3.2.0の依存49個はすべて`@remix-run/`配下で、`react`は含まれていません。2025年5月の方針発表では「Preactのフォークから始める」と書かれていましたが、正式版はPreactにも依存していません。

### Remix 3の最新バージョンはいくつですか？

2026年11月10日時点のnpmの`latest`は3.2.0（11月4日公開）です。3.0.0の正式版は10月1日、その直前のリリース候補が3.0.0-rc.5でした。UI部品の`@remix-run/ui`は0.13.1の不安定版で、正式版の範囲に含まれていません。

### Remix v2のアプリはRemix 3へ移行できますか？

コンポーネントの書き方が別物なので、移行というより書き直しになります。Remix v2の後継として公式に案内されているのはReact Routerで、2026年10月27日時点で8.5.1とv7系の7.18.5が並行して更新されています。

### Remix 3で使えるUIコンポーネントライブラリはありますか？

公式の`@remix-run/ui`があり、menu・popover・select・tabs・combobox・accordionなど9種類の振る舞いと、アニメーション用の部品を提供しています。見た目を持たないヘッドレス部品なので、スタイルは`css()`や既存のCSSで自分で付けます。React向けのUIライブラリは使えません。

### Remix 3にuseStateやuseEffectはありますか？

ありません。状態はsetupで宣言した変数に置き、変更後に`handle.update()`を呼びます。useEffectの後片付けにあたる処理は`handle.signal`のabortで、描画後の処理は`handle.queueTask()`で書きます。カスタムフックのような使い回しは`createMixin()`で作ります。

## 関連記事

- [PESPAとは？SPA・MPAとの違いとReact Router v8・Remix 3での実装【2026年10月版】](/tech/details/2279/)
- [Remix SPAモードとは？ssr:falseの設定と制約、React Router v8での違いと移行【8.5.1実測】](/tech/details/3840/)
- [gRPC×Connect-ESをRemixで使う実装｜React Router v8のloaderとRemix 3から呼ぶ【2.3.0実測】](/tech/details/7063/)
- [Preactとは？Reactとの違い・導入手順・compat移行と採用判断【2026年9月版】](/tech/details/16011/)
- [アイランドアーキテクチャとは？仕組みとAstro 7の実装・JS量の実測、PPRとの違い](/tech/details/2423/)

---

出典: [Remix 3のコンポーネントとは？handle・mixの書き方と3.2の変更点、@remix-run/ui](<https://www.issoh.co.jp/tech/details/10504/>)（株式会社一創）
