From 090b156bff490d381d9b65ab3289e099abae10f0 Mon Sep 17 00:00:00 2001 From: Sergey Korolev Date: Wed, 7 Oct 2026 13:17:41 +0300 Subject: [PATCH] Document inertia-native 1.0 Install without @beta, initInertiaNative, @inertiajs/core 3, Vue and Svelte bindings, restored, proposeFormRedirects, in-page visits, location visits and historical locations. Bumps the registry submodule to the inertia-native imports. --- docs/components/overview.md | 23 ++++++++- docs/guide/installation.md | 28 ++++++----- docs/guide/introduction.md | 16 ++++--- docs/guide/navigation.md | 80 ++++++++++++++++++++++++++++++-- docs/reference/api.md | 56 +++++++++++++++++++--- vendor/hotwire-bridge-components | 2 +- 6 files changed, 174 insertions(+), 31 deletions(-) diff --git a/docs/components/overview.md b/docs/components/overview.md index bc1be3e..4faebdd 100644 --- a/docs/components/overview.md +++ b/docs/components/overview.md @@ -17,7 +17,8 @@ regular browser. ## `useBridgeComponent` -The React entry exposes one generic primitive: +Each framework entry exposes one generic primitive — the examples below use +React; Vue and Svelte have the same shape (see the [API](/reference/api#usebridgecomponent)): ```js import { useBridgeComponent } from 'inertia-native/react' @@ -64,6 +65,26 @@ useEffect(() => { Without the cleanup, a changed prop leaves the old callback registered and every subsequent interaction is reported twice. +## Restoring after a native screen + +On Android, coming back to a web screen from a native one dispatches +`native:restore`, and native may have dropped the UI a component drew on +`connect`. `useBridgeComponent` exposes this as `restored`, a counter. Put it +in the deps of the effect that sends `connect`, so it is sent again: + +```js +const { supported, send, restored } = useBridgeComponent('button') + +useEffect(() => { + if (!supported) return + const id = send('connect', { title }, () => onTapRef.current?.()) + return () => window.HotwireNative?.web?.removeCallback(id) +}, [supported, title, send, restored]) +``` + +Nothing is resent automatically: a component that only sends when called (like +`alert`) must not fire again. + ## Ready-made components Rather than writing each component from scratch, you can copy one from the diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 9df81b4..a793557 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -1,37 +1,43 @@ # Installation -::: warning Beta -Published under the `beta` tag while the API stabilizes. Install with `@beta`. -::: - ```bash -npm add inertia-native@beta +npm add inertia-native ``` ## Requirements -- `@inertiajs/core` >= 2.0 (works with the v3 line) +- `@inertiajs/core` >= 3.0 - `react` >= 18 — only for the `inertia-native/react` entry +- `vue` >= 3.0 — only for the `inertia-native/vue` entry +- `svelte` >= 4.0 — only for the `inertia-native/svelte` entry ## Setup -Call `initHotwireNative()` once, **before** `createInertiaApp`, in your Inertia +Call `initInertiaNative()` once, **before** `createInertiaApp`, in your Inertia entrypoint: ```js import { createInertiaApp } from '@inertiajs/react' -import { initHotwireNative } from 'inertia-native' +import { initInertiaNative } from 'inertia-native' -const isHotwireNative = !!window.webkit?.messageHandlers?.turbo -initHotwireNative({ debug: import.meta.env.DEV || isHotwireNative }) +const isNativeApp = !!window.webkit?.messageHandlers?.turbo +initInertiaNative({ debug: import.meta.env.DEV || isNativeApp }) createInertiaApp({ /* ... */ }) ``` That's all that's needed for native navigation (push/pop/replace/restore, -modals, forms, error screens, pull-to-refresh). +modals, forms, error screens, pull-to-refresh). See the [API](/reference/api) +for the available options. To set up the native iOS and Android shells that load your app, see [Native apps → iOS](/native/ios) and [Android](/native/android). +## Upgrading from 0.1.0-beta + +- `initHotwireNative` is now `initInertiaNative`. +- `@inertiajs/core` 2.x is no longer supported. +- The package was published as `inertia-hotwire-native` before + `0.1.0-beta.2`; replace that dependency with `inertia-native`. + Next: [Navigation](/guide/navigation). diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index 46de47c..1ce16fd 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -5,10 +5,9 @@ lets an [Inertia.js](https://inertiajs.com) application run inside [Hotwire Native](https://native.hotwired.dev) (iOS & Android) and behave like a native app. -::: warning Beta & unofficial -This is a pre-1.0, community-built integration. It is **not** an official -Inertia.js or Hotwire project, and the API may still change between beta -releases. +::: info Unofficial +This is a community-built integration. It is **not** an official Inertia.js or +Hotwire project. ::: ## How it works @@ -22,12 +21,15 @@ bridge components (submit buttons, menus, etc.). In a regular browser it stays inert: with no native adapter connected, Inertia navigates exactly as usual. -## Two entry points +## Entry points - **Framework-agnostic core** (`inertia-native`) — peer-depends on `@inertiajs/core`. Handles all navigation. -- **React bindings** (`inertia-native/react`) — optional, peer-depends - on `react`. Provides `useBridgeComponent` for bridge components. +- **Bridge component bindings** — optional, each peer-depends on its framework + and provides `useBridgeComponent`: + - `inertia-native/react` + - `inertia-native/vue` + - `inertia-native/svelte` ## The ecosystem diff --git a/docs/guide/navigation.md b/docs/guide/navigation.md index bc9cf9a..30f5c5a 100644 --- a/docs/guide/navigation.md +++ b/docs/guide/navigation.md @@ -1,6 +1,6 @@ # Navigation -Once [`initHotwireNative()`](/reference/api#inithotwirenative) is called, every +Once [`initInertiaNative()`](/reference/api#initinertianative) is called, every Inertia navigation is mapped onto the native navigation stack. You don't change how you write Inertia links or visits — the shim translates them. @@ -11,24 +11,94 @@ how you write Inertia links or visits — the shim translates them. | Link tap (`` / `router.visit`) | Cancelled in the web view; the native side presents the destination (push / modal). | | `replace: true` visits | Replaces the current native screen instead of pushing. | | Back / restore | Restores the cached page from Inertia's history (no re-fetch); falls back to a fresh request if there's no cached entry. | -| Form submissions (non-GET) | Stay in the web view; the native side is notified via form-submission events. | +| Form submissions (non-GET) | Stay in the web view; the native side is notified via form-submission events. See [Form redirects](#form-redirects). | | HTTP errors (404 / 500) & network failures | Surface the native error screen for the failed visit. | +| A non-Inertia page (e.g. a classic Turbo page) | Native reloads the screen, so the page loads as a full document. | +| Asset version mismatch (409) | Native reloads the screen. | +| `inertia_location` / `Inertia::location` | Handed to native, which decides where it opens — Safari, an in-app browser, a modal. | | Pull-to-refresh | Treated as an in-place reload of the current URL. | +## Visits that stay on the page + +Some visits update the current page rather than open a new one. These run in +the web view as usual and never reach native: + +- partial reloads (`only`, `except`, `reset`) — including `router.reload`, + `usePoll`, ``, `` and deferred props +- `async` visits, and visits with `preserveState: true` or `preserveUrl` +- visits to the current URL + +Prefetches (``, `router.prefetch`) are cancelled: the next +screen may load in another web view, so the prefetched response would be +wasted. Hotwire Native's own `turbo.js` disables prefetching for the same +reason. + ## Modals A visit presented as a modal is driven by your native path configuration (Hotwire Native's `path-configuration` JSON), not by the package — the package just proposes the visit and reports its lifecycle. See the native setup guides. +## Form redirects + +By default a form's result stays in the web view the form was in, so a form in +a modal shows its result inside the modal. Pass `proposeFormRedirects: true` to +propose the page a form lands on to native instead, as Turbo does — your path +configuration then decides, e.g. dismissing the modal to show the result in the +main stack: + +```js +initInertiaNative({ proposeFormRedirects: true }) +``` + +A form that lands back on its own URL — validation errors — is not proposed. + +## Historical locations + +To dismiss a modal or pop a screen after a form, answer the form with one of +Hotwire Native's historical locations: `/recede_historical_location` (pop), +`/refresh_historical_location` (refresh) or `/resume_historical_location` +(stay). Native handles these paths itself; no path configuration is needed. + +A plain redirect does not work with Inertia: Inertia follows it inside its own +request and native never sees it. Answer with an Inertia location visit +instead, which this package hands to native. + +With [turbo-rails](https://github.com/hotwired/turbo-rails), its +`recede_or_redirect_to`, `refresh_or_redirect_to`, `resume_or_redirect_to` and +`*_or_redirect_back_or_to` helpers all go through one private method. Override +it in your `ApplicationController` to answer Inertia requests from the native +app with a location visit: + +```ruby +private + +def turbo_native_action_or_redirect(url, action, redirect_type, options = {}) + if turbo_native_app? && request.inertia? + native_params = options.delete(:native_params) || {} + inertia_location send("turbo_#{action}_historical_location_url", notice: options[:notice], **native_params) + else + super + end +end +``` + +This overrides a private turbo-rails method, so check it when you upgrade +turbo-rails. Without turbo-rails, the demo app's +[`NativeSupport`](https://github.com/inertia-native/demo-rails/blob/main/app/controllers/concerns/native_support.rb) +concern has standalone versions of the three helpers. + +These helpers are for form responses. A GET link that native opens as a new +screen would reload that screen rather than pop it. + ## Debugging -Pass `debug: true` to `initHotwireNative()` to log the web ↔ native message flow +Pass `debug: true` to `initInertiaNative()` to log the web ↔ native message flow to the web view console. Attach Safari Web Inspector (iOS) or Chrome remote -inspect (Android) to read it. +inspect (Android) to read it. Messages are prefixed `[inertia-native]`. ```js -initHotwireNative({ debug: true }) +initInertiaNative({ debug: true }) ``` Next: [Bridge components](/components/overview). diff --git a/docs/reference/api.md b/docs/reference/api.md index 315f56b..0591f7e 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,11 +1,14 @@ # API -## `initHotwireNative` +## `initInertiaNative` ```ts -import { initHotwireNative } from 'inertia-native' +import { initInertiaNative } from 'inertia-native' -initHotwireNative(options?: { debug?: boolean }): void +initInertiaNative(options?: { + debug?: boolean + proposeFormRedirects?: boolean +}): void ``` Installs the `window.Turbo` shim that Hotwire Native's injected `turbo.js` @@ -15,17 +18,25 @@ inert. | Option | Type | Default | Description | | --- | --- | --- | --- | | `debug` | `boolean` | `false` | Log the web ↔ native message flow to the web view console. | +| `proposeFormRedirects` | `boolean` | `false` | After a form lands on another URL, propose that URL to native, as Turbo does. See [Form redirects](/guide/navigation#form-redirects). | The call is idempotent — a repeat call (HMR, double import, React StrictMode) is a no-op. ## `useBridgeComponent` +The same primitive ships for React, Vue and Svelte. It returns whether the +connected native app supports a component, a `send` function, and a `restored` +counter. + +### React + ```ts import { useBridgeComponent } from 'inertia-native/react' useBridgeComponent(component: string): { supported: boolean + restored: number send( event: string, data?: Record, @@ -34,10 +45,43 @@ useBridgeComponent(component: string): { } ``` -Generic React wrapper over the web bridge core (`window.HotwireNative.web`). +### Vue + +```ts +import { useBridgeComponent } from 'inertia-native/vue' + +useBridgeComponent(component: string): { + supported: Ref + restored: Ref + send(event, data?, callback?): string | null +} +``` + +A composable: call it from `setup`. Cleanup runs on unmount. + +### Svelte + +```ts +import { useBridgeComponent } from 'inertia-native/svelte' + +useBridgeComponent(component: string): { + supported: Readable + restored: Readable + send(event, data?, callback?): string | null +} +``` + +Call it during component initialization — it registers an `onDestroy` cleanup. +Read the stores as `$supported` and `$restored`. + +### Fields - `supported` — whether the connected native app supports `component`. Re-checks when the native handshake completes after mount. +- `restored` — bumped each time the web view comes back from a native screen + (`native:restore`, dispatched on Android). A component that draws native UI on + `connect` should send it again when this changes; see + [Restoring after a native screen](/components/overview#restoring-after-a-native-screen). - `send` — sends a message to native. Returns the message id (for callback cleanup), or `null` if it was queued or the component is unsupported. @@ -58,6 +102,6 @@ The package installs and reads these on `window`: | Global | Set by | Purpose | | --- | --- | --- | -| `window.Turbo` | `initHotwireNative()` | Shim that `turbo.js` drives for navigation. | -| `window.HotwireNative.web` | `initHotwireNative()` | Web side of the bridge (`send`, `supportsComponent`, …). | +| `window.Turbo` | `initInertiaNative()` | Shim that `turbo.js` drives for navigation. Internal. | +| `window.HotwireNative.web` | `initInertiaNative()` | Web side of the bridge (`send`, `supportsComponent`, `removeCallback`, …). The name is Hotwire Native's bridge protocol. | | `window.webkit.messageHandlers.turbo` | WKWebView (iOS) | Feature-detect that the app runs inside the native shell. | diff --git a/vendor/hotwire-bridge-components b/vendor/hotwire-bridge-components index 8b6d14c..ca539f5 160000 --- a/vendor/hotwire-bridge-components +++ b/vendor/hotwire-bridge-components @@ -1 +1 @@ -Subproject commit 8b6d14cebadc721efc3400795c08583c2fd89eff +Subproject commit ca539f5013a9be7c115d1ae86eb213ffa68b154c