# Technical Documentation: onejs/one

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/onejs/one)

### Overview (README.md)

# One

We'll get this filled out properly soon.

**[See our documentation website](https://onestack.dev)** for more.

---

### Apps Onestack.Dev Data Docs Native Overview (`apps/onestack.dev/data/docs/native-overview.mdx`)

---
title: One Native
description: Build native screens and use platform services from the One package
---

One Native is the native component and service library included in `one`.
Use it alongside React Native views and One's file router. It supplies SwiftUI
on iOS, Jetpack Compose on Android, shared components, and native services.

Start with [setup](/docs/native-setup), choose your
[components](/docs/native-components), and look up the
[platform APIs](/docs/native-apis). Read the
[platform notes](/docs/native-platforms) before sharing a screen across targets.
The [Native reference](/native) has a page for each component family and service.

## Imports and namespaces

```tsx
import { One, useNativeState, useSizeClass } from 'one'
```

| Namespace | What it contains |
| --- | --- |
| `One.iOS` | SwiftUI components and iOS services such as Location, Audio, and Photos |
| `One.Android` | Compose components, Android colors, and native menus |
| `One.UI` | Shared components: Image, Map, Icon, TextInput, effects, fonts, and safe areas |
| `One` | Shared services such as Notifications, Clipboard, Browser, and SecureStore |

Hooks are named imports. For example, `useNativeState` creates state that bound
native controls can share; `useSizeClass` reads the current window size class.
Components and services use the `One` namespace.

## Choose a platform explicitly

`One.iOS` and `One.Android` expose different native frameworks. Keep the
platform branch outside the component that renders those views. A web route
can render ordinary React DOM content:

```tsx
import { useState } from 'react'
import { One } from 'one'

export function NotificationsSetting() {
  const [enabled, setEnabled] = useState(true)

  if (One.platform === 'ios') {
    return (
      <One.iOS.Toggle
        label="Notifications"
        isOn={enabled}
        onIsOnChange={setEnabled}
      />
    )
  }
  if (One.platform === 'android') {
    return (
      <One.Android.Switch
        label="Notifications"
        isOn={enabled}
        onIsOnChange={setEnabled}
      />
    )
  }
  return (
    <label>
      <input
        type="checkbox"
        checked={enabled}
        onChange={(event) => setEnabled(event.currentTarget.checked)}
      />
      Notifications
    </label>
  )
}
```

You can also put native implementations in `.ios.tsx` and `.android.tsx`
files and the web implementation in `.tsx`. See
[platform-specific routing](/docs/routing).

The routed `Stack` and `Tabs` remain the navigation layer for file routes.
`One.iOS.NavigationStack` and `One.iOS.TabView` are separate primitives for
explicit SwiftUI composition. Choose the layer that owns the navigation you
are building; see the [component guide](/docs/native-components).

## Credit

One native services use [Nitro Modules](https://github.com/margelo/nitro),
created by Marc Rousavy and Margelo. Views are Fabric components, not Nitro
views.

---

### Releasing (`docs/releasing.md`)

# Releasing One

## Stable releases

Main pushes do not publish packages. Keep unreleased features on one release
branch and assemble them into a single minor release PR. Test branch canaries
and the installed packages in downstream apps. Prepare the minor version in
that PR, then ask the owner to review the complete batch before merging and
cutting the stable minor. Do not release features as patches.

Patch releases contain reviewed bug fixes only. Do not cut a patch from a
source containing unreleased features. Dependency and documentation maintenance
need no library release when published runtime bytes are unchanged.

`main` requires the `checks` and `tests` jobs on every pull request. After the
approved release PR merges and full CI passes for that exact source, publish
with an explicit workflow dispatch:

```sh
gh workflow run release.yml --repo onejs/one --ref main -f release=minor
```

A version prepared in the PR above npm latest is published as is, provided its
bump matches the requested release kind. A patch dispatch fails when main
carries a prepared minor. If no newer version was prepared, the workflow starts
from npm latest and creates the requested bump on its release tag. It never
pushes a generated version commit directly to protected main.

Prepare a feature batch version on its branch without publishing:

```sh
bun scripts/release.ts --minor --ci --dirty \
  --skip-publish --skip-push --skip-tests --skip-native-tests
```

Run `bun install`, commit the prepared version with the batch, and validate the
complete PR before owner review. `release=republish` publishes the version main
already carries after exact-source full CI; use it only for an approved prepared
version or to finish that version's interrupted publication.

## Canaries

Pushing `v2-beta` publishes a canary automatically. Canaries publish the
prepared tree to the `canary` dist-tag and mutate no git at all. They build
the packages without requiring full CI and never move `latest`.

For an explicit branch canary:

```sh
gh workflow run release.yml --repo onejs/one --ref <branch> -f release=canary
```

Manual canaries use a queue per branch, separate from push canaries. A newer
`v2-beta` push cannot replace a queued manual canary.

One V2 canaries use `2.0.0-0.canary.<timestamp>`. Pin the printed version
because the shared `canary` tag moves. One branch canaries need no owner approval. Branch canaries and normal beta
releases use separate queues.

To test an upstream fix downstream without publishing anything at all, prefer:

```sh
bun release --into ~/<downstream>
```

Verify a canary by its content: run `npm pack <pkg>@<version> --ignore-scripts`,
extract it, check `releaseSourceCommit` in its manifest, and inspect the changed
source or built output. A local `--into` build can keep a version while changing
code, so the version string alone is insufficient.

## V2 beta branch

Pushing `v2-beta` publishes a canary. Publish a V2 beta on the `beta` dist-tag
with a manual workflow dispatch:

```sh
gh workflow run release.yml --repo onejs/one --ref v2-beta -f release=beta
```

The workflow requires successful full CI for the exact current `v2-beta` SHA,
then publishes versions named `2.0.0-beta.<workflow-run>.<attempt>` on the npm
`beta` dist-tag. It does not push a version commit, create a tag, or create a
GitHub release. Packages marked `skipPublish` remain excluded.

The publish step verifies itself: after `npm publish` returns it polls the
registry for every package in the publish set and fails the run if any version
is still missing, so a green Release run means those versions are readable on
npm. The registry publishes version documents asynchronously and can take
several minutes per package, which is why this polls rather than checking once.
A registry check in the first few minutes after a run is not evidence that a
package was skipped.

Inspect a published beta by its content:

```sh
npm pack one@2.0.0-beta.<workflow-run>.<attempt>
```

---

### Apps Onestack.Dev Data Docs Cli (`apps/onestack.dev/data/docs/cli.mdx`)

---
title: CLI
---

One provides a single CLI for development, building, and serving your apps. Run `npx one --help` for the full list, or `npx one <command> --help` for one command.

## npx one

Scaffolding and entry point:

```bash
npx one [name]
```

With no arguments inside an existing app (a `vite.config.ts` is present), this runs the dev server. Otherwise it starts the create flow, scaffolding a new app into `[name]`.

## one dev

Start the development server. See [one dev](/docs/one-dev) for details.

```bash
one dev [options]

--clean           # clear all caches before running
--host            # set the hostname to bind to
--port            # set the port to bind to
--mode            # "development" or "production": run the dev server against the production bundle
--debug-bundle    # write the bundle to a temp file and serve it from there for debugging
--debug           # pass debug args to Vite
--extra-config    # path to an extra Vite config file to merge on top of the project config
```

The One development server serves both web and native apps on the same port at the same time, and should hot reload both at once. When you see the `Server running on` message with a host and port, that is the same for both native and web.

## one build

Build your app for production. See [one build](/docs/one-build) for details.

```bash
one build [options]

--platform web|ios|android   # what to build (default: web)
--only <pattern>             # limit the pages built to a micromatch pattern
--step <name>                # run a single internal build step
--skip-env                   # skip loading .env files during build
```

## one serve

Serve a built web app for production. See [one serve](/docs/one-serve) for details.

```bash
one serve [options]

--host          # set the hostname to bind to
--port          # set the port to bind to
--compress      # enable gzip compression (default: true)
--loadEnv       # load .env files before running
--outDir        # build output directory (default: dist)
--cluster       # enable cluster mode using all CPU cores
--cluster=N     # enable cluster mode with N workers
```

You must run `one build` before serve.

### Programmatic Usage

You can also run your production server programmatically:

```tsx
import { serve } from 'one/serve'

await serve()
```

Which takes options:

```tsx
type ServeOptions = {
  // you can pass in your own Hono server
  app?: Hono

  host?: string
  port?: number
  compress?: boolean
  outDir?: string

  /**
    * Whether to run the Vite logic to load .env files before running the server
    * @default false
    */
  loadEnv?: boolean

  /**
    * Enable cluster mode with multiple worker processes.
    * Pass true to use all CPU cores, or a number for specific worker count.
    */
  cluster?: boolean | number
}
```

## one prebuild

Generate the native Xcode and Android projects. Requires `native.app` in your Vite config with the app name, iOS bundle ID, and Android application ID. See the [Build or Run the Native iOS App](/docs/guides-ios-native) guide.

```bash
one prebuild [options]

--platform ios|android   # which native project to generate
--no-install             # skip installing native dependencies
```

Apps with `expo` in their dependencies use Expo prebuild instead; see [Coming from Expo](/native/migrating-to-one#keeping-expo-prebuild).

## one run:ios

Build and launch the app on an iOS simulator. Generates the native project with `prebuild` first when `./ios` does not exist yet. Start `one dev` first so the app can connect to the bundler.

```bash
one run:ios [options]

--simulator <name>   # explicitly set the simulator to use, e.g. "iPhone 16"
--udid <udid>        # explicitly set the simulator to use by UDID
```

## one run:android

Build and launch the app on Android. Generates the native project with `prebuild` first when `./android` does not exist yet. Start `one dev` first so the app can connect to the bundler.

```bash
one run:android
```

## one clean

Remove build folders and caches for the current project.

```bash
one clean
```

## one patch

Apply package patches configured via the [`patches` option](/docs/configuration#patches) in your Vite config. Patches are automatically applied during `one dev` and `one build`, but you can run this command to apply them manually.

```bash
one patch [options]

--force    # re-apply all patches, even if already applied
```

## one generate-routes

Generate route type definitions (`app/routes.d.ts`). See [Typed Routes](/docs/routing-typed-routes) for details.

```bash
one generate-routes [options]

--appDir <path>        # path to app directory (default: "app")
--typed type|runtime    # auto-generate route helpers alongside the types
```

## one typegen

Alias for `one generate-routes`. Takes the same options.

```bash
one typegen [--appDir <path>] [--typed type|runtime]
```

## one metro-eject

Write `babel.config.cjs` and `metro.config.cjs` into your project so you can own and customize the Metro configs freely. The generated files delegate to `one/babel-preset` and `one/metro-config`. See [Metro Mode](/docs/metro-mode) for details.

```bash
one metro-eject [options]

--force    # overwrite existing files
```

## one daemon

Multi-app development server proxy. Coordinates several One dev servers behind one port.

```bash
one daemon [subcommand] [options]

start (default)   # start the daemon
stop              # stop the daemon
status            # show running servers and active routes
route             # route a bundle ID to a server slot

--port <port>       # port to listen on (default: 8081)
--host <host>       # host to bind to (default: 0.0.0.0)
--app <bundle-id>   # bundle ID for the route subcommand
--slot <n>          # slot number for the route subcommand
--project <path>    # project path for the route subcommand
--tui               # show TUI (default: true if TTY)
```

## one updates publish

Bundle the native JS and publish an over-the-air update directory with `manifest.json` and assets. Requires `native.app.updates.runtimeVersion` in your Vite config, so the binary only takes updates published for its own runtime version. See [Over-the-air Updates](/native/updates) for the full flow.

```bash
one updates publish [options]

--platform ios|android       # which platform to bundle for (required)
--out <dir>                  # directory to write manifest.json and assets into (required, must be empty)
--metadata key=value          # manifest metadata (repeatable, values parse as JSON scalars)
--intermediatesOut <dir>     # separate directory for the local debug bundle and composed source map
```

---

### Apps Onestack.Dev Data Docs Common Issues (`apps/onestack.dev/data/docs/common-issues.mdx`)

---
title: Common Issues
---

This document collects tips and tricks for debugging common issues. If you'd like to see a workaround documented here, please [submit it to our GitHub issues](https://github.com/onejs/one).

### General errors when server rendering

If you see an error while rendering from the server, it can often be hard to diagnose. There's a few ways to debug it.

First, we've found turning off Vite external dependencies entirely will often help, what it does is make Vite use esbuild and apply CommonJS transforms for compatibility, transforming all of them into your `.vite` directory. You can enable it like so:

```tsx fileName=vite.config.ts
{
  ssr: {
    noExternal: true
  }
}
```

The reason we don't turn this on by default it is can cause other issues, namely if a node module relies on, say, __dirname, or __filename, then when they are transformed into the `.vite` folder these paths will change. Another reason we don't turn it on by default is that it can slow things down.

If you do find the individual module that is causing an issue, you can also configure it quickly using the `patches` option in the [One Vite plugin configuration](/docs/configuration#patches).

### Uncaught SyntaxError: The requested module ... does not provide an export named 'default'

In general you want to add any dependency like this to `optimizeDeps.exclude` in your Vite config.

### `require` is not defined

You can try using one of the commonjs plugins, either `vite-require` or `vite-plugin-commonjs`.

### Common React Native package issues

When using bundlers like Vite, Rollup, or esbuild, developers often encounter issues with React Native packages originally designed to work only for Metro. Metro’s tolerant nature allows certain practices that other bundlers may not support, such as:

* Including Flow types in distributed `.js` files.
* Including JSX syntax in `.js` files instead of `.jsx`.
* Releasing packages as ESM-only without proper declarations in `package.json` (e.g., missing `"type": "module"`) or appropriate `.mjs` extensions.
* Omitting import path extensions in distributed ESM, which can [prevent Node.js from running those modules correctly](https://nodejs.org/docs/latest-v20.x/api/esm.html#mandatory-file-extensions), and break SSR.

These issues can break builds and require specific handling to make the packages compatible with non-Metro bundlers. Related error messages you might encounter includes:

* `The JSX syntax extension is not currently enabled.`
* `The esbuild loader for this file is currently set to "js" but it must be set to "jsx" to be able to parse JSX syntax.`
* `SWC failed to transform file` following with a syntax error parsing something like `import type`.

To workaround these issues, you can use the package patching feature of One to fix the packages that are causing problems. Here's an example:

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      // ...
      patches: {
        'react-native-vector-icons': {
          '**/*.js': [
            'jsx', // Transpile JSX in .js files
            'flow', // Remove flow types
          ],
        },
      },
    }),
  ],
}
```

For more details, see the [`patches` section in Configuration](/docs/configuration#patches).

<Notice>
  We encourage you to [open an issue](https://github.com/onejs/one/issues/new) on our GitHub repository if you encounter similar issues, as your feedback helps us improve One's compatibility with React Native packages.

  If you've resolved an issue with a patch, we'd love for you to share your patch configuration. Alternatively, feel free to reach out, and we'll gladly assist in finding a solution to get things working.
</Notice>

<Notice>
  If appropriate, it's also nice to reach out to the maintainers of those packages to help improve their compatibility with non-Metro bundlers.
</Notice>

### Making fetch() requests on native platforms

When using `fetch()` on native platforms (iOS/Android), relative URLs behave differently than on web. You need to use the `ONE_SERVER_URL` environment variable to construct absolute URLs for API calls.

#### URL behavior by platform:

**Web** – works as expected:
- Relative URL (`/api/hello`) ✅
- localhost URL (`http://localhost:8081/api/hello`) ✅
- Local IP URL (`http://192.168.0.[x]:8081/api/hello`) ✅

**iOS Simulator**:
- Relative URL (`/api/hello`) ❌ – Network request failed
- localhost URL (`http://localhost:8081/api/hello`) ✅
- Local IP URL (`http://192.168.0.[x]:8081/api/hello`) ✅

**Real iOS Device**:
- Relative URL (`/api/hello`) ❌ – Network request failed
- localhost URL (`http://localhost:8081/api/hello`) ❌ – Network request failed
- Local IP URL (`http://192.168.0.[x]:8081/api/hello`) ✅

#### Solution

Use the `ONE_SERVER_URL` environment variable to construct absolute URLs:

```tsx
// ❌ Won't work on native
const response = await fetch('/api/hello')

// ✅ Works everywhere
const response = await fetch(`${process.env.ONE_SERVER_URL}/api/hello`)
```

One automatically sets `ONE_SERVER_URL` in development mode to the current running server URL (e.g., `http://0.0.0.0:8081`). For production builds, you'll need to set this environment variable yourself.

### Application has not been registered

One calls AppRegistry.registerComponent for you to set up your React Native entry component, using `native.app.name` (falling back to `native.key`).

If your React Native app is erroring on startup, make sure your One Vite plugin names the app, and that the name matches the key your native app container was built with:

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      native: {
        app: {
          name: 'my-app',
        },
      },
    })
  ]
}
```

---

### Apps Onestack.Dev Data Docs Components Drawer (`apps/onestack.dev/data/docs/components-Drawer.mdx`)

---
title: "<Drawer />"
---

The Drawer component provides a slide-out navigation panel, commonly used for app-wide navigation menus. Import it from `one/drawer` to keep the bundle size minimal when not using the drawer.

This component should only be rendered inside a `_layout.tsx` file, where it will serve as the location that children will render for routes below the layout.

Drawer wraps [React Navigation's Drawer Navigator](https://reactnavigation.org/docs/drawer-navigator) and accepts the same props.

## Basic Usage

```tsx fileName="_layout.tsx"
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { Drawer } from 'one/drawer'

export default function Layout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer />
    </GestureHandlerRootView>
  )
}
```

<Notice>
  The `GestureHandlerRootView` wrapper is required for gesture handling to work properly on native platforms.
</Notice>

## With Screen Options

You can customize the drawer appearance and behavior using `screenOptions`:

```tsx fileName="_layout.tsx"
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { Drawer } from 'one/drawer'

export default function Layout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer
        screenOptions={{
          headerShown: true,
          drawerType: 'front',
          // improve swipe gesture feel
          swipeEdgeWidth: 80,
          swipeMinDistance: 20,
        }}
      >
        <Drawer.Screen
          name="index"
          options={{
            drawerLabel: 'Home',
            title: 'Home',
          }}
        />
        <Drawer.Screen
          name="settings"
          options={{
            drawerLabel: 'Settings',
            title: 'Settings',
          }}
        />
      </Drawer>
    </GestureHandlerRootView>
  )
}
```

## Programmatic Control

You can open and close the drawer programmatically using navigation:

```tsx
import { useNavigation, DrawerActions } from '@react-navigation/native'

function MyScreen() {
  const navigation = useNavigation()

  return (
    <Button
      onPress={() => navigation.dispatch(DrawerActions.openDrawer())}
      title="Open Drawer"
    />
  )
}
```

## Drawer Types

The `drawerType` option controls how the drawer appears:

- `'front'` (default) - Drawer slides over the content
- `'back'` - Content slides to reveal drawer behind
- `'slide'` - Both drawer and content slide together
- `'permanent'` - Drawer is always visible (good for tablets)

## Common Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `drawerType` | `'front' \| 'back' \| 'slide' \| 'permanent'` | `'front'` | How the drawer animates |
| `swipeEdgeWidth` | `number` | `32` | Width of the swipe area from screen edge |
| `swipeMinDistance` | `number` | `60` | Minimum swipe distance to open drawer |
| `swipeEnabled` | `boolean` | `true` | Enable/disable swipe gestures |
| `drawerPosition` | `'left' \| 'right'` | `'left'` | Which side the drawer appears |
| `headerShown` | `boolean` | `true` | Show/hide the header |

## On web

<New />

On web, `<Drawer>` renders headless: only the focused screen, no sidebar. Open state lives in navigation state - drive your own sidebar with [useDrawer](/docs/hooks-useDrawer), which gives you the screen list, the focused screen, `isOpen`, and `open`/`close`/`toggle` actions:

```tsx
import { Drawer } from 'one/drawer'
import { Link, useDrawer } from 'one'

function WebDrawerLayout() {
  const { screens, focused, isOpen, close, toggle } = useDrawer()

  return (
    <>
      <button onClick={toggle}>Menu</button>

      {isOpen && (
        <nav>
          {screens.map((screen) => (
            <Link key={screen.key} href={screen.href} onPress={close}>
              {screen.options.drawerLabel ?? screen.options.title ?? screen.name}
            </Link>
          ))}
        </nav>
      )}

      {focused.element}
    </>
  )
}

export default function Layout() {
  return (
    <Drawer>
      <Drawer.Screen name="index" options={{ drawerLabel: 'Home' }} />
      <Drawer.Screen name="settings" options={{ drawerLabel: 'Settings' }} />
      <WebDrawerLayout />
    </Drawer>
  )
}
```

A component placed as a child of `<Drawer>` replaces the headless default on web and is ignored on native. See the [headless web navigators guide](/docs/guides-headless-web) for more.

## Dependencies

The Drawer requires these optional peer dependencies to be installed:

```bash
npm install @react-navigation/drawer react-native-gesture-handler react-native-reanimated
```

These are marked as optional peer dependencies in `one`, so they won't be installed automatically. This keeps bundle sizes smaller for apps that don't use the Drawer.

For more configuration options, see the [React Navigation Drawer documentation](https://reactnavigation.org/docs/drawer-navigator).

---

### Apps Onestack.Dev Data Docs Components Head (`apps/onestack.dev/data/docs/components-Head.mdx`)

---
title: "<Head />"
---

The `Head` component lets you render elements into the document `<head>` on web, and integrates with native platform features like Apple Handoff and Spotlight Search on iOS.

## Usage

```tsx fileName=app/blog/[slug].tsx
import { Head, useLoader } from 'one'

export async function loader({ params }) {
  const post = await getPost(params.slug)
  return { post }
}

export default function BlogPost() {
  const { post } = useLoader(loader)

  return (
    <>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.excerpt} />
        <meta property="og:title" content={post.title} />
        <meta property="og:description" content={post.excerpt} />
        <meta property="og:image" content={post.image} />
      </Head>

      <article>
        <h1>{post.title}</h1>
        {/* ... */}
      </article>
    </>
  )
}
```

## Props

| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | Standard HTML head elements like `<title>`, `<meta>`, `<link>`, etc. |

## Supported Elements

The `Head` component accepts standard HTML head elements:

- `<title>` - Page title
- `<meta>` - Meta tags (description, Open Graph, Twitter Cards, etc.)
- `<link>` - Stylesheets, favicons, preload hints
- `<script>` - External scripts
- `<style>` - Inline styles

```tsx
<Head>
  {/* Page title */}
  <title>My Page Title</title>

  {/* Meta tags */}
  <meta name="description" content="Page description" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />

  {/* Open Graph */}
  <meta property="og:title" content="My Page" />
  <meta property="og:description" content="Page description" />
  <meta property="og:image" content="https://example.com/image.png" />
  <meta property="og:url" content="https://example.com/page" />

  {/* Twitter Cards */}
  <meta name="twitter:card" content="summary_large_image" />
  <meta name="twitter:title" content="My Page" />

  {/* Favicon */}
  <link rel="icon" href="/favicon.ico" />

  {/* Preload fonts */}
  <link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossOrigin="" />
</Head>
```

## Dynamic Metadata

Use loader data or component state to set dynamic metadata:

```tsx fileName=app/products/[id].tsx
import { Head, useLoader, useParams } from 'one'

export async function loader({ params }) {
  const product = await getProduct(params.id)
  return { product }
}

export default function ProductPage() {
  const { product } = useLoader(loader)

  const ogImageUrl = `/api/og?title=${encodeURIComponent(product.name)}&price=${product.price}`

  return (
    <>
      <Head>
        <title>{product.name} | My Store</title>
        <meta name="description" content={product.description} />
        <meta property="og:title" content={product.name} />
        <meta property="og:image" content={ogImageUrl} />
        <meta property="og:image:width" content="1200" />
        <meta property="og:image:height" content="630" />
        <meta property="product:price:amount" content={String(product.price)} />
        <meta property="product:price:currency" content="USD" />
      </Head>

      {/* Page content */}
    </>
  )
}
```

## Platform Behavior

### Web

On web, `Head` renders its children directly into the document's `<head>` element. Multiple `Head` components can be used across your app - they will all contribute to the final head content.

### iOS (Native)

On iOS, `Head` integrates with native platform features:

- **Apple Handoff**: Continue browsing on other Apple devices
- **Spotlight Search**: Make your content searchable in iOS Spotlight

Enable these features using special meta properties:

```tsx
<Head>
  <title>My Article</title>
  <meta property="og:title" content="My Article" />
  <meta property="og:description" content="Article description" />
  <meta property="og:url" content="https://example.com/article" />

  {/* Enable Apple Handoff */}
  <meta property="expo:handoff" content="true" />

  {/* Enable Spotlight Search indexing */}
  <meta property="expo:spotlight" content="true" />
</Head>
```

| Meta Property | Description |
|---------------|-------------|
| `expo:handoff` | Set to `"true"` to enable Apple Handoff for this page |
| `expo:spotlight` | Set to `"true"` to index this page in iOS Spotlight Search |

### Android

On Android, `Head` currently has no effect. Native metadata features may be added in future versions.

## Creating a Reusable Head Component

For consistent metadata across your app, create a wrapper component:

```tsx fileName=features/HeadInfo.tsx
import { Head } from 'one'

type HeadInfoProps = {
  title: string
  description?: string
  image?: string
  url?: string
}

export function HeadInfo({ title, description, image, url }: HeadInfoProps) {
  const fullTitle = `${title} | My App`

  return (
    <Head>
      <title>{fullTitle}</title>
      {description && <meta name="description" content={description} />}

      {/* Open Graph */}
      <meta property="og:title" content={fullTitle} />
      {description && <meta property="og:description" content={description} />}
      {image && <meta property="og:image" content={image} />}
      {url && <meta property="og:url" content={url} />}

      {/* Twitter */}
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:title" content={fullTitle} />
      {description && <meta name="twitter:description" content={description} />}
      {image && <meta name="twitter:image" content={image} />}
    </Head>
  )
}
```

Use it in your pages:

```tsx
import { HeadInfo } from '~/features/HeadInfo'

export default function AboutPage() {
  return (
    <>
      <HeadInfo
        title="About Us"
        description="Learn about our company"
        image="/images/about-og.png"
      />

      {/* Page content */}
    </>
  )
}
```

## Related

- [Dynamic OpenGraph Images](/docs/guides-open-graph) - Generate dynamic OG images
- [OpenGraph Images](/docs/guides-open-graph) - Social preview images for your pages

---

### Apps Onestack.Dev Data Docs Components Link (`apps/onestack.dev/data/docs/components-Link.mdx`)

---
title: "<Link />"
---

Render a link to another page.

### Props

Note that `Href` is a string constant typed based on your file structure.

`Link` accepts all React Native `Text` props except `href`, plus the pressable props `disabled`, `onLongPress`, `onPressIn` and `onPressOut`, plus the link-specific props below:

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

### Apps Onestack.Dev Data Docs Components LoadProgressBar (`apps/onestack.dev/data/docs/components-LoadProgressBar.mdx`)

---
title: "<LoadProgressBar />"
---

Render this component to show a loading progress bar at the top of the browser frame while pages are loading. This is a web-only component and should be rendered in your root `_layout.tsx` file.

```tsx
import { LoadProgressBar, Slot } from 'one'

export default function Layout() {
  return (
    <>
      <LoadProgressBar />
      <Slot />
    </>
  )
}
```

### Props

```tsx
export type LoadProgressBarProps = {
  /** How long after a navigation to wait before showing the progress bar (in ms) */
  startDelay?: number
  /** How long after a navigation completes to wait before hiding the progress bar (in ms) */
  finishDelay?: number
  /** The starting percent it should show the loading state at */
  initialPercent?: number
  /** How often the progress bar should update (in ms) */
  updateInterval?: number
  /** How often to skip an update (checked each during the loop) */
  sporadicness?: number
  /** Pass style to the inner View */
  style?: ViewProps['style']
  /** Pass onLayout to the inner View */
  onLayout?: ViewProps['onLayout']
  /** Pass children to the inner View */
  children?: ViewProps['children']
}
```

---

### Apps Onestack.Dev Data Docs Components Presentations (`apps/onestack.dev/data/docs/components-Presentations.mdx`)

---
title: "<Presentations />"
---

`Presentations` sets the components that render `sheet` and `modal` presentations on web. Mount it once near the root of your app.

By default a presentation screen renders on web with no chrome at all: just the screen's content, inline, with no backdrop or floating position. `Presentations` is how you plug in a real sheet or modal (Tamagui Sheet, Vaul, Radix Dialog, or your own).

## Usage

```tsx
// app/_layout.tsx
import { Presentations, Slot } from 'one'
import { Modal } from '~/interface/Modal'
import { Sheet } from '~/interface/Sheet'

export default function Layout() {
  return (
    <Presentations web={{ sheet: Sheet, modal: Modal }}>
      <Slot />
    </Presentations>
  )
}
```

Any `<Stack.Screen>` with a `presentation` option picks these up automatically, using the same vocabulary you'd write for native:

```tsx
<Stack>
  <Stack.Screen
    name="compose"
    options={{ presentation: 'sheet', sheetAllowedDetents: [0.6, 1] }}
  />
</Stack>
```

## Writing a presentation component

Each one is an ordinary component, so hooks, context, and refs all work normally:

```tsx
import type { SheetPresentationProps } from 'one'

export function Sheet({ open, onOpenChange, options, children }: SheetPresentationProps) {
  const theme = useTheme()

  return (
    <MySheet open={open} onOpenChange={onOpenChange} snapPoints={options.sheetAllowedDetents}>
      {children}
    </MySheet>
  )
}
```

| Prop | Type | Description |
|------|------|--------------|
| `open` | `boolean` | Whether the presentation should be visible. Starts `false` on mount and flips to `true` on the next commit, so enter animations play. |
| `onOpenChange` | `(open: boolean) => void` | Call with `false` when your UI dismisses. Pops the screen. |
| `options` | `SheetPresentationOptions` / `ModalPresentationOptions` | The presentation options set on the screen. |
| `screen` | `ScreenEntry` | The full screen entry: `name`, `params`, complete `options`, `href`. |
| `children` | `ReactNode` | The screen's content. |

`options` is a narrowed view of the screen's `options`, taken straight from `Stack.Screen`, using the same values you'd set for native:

- Sheet: `sheetAllowedDetents`, `sheetGrabberVisible`, `sheetCornerRadius`, `sheetExpandsWhenScrolledToEdge`, `gestureEnabled`, `title`
- Modal: `gestureEnabled`, `title`

Everything the screen declared is still on `screen.options` if you need a key that isn't narrowed.

## Which presentations map where

| Component | `presentation` values |
|-----------|-----------------------|
| `sheet` | `'sheet'`, `'formSheet'`, `'pageSheet'` |
| `modal` | Any other non-`'card'`/`'push'` value (`'modal'`, `'containedModal'`, `'fullScreenModal'`, `'transparentModal'`, ...) |

Omit one and that presentation keeps rendering its content inline with no chrome.

## Scoping

`Presentations` is just context, so nesting one further down the tree overrides it for that subtree only:

```tsx
<Presentations web={{ sheet: Sheet }}>
  <Slot />
  <Presentations web={{ sheet: SpecialSheet }}>
    {/* screens rendered under here use SpecialSheet instead */}
  </Presentations>
</Presentations>
```

## Native

On native, presentations are handled by react-navigation, so the `web` components never run there. A `native` option that lets you swap the OS sheet for your own component is not implemented yet.

## Related

- [Stack](/docs/components-Stack)
- [useStack](/docs/hooks-useStack)
- [Headless web navigators](/docs/guides-headless-web)

---

### Apps Onestack.Dev Data Docs Components Protected (`apps/onestack.dev/data/docs/components-Protected.mdx`)

---
title: "<Protected />"
---

The `<Protected />` component allows you to declaratively hide routes based on a boolean condition. When the `guard` prop is `false`, wrapped screens are completely filtered out from the navigator - they won't appear in the navigation state and cannot be navigated to.

This is useful for authentication flows, feature flags, role-based access, or any scenario where routes should be conditionally available.

```tsx
import { Stack, Protected } from 'one'
import { useSession } from '~/features/auth/ctx'

export default function Layout() {
  const { session } = useSession()

  return (
    <Stack>
      <Stack.Screen name="login" options={{ title: 'Login' }} />
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Protected guard={!!session}>
        <Stack.Screen name="dashboard" options={{ title: 'Dashboard' }} />
        <Stack.Screen name="settings" options={{ title: 'Settings' }} />
      </Protected>
    </Stack>
  )
}
```

When `session` is null/undefined, the `dashboard` and `settings` routes don't exist in the navigation tree. Attempts to navigate to them will be blocked.

## Redirect Unauthorized Navigation

Set `redirectTo` to send unauthorized navigation to a specific route. One uses a replace navigation, so the protected route is not left in history:

```tsx
<Stack>
  <Stack.Screen name="login" />
  <Protected guard={!!session} redirectTo="/login">
    <Stack.Screen name="dashboard" />
    <Stack.Screen name="settings" />
  </Protected>
</Stack>
```

If the guard becomes false while a wrapped route is focused, One redirects to `redirectTo` and removes guarded screens from the navigator history. Without `redirectTo`, the navigator uses its first available route.

## Works with Any Navigator

`<Protected />` works with Stack, Tabs, Drawer, and the base Navigator component:

```tsx
import { Tabs, Protected } from 'one'

export default function TabLayout() {
  const { isAdmin } = useUser()

  return (
    <Tabs>
      <Tabs.Screen name="home" />
      <Tabs.Screen name="profile" />
      <Protected guard={isAdmin}>
        <Tabs.Screen name="admin" />
      </Protected>
    </Tabs>
  )
}
```

## Nested Guards

You can nest `<Protected />` components. Routes are only shown when **all** parent guards are true:

```tsx
<Stack>
  <Stack.Screen name="public" />
  <Protected guard={isLoggedIn}>
    <Stack.Screen name="dashboard" />
    <Protected guard={isPremium}>
      <Stack.Screen name="premium-features" />
    </Protected>
  </Protected>
</Stack>
```

In this example:
- `public` is always available
- `dashboard` requires `isLoggedIn` to be true
- `premium-features` requires both `isLoggedIn` AND `isPremium` to be true

## Comparison with Redirect

There are two main approaches to protecting routes:

| Approach | When to Use |
|----------|-------------|
| `<Protected />` | Routes should not exist when unauthorized, with an optional redirect target |
| `<Redirect />` | Routes exist but redirect unauthorized users |

**Use `<Protected />`** when routes should be completely hidden - they won't appear in tab bars or remain in navigation history. Add `redirectTo` when unauthorized navigation should land on a specific route.

**Use `<Redirect />`** when you want to intercept navigation and send users to a login page. This is better for deep linking scenarios where you want to redirect to login, then back to the original destination.

You can combine both approaches:

```tsx
export default function AppLayout() {
  const { session, isLoading } = useSession()

  if (isLoading) return null
  if (!session) return <Redirect href="/login" />

  return (
    <Stack>
      <Stack.Screen name="index" />
      <Protected guard={session.role === 'admin'}>
        <Stack.Screen name="admin" />
      </Protected>
    </Stack>
  )
}
```

## Props

| Prop | Type | Description |
|------|------|-------------|
| `guard` | `boolean` | When `false`, all wrapped screens are filtered out |
| `redirectTo` | `Href` | Route used when the guard fails. Defaults to the navigator's first available route |
| `children` | `ReactNode` | `<Stack.Screen />`, `<Tabs.Screen />`, or other `<Protected />` elements |

## Related

- [Authentication Guide](/docs/guides-authentication) - Full auth implementation patterns
- [Stack](/docs/components-Stack) - Stack navigator
- [Tabs](/docs/components-Tabs) - Tab navigator
- [Redirect](/docs/components-Redirect) - Redirect component

---

### Apps Onestack.Dev Data Docs Components Redirect (`apps/onestack.dev/data/docs/components-Redirect.mdx`)

---
title: "<Redirect />"
---

Redirect to another route. Useful for protecting routes or redirecting from old paths.

### Usage

```tsx
import { Redirect } from 'one'

export default function ProtectedPage() {
  const { user } = useAuth()

  if (!user) {
    return <Redirect href="/login" />
  }

  return <Dashboard user={user} />
}
```

### Props

| Prop | Type | Description |
|------|------|-------------|
| `href` | `Href` | The typed path to redirect to |

### Behavior

`<Redirect>` navigates with `router.replace` as soon as it mounts. It's designed to be rendered conditionally from a layout guard, so it guards against the two ways that can otherwise turn into a redirect loop:

- **Fires once per mount.** Re-renders, and re-focusing the screen (navigating away and back), won't re-issue the navigation.
- **De-duplicates across remounts.** An auth gate that flips between `<Redirect>` and `<Slot>`/`null` while auth state settles remounts `<Redirect>` repeatedly, and each remount is a fresh instance. Because `router.replace` is async (it preloads the target route before dispatching), every remount would otherwise fire another concurrent navigation and throw `Maximum update depth exceeded`. `<Redirect>` shares an in-flight guard across instances so only one navigation is dispatched until it lands; the guard clears once it settles, so a later redirect to the same href still fires.

Because of this you don't need a hand-rolled `useEffect` + `useRef` redirect in layout guards. Prefer the declarative `<Redirect>`.

---

### Apps Onestack.Dev Data Docs Components SafeAreaView (`apps/onestack.dev/data/docs/components-SafeAreaView.mdx`)

---
title: "<SafeAreaView />"
---

`SafeAreaView` renders content within the safe area boundaries of a device, avoiding notches, status bars, and home indicators.

`One.UI.SafeArea` is One's shared safe area API. It uses One's native host on iOS and
Android and CSS environment insets on web, with no separate safe area package in your app.
The same view is also exported directly as `SafeAreaView` from `one`.

## Usage

```tsx fileName=app/_layout.tsx
import { One, Slot } from 'one'

export default function Layout() {
  return (
    <One.UI.SafeArea.View style={{ flex: 1 }}>
      <Slot />
    </One.UI.SafeArea.View>
  )
}
```

## Props

`One.UI.SafeArea.View` extends all standard `View` props and adds:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `mode` | `'padding' \| 'margin'` | `'padding'` | How to apply the safe area insets |
| `edges` | `Edge[]` | All edges | Which edges to apply insets to |

## Examples

### Apply to Specific Edges

```tsx
import { One } from 'one'

// Only apply to top (useful for screens with bottom tabs)
<One.UI.SafeArea.View edges={['top']}>
  {/* Content */}
</One.UI.SafeArea.View>

// Only apply to top and bottom
<One.UI.SafeArea.View edges={['top', 'bottom']}>
  {/* Content */}
</One.UI.SafeArea.View>
```

### Use Margin Instead of Padding

```tsx
<One.UI.SafeArea.View mode="margin" style={{ flex: 1 }}>
  {/* Content */}
</One.UI.SafeArea.View>
```

## Hooks

### useSafeAreaInsets

Get the current safe area insets for custom layouts:

```tsx
import { useSafeAreaInsets } from 'one'

function MyComponent() {
  const insets = useSafeAreaInsets()

  return (
    <View style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}>
      {/* Content */}
    </View>
  )
}
```

Returns `{ top, right, bottom, left }` in pixels.

## Platform Behavior

- **Web**: Reads CSS `env(safe-area-inset-*)` values.
- **iOS**: Reads native safe area APIs for the status bar, notch or Dynamic Island, and home indicator.
- **Android**: Reads native window insets for status bars, navigation bars, and display cutouts.

One mounts the safe area provider at its root on every platform, so there is no setup. Libraries that import `react-native-safe-area-context`, such as React Navigation, resolve to One's safe area and read the same insets. If your app declares `react-native-safe-area-context` in its own `package.json`, One leaves that package alone and your copy is used.

## Related

- [Layouts](/docs/routing-layouts) - Using SafeAreaView in layouts
- [Stack](/docs/components-Stack) - Stack navigation with safe areas

---

### Apps Onestack.Dev Data Docs Components ScrollBehavior (`apps/onestack.dev/data/docs/components-ScrollBehavior.mdx`)

---
title: "<ScrollBehavior />"
---

One automatically handles scroll restoration to provide a native browser-like experience. When you navigate to a new page, scroll resets to the top. When you press back/forward, your previous scroll position is restored.

The `ScrollBehavior` component lets you customize this behavior for special cases.

## Default Behavior

Out of the box, One handles:

- **Hash scrolling**: Scrolls to `#element-id` when present in the URL
- **New page navigation**: Scrolls to top when navigating to a new route
- **Back/forward navigation**: Restores the previous scroll position
- **Session persistence**: Scroll positions are saved to sessionStorage

You don't need to add `ScrollBehavior` unless you want to change this behavior.

## Usage

Add `ScrollBehavior` once in your root layout:

```tsx fileName=app/_layout.tsx
import { ScrollBehavior, Slot } from 'one'

export default function Layout() {
  return (
    <>
      <ScrollBehavior />
      <Slot />
    </>
  )
}
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `disable` | `boolean \| 'restore'` | `false` | Disable scroll behavior entirely or just restoration |

### Disable Options

```tsx
// Disable all scroll behavior
<ScrollBehavior disable />

// Disable only scroll restoration on back/forward
// (still scrolls to top on new navigation)
<ScrollBehavior disable="restore" />
```

## Scroll Groups

For complex layouts like tabs or dashboards, you may want child routes to share a scroll position. Use the `useScrollGroup` hook to create a scroll group:

```tsx fileName=app/dashboard/_layout.tsx
import { Slot, useScrollGroup } from 'one'

export default function DashboardLayout() {
  // All routes under /dashboard will share scroll position
  useScrollGroup()

  return (
    <div>
      <Sidebar />
      <main>
        <Slot />
      </main>
    </div>
  )
}
```

Now when navigating between `/dashboard/overview` and `/dashboard/settings`, the scroll position is preserved instead of resetting to the top.

### Custom Scroll Group Path

You can specify a custom group path:

```tsx
import { useScrollGroup } from 'one'

function MyLayout() {
  // Create a scroll group for a specific path
  useScrollGroup('/products')

  return <Slot />
}
```

## Navigation-Level Control

Disable scroll behavior for an individual navigation by passing `scroll: false` to the router:

```tsx
import { useRouter } from 'one'

function MyComponent() {
  const router = useRouter()

  const handleClick = () => {
    router.push('/next-page', { scroll: false })
  }

  return <button onClick={handleClick}>Navigate without scrolling</button>
}
```

## How It Works

1. **On navigation start**: Current scroll position is saved to sessionStorage keyed by pathname
2. **On navigation complete**:
   - If URL has a hash (`#section`), scrolls to that element
   - If back/forward navigation, restores saved position
   - If within a scroll group, preserves group position
   - Otherwise, scrolls to top

Scroll positions are stored per-pathname in sessionStorage, so they persist across page refreshes within a session but not across browser sessions.

## Platform Support

| Platform | Support |
|----------|---------|
| Web | Full support |
| React Native | Not applicable (no window scroll) |

`ScrollBehavior` is a web-only component. On native platforms, scroll restoration is handled by the native scroll views themselves.

## Common Use Cases

### Infinite Scroll Lists

Preserve scroll position when users navigate away and back:

```tsx fileName=app/feed/_layout.tsx
import { Slot, useScrollGroup } from 'one'

export default function FeedLayout() {
  // Preserve scroll in the feed when viewing details
  useScrollGroup()

  return <Slot />
}
```

### Tab-Based Navigation

Keep each tab's scroll position independent:

```tsx fileName=app/(tabs)/_layout.tsx
import { Tabs, useScrollGroup } from 'one'

export default function TabLayout() {
  useScrollGroup()

  return (
    <Tabs>
      <Tabs.Screen name="home" />
      <Tabs.Screen name="search" />
      <Tabs.Screen name="profile" />
    </Tabs>
  )
}
```

### Disable for Modal Routes

Don't scroll when opening modals:

```tsx
router.push('/modal', { scroll: false })
```

## Related

- [Link](/docs/components-Link) - Navigation component with scroll control
- [useRouter](/docs/hooks-useRouter) - Programmatic navigation with scroll options
- [Layouts](/docs/routing-layouts) - Layout patterns for scroll groups

---

### Apps Onestack.Dev Data Docs Components Slot (`apps/onestack.dev/data/docs/components-Slot.mdx`)

---
title: "<Slot />"
---

This component should only be rendered inside a `_layout.tsx` file, where it will serve as the location that children will render for routes below the layout.

This is the simplest option for rendering children, it does not create a Stack, Drawer, or Tab view, instead it just renders the children directly.

```tsx
import { Slot } from 'one'

export default function Layout() {
  return (
    <Slot />
  )
}
```

## NamedSlot

For intercepting routes, layouts render one `<NamedSlot>` per parallel `@slotName` directory. It renders the intercepted route when an intercept is active, and its children (or the slot's `default.tsx`) otherwise:

```tsx
import { NamedSlot } from 'one'

export default function Layout({ children, modal }) {
  return (
    <>
      {children}
      <NamedSlot name="modal">{modal}</NamedSlot>
    </>
  )
}
```

`name` matches the `@modal` directory name. Pass `layoutContextKey` to scope slot state to one layout so duplicate modals do not render across layouts.

Close an intercepted modal with `closeIntercept()` instead of `router.back()`. It restores the pre-intercept URL and returns `true` when an intercept was active, `false` otherwise:

```tsx
import { closeIntercept } from 'one'

function CloseButton() {
  return <button onClick={() => closeIntercept()}>Close</button>
}
```

Use `isInterceptedNavigation()` to check whether the current history entry came from an interception.

See [NamedSlot](#namedslot) above for the full file conventions.

---

### Apps Onestack.Dev Data Docs Components Stack (`apps/onestack.dev/data/docs/components-Stack.mdx`)

---
title: "<Stack />"
---

This component should only be rendered inside a `_layout.tsx` file, where it will serve as the location that children will render for routes below the layout.

Stack is simply a [React Navigation Native Stack view](https://reactnavigation.org/docs/native-stack-navigator) and accepts the same props as React Navigation.

```tsx
import { Stack } from 'one'
import { Button } from 'react-native'

export default function Layout() {
  return (
    <Stack
      screenOptions={{
        headerRight() {
          return (
            <Button title="Settings" onPress={() => {}} />
          )
        },
      }}
    />
  )
}
```

### Stack.Screen

You can customize the children of the Stack in your layout by passing a children prop to Stack has Stack.Screen elements, like so:

```tsx
import { Stack } from 'one'

export default function Layout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: 'Feed' }} />
      <Stack.Screen name="[id]" options={{ title: 'Post' }} />
      <Stack.Screen
        name="sheet"
        options={{
          presentation: 'formSheet',
          gestureDirection: 'vertical',
          animation: 'slide_from_bottom',
          headerShown: false,
        }}
      />
    </Stack>
  )
}
```

The `name` must match the full name of the file inside `app`, without the extension but including groups.

In this example we are setting `index`, `[id]`, and `sheet` screens, which would correspond to `index.tsx` and `[id].tsx` and `sheet.tsx` pages in the same directory.

This is a convenient way to configure settings for each page up front, but you could also render `Stack.Screen` inside each individual page so you can access data loaded inside that page. The upside of doing it in the layout is that it will configure things before any stack animation runs on enter, with the downside being that you can't access page-level data.

The `options` property passes to the React Navigation NativeStack, and so [takes the same options](https://reactnavigation.org/docs/native-stack-navigator#options).

### Header Composition API

<New />

For more declarative header configuration, use the compositional API with `Stack.Header` and its child components:

```tsx
import { Stack } from 'one'

export default function Layout() {
  return (
    <Stack>
      <Stack.Screen name="index">
        <Stack.Header blurEffect="regular">
          <Stack.Header.Title large>Articles</Stack.Header.Title>
          <Stack.Header.SearchBar placeholder="Search..." />
        </Stack.Header>
      </Stack.Screen>

      <Stack.Screen name="[id]">
        <Stack.Header>
          <Stack.Header.Title>Post</Stack.Header.Title>
          <Stack.Header.Right asChild>
            <ShareButton />
          </Stack.Header.Right>
        </Stack.Header>
      </Stack.Screen>
    </Stack>
  )
}
```

#### Stack.Header

The container for header configuration. Props:

- `hidden` - Hide the header entirely
- `blurEffect` - iOS blur effect (`'regular'`, `'prominent'`, `'systemMaterial'`, etc.)
- `asChild` - Render a completely custom header component
- `style` - Style with `backgroundColor`, `shadowColor` (set to `'transparent'` to hide)
- `largeStyle` - Style for large title mode

#### Stack.Header.Title

Configure the header title:

```tsx
<Stack.Header.Title large>My Title</Stack.Header.Title>
```

Props:
- `children` - The title text
- `large` - Enable iOS large title mode
- `style` - Text style (`fontWeight`, `fontSize`, `color`, `textAlign`)
- `largeStyle` - Text style for large title mode

#### Stack.Header.BackButton

Configure the back button:

```tsx
<Stack.Header.BackButton hidden />
<Stack.Header.BackButton displayMode="minimal">Back</Stack.Header.BackButton>
```

Props:
- `children` - Custom back button text
- `hidden` - Hide the back button
- `displayMode` - `'default'`, `'generic'`, or `'minimal'`
- `withMenu` - Enable long-press menu on iOS
- `src` - Custom back button image
- `style` - Back button title style

#### Stack.Header.Left / Stack.Header.Right

Add custom components to the header:

```tsx
<Stack.Header.Left asChild>
  <MenuButton />
</Stack.Header.Left>

<Stack.Header.Right asChild>
  <SettingsButton />
</Stack.Header.Right>
```

Props:
- `asChild` - Required to render custom children
- `children` - Your custom component

#### Stack.Header.SearchBar

Add an iOS-style search bar:

```tsx
<Stack.Header.SearchBar
  placeholder="Search articles..."
  autoCapitalize="none"
  placement="stacked"
/>
```

Props:
- `placeholder` - Placeholder text
- `autoCapitalize` - `'systemDefault'`, `'none'`, `'words'`, `'sentences'`, `'characters'`
- `placement` - `'automatic'`, `'inline'`, `'stacked'`, `'integrated'`, `'integratedButton'`, `'integratedCentered'`
- `hideWhenScrolling` - Hide when scrolling
- `obscureBackground` - Obscure background when active

#### Default Header for All Screens

You can set a default header for all screens by placing `Stack.Header` directly inside `Stack`:

```tsx
<Stack>
  <Stack.Header blurEffect="regular">
    <Stack.Header.BackButton displayMode="minimal" />
  </Stack.Header>

  <Stack.Screen name="index" options={{ title: 'Home' }} />
  <Stack.Screen name="profile" options={{ title: 'Profile' }} />
</Stack>
```

### Toolbar Composition API

Native toolbar items use the same compound style as the header. `Stack.Toolbar`
with `placement="left"` or `placement="right"` compiles to native header
items on iOS, and `placement="bottom"` (the default) renders a native bottom
toolbar from screen content:

```tsx
<Stack.Screen name="index">
  <Stack.Toolbar placement="left">
    <Stack.Toolbar.Button icon="pencil" onPress={edit}>
      Edit
    </Stack.Toolbar.Button>
  </Stack.Toolbar>
</Stack.Screen>
```

See [Toolbar and Menus](/native/toolbar) for the full toolbar reference,
including menus, the bottom toolbar, and the lower-level `One.iOS.ToolbarHost`
primitive.

### Typing the composition API

The prop types for the header and toolbar components are exported from `one`:

```tsx
import type {
  StackHeaderBackButtonProps,
  StackHeaderLeftProps,
  StackHeaderProps,
  StackHeaderRightProps,
  StackHeaderSearchBarProps,
  StackHeaderTitleProps,
  StackToolbarButtonProps,
  StackToolbarProps,
} from 'one'
```

### Shared element transitions

<New />

On native, `<Stack>` wraps each screen in Reanimated's `SharedTransitionBoundary`, active while the screen is focused. A view with the same `sharedTransitionTag` on two screens morphs between them on push, pop and the back swipe, with no setup in your screens:

```tsx
import Animated from 'react-native-reanimated'

<Animated.Image source={photo} sharedTransitionTag={`photo-${id}`} />
```

Native apps need `react-native-reanimated` installed (the starter templates include it) and its `ENABLE_SHARED_ELEMENT_TRANSITIONS` static feature flag on in `package.json`:

```json
"reanimated": { "staticFeatureFlags": { "ENABLE_SHARED_ELEMENT_TRANSITIONS": true } }
```

Web never loads Reanimated for this.

### On web

<New />

On web, `<Stack>` renders headless: only the focused screen's element, no header, no chrome. Screens with a `presentation` (`sheet`, `modal`, `formSheet`, etc.) render as overlays above the base screen - by default inline with no floating chrome, or with real chrome once you mount [Presentations](/docs/components-Presentations).

Set `keepMounted: true` on a screen's options to keep it mounted (hidden via React 19.2's `<Activity>`) instead of unmounting it when it loses focus:

```tsx
<Stack.Screen name="settings" options={{ keepMounted: true }} />
```

To build your own web layout entirely (a custom header, transitions, anything), place a component as a child of `<Stack>` - it replaces the headless default on web and is ignored on native. Use [useStack](/docs/hooks-useStack) to get the full stack state.

See the [headless web navigators guide](/docs/guides-headless-web) for the full story, including how `Presentations` maps sheets and modals to real chrome.

---

### Apps Onestack.Dev Data Docs Components Tabs (`apps/onestack.dev/data/docs/components-Tabs.mdx`)

---
title: '<Tabs />'
---

This component should only be rendered inside a `_layout.tsx` file, where it will serve as the location that children will render for routes below the layout.

Tabs is a [React Navigation Bottom Tabs view](https://reactnavigation.org/docs/bottom-tab-navigator) and accepts the same props as React Navigation. On iOS and Android it uses the bottom tab navigator. On web it uses One's headless implementation.

The screen `name` matches the route file name. One derives the link from the route tree.

```tsx
import { Tabs } from 'one'

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen
        name="explore"
        options={{
          title: 'Explore',
        }}
      />
    </Tabs>
  )
}
```

<Notice>
  Adding a new tab route has a bug currently that can cause it to not show up until you
  re-run your app with `--clean`.
</Notice>

### Native tabs

React Navigation owns the tab bar on iOS and Android. Use its bottom tab options directly, including `tabBarIcon`, `tabBarSystemItem`, `tabBarMinimizeBehavior`, and `tabBarSelectionEnabled`. An action tab listens for `tabPress` and disables route selection:

```tsx
<Tabs.Screen
  name="new"
  listeners={{
    tabPress() {
      openComposer()
    },
  }}
  options={{
    title: 'New',
    tabBarSelectionEnabled: false,
    tabBarIcon: ({ focused }) => ({
      type: 'sfSymbol',
      name: focused ? 'plus.circle.fill' : 'plus.circle',
    }),
  }}
/>
```

The `href` screen option belongs to One's web headless implementation. Native layouts reject it so a hidden or redirected tab cannot silently become an ordinary native tab. Put screens that should not appear as tabs in a parent `Stack`. If a web tab needs a custom target, define that option in the route layout's `.web.tsx` sibling.

### On web

<New />

On web, `<Tabs>` renders headless: only the focused tab's screen, no tab bar. Your site's own navigation is your tab bar.

To build one, place a component as a child of `<Tabs>` - it replaces the headless default on web and is ignored on native. Use [useTabs](/docs/hooks-useTabs) to get the tab list and the focused tab:

```tsx
// app/(tabs)/_layout.web.tsx
import { Link, Tabs, useTabs } from 'one'

function WebTabBar() {
  const { screens, focused } = useTabs()

  return (
    <>
      <nav>
        {screens.map((tab) => (
          <Link
            key={tab.name}
            href={tab.href}
            className={tab.isFocused ? 'active' : undefined}
          >
            {tab.options.title}
          </Link>
        ))}
      </nav>
      {focused.element}
    </>
  )
}

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="explore" options={{ title: 'Explore', href: '/explore' }} />
      <Tabs.Screen name="profile" options={{ title: 'Profile', href: '/profile' }} />
      <WebTabBar />
    </Tabs>
  )
}
```

See the [headless web navigators guide](/docs/guides-headless-web) for more, including how native and web can share the same layout file.

---

### Apps Onestack.Dev Data Docs Configuration (`apps/onestack.dev/data/docs/configuration.mdx`)

---
title: Configuration
---

One is entirely configured through a Vite plugin:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Note that some of this configuration is ultimately stored into [environment variables](/docs/environment) automatically, accessible in your apps and website. For TypeScript support, add `/// <reference types="one/env" />` to get typed access to these variables.

## alias

Per-environment module aliases that work during both Vite transforms and rolldown dep pre-bundling. Unlike Vite's `resolve.alias`, these are applied via `resolveId` hooks so they're respected by the optimizer.

Specify aliases per environment. More specific environments (`client`, `ssr`, `ios`, `android`) override the general ones (`web`, `native`).

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      alias: {
        // applies to client + ssr environments
        web: {
          'react-native-web': '@tamagui/react-native-web-lite',
        },

        // applies to ios + android environments
        native: {
          'some-package': 'some-package-native',
        },

        // overrides web for ssr only
        ssr: {
          'some-package': 'some-package-ssr',
        },
      },
    }),
  ],
}
```

Replacement values can be bare specifiers — One resolves them internally.

## setupFile

Path to a JS or TS file to import before the rest of your app runs. One controls your root component, but you may want to run some initialization code before anything else.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      // Single file for all environments
      setupFile: './setup.ts',

      // Or different files per environment
      setupFile: {
        client: './setup.client.ts',
        server: './setup.server.ts',
        native: './setup.native.ts',
      },

      // Or with platform-specific native files
      setupFile: {
        client: './setup.client.ts',
        server: './setup.server.ts',
        ios: './setup.ios.ts',
        android: './setup.android.ts',
      },
    }),
  ],
}
```

## native

The native object allows you to configure your native apps.

Pass `false` to completely disable native (iOS/Android) support. When disabled, One will not register the `ios`/`android` Vite environments, skip the Metro / React Native dev server plugins, and skip the rolldown native bundle pipeline — useful when you want to use One purely as a web framework.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      native: false,
    }),
  ],
}
```

### native.key

One will set up your React Native app to run via `AppRegistry.registerComponent(app.key)`.

This setting must match the key you gave the React Native app container you've built.

### native.bundler

Which bundler serves the native dev bundle. Options are:

- `'vite'` (default): Runs Rolldown with OXC transforms and no Babel
- `'metro'`: Runs Metro, also with no Babel by default

Use Metro when a dependency requires it. You can also set the `ONE_METRO_MODE` environment variable to override this setting to `'metro'` for a single run.

For detailed information including limitations, configuration options, and performance considerations, see the [Metro Mode documentation](/docs/metro-mode).

Note that `native.bundler` only affects dev. `one build --platform ios` and `--platform android` always produce the native bundle through the Rolldown path.

**Default:** `'vite'`

### native.bundlerOptions

Options for whichever bundler `native.bundler` selects. With `'vite'`:

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      native: {
        bundlerOptions: {
          plugins: [myRolldownPlugin()],
          define: {
            __MY_FLAG__: true,
          },
        },
      },
    }),
  ],
}
```

`plugins` are Rolldown plugins run on the native bundle in both dev and build. The native bundler is its own Rolldown instance rather than a Vite environment, so plugins in your top-level `plugins` array don't reach it unless they ship a native form, as `@tamagui/vite-plugin` does for its compiler. Those run on native automatically. This option is how you add any other plugin, and a plugin listed here takes the place of a top-level one with the same name.

`define` follows Vite's rules: a string value is inserted as a raw expression, anything else is JSON stringified. It's deliberately separate from your Vite `define` rather than inherited from it, because plugins add web-only defines there. `@tamagui/vite-plugin` sets `_WORKLET: false` for Reanimated on web, and inheriting that on native would statically kill every worklet.

With `'metro'`, see the [Metro Mode documentation](/docs/metro-mode) for `nativeTransformModules`, `nativeTransforms`, `startup`, and the Metro config passthroughs.

#### Imported functions in worklets

For Rolldown, `workletImportsPlugin` from `vxrn` makes selected exports from a pure JavaScript package callable inside ordinary worklets:

```tsx fileName=vite.config.ts
import { one } from 'one/vite'
import { workletImportsPlugin } from 'vxrn'

export default {
  plugins: [
    one({
      native: {
        bundler: 'vite',
        bundlerOptions: {
          plugins: [workletImportsPlugin({ remend: ['default'] })],
        },
      },
    }),
  ],
}
```

Keep your usual `import remend from 'remend'` in application code. The plugin bundles the selected exports and their dependencies into worklets, so they can run on a separate worklet runtime. This is an explicit opt-in that adds bundled code and serialized worklet code for each configured entry.

Select synchronous functions that work without a receiver or native runtime dependencies. The replacement module exposes only the selected exports. Function identity, `name`, `length`, attached properties, and constructor behavior are not preserved. Each entry owns its dependency graph, so module state is not shared with other configured entries or ordinary imports through another path.

Initialization happens lazily once per runtime and live module version. Editing a dependency rebuilds the worklet module during development; retained worklets keep their original version. Restart the dev server after changing the plugin configuration or package export mappings.

Node built-ins, React Native runtime packages, dynamic imports, and unsupported free runtime globals are rejected. The plugin cannot prove that a function is pure: the selected functions and their dependencies must not rely on host objects, runtime-specific properties on `globalThis`, or asynchronous module initialization.

### native.app

Typed native app manifest. `name` is the native target and AppRegistry key. `one prebuild` requires `name` with `ios.bundleId` and `android.applicationId` configured.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      native: {
        app: {
          name: 'OneApp',
          ios: { bundleId: 'com.example.oneapp' },
          android: { applicationId: 'com.example.oneapp' },
        },
      },
    }),
  ],
}
```

See the [iOS Native Guide](/docs/guides-ios-native) for the full setup.

### native.suspendRoutes

Wrap each route screen in a React Suspense boundary on native. On native the JS bundle already contains all route modules, so the boundary only catches the async `import()` wrapper which resolves on the next microtick. Set this to `false` to remove the `<Suspense fallback={null}>` wrapper, which prevents a blank flash when JS-driven navigation animations slide a screen in before the fallback is replaced.

**Default:** `true`

### native.css

Turns on [react-native-css-interop](https://github.com/marklawlor/nativewind) support when importing CSS on native. This allows you to use CSS imports in your React Native code.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      native: {
        css: true,
      },
    }),
  ],
}
```

**Default:** `false`

## web

The web object allows you to configure web specific settings.

### web.defaultRenderMode

Choose between one of `ssg`, `spa` or `ssr`. The default is `ssg`.

Each mode has trade-offs:

- `ssg` stands for "Static Site Generated", it is the default mode.

SSG is a great general purpose strategy, where your pages are rendered fully at build-time and turned into static HTML. This means you can get a "perfect" first load, where your app is fully rendered with all content, servable easily by a CDN.

The JS is sent alongside it, and React will hydrate your app and resume running from there. The downside is that you do have some complexity in needing to ensure your pages can render in node, the upside is increased performance and SEO on the web.

- `spa` stands for "Single Page App", which is the simplest strategy.

SPA mode will not render the page on the server at all, and instead only serve the JavaScript to render your page
to the users browser. This mode avoids some complexity - you don't have to make your React code

- `ssr` stands for "Sever Side Rendered".

This mode will render your page on the server at the time the browser requests it, running loaders as well for each request.

This mode is likely the least useful, as it means that you must make a full trip to your database at the time of each request, and your client is left waiting for the full render from the server before it can show anything. That said, this mode can be useful if you have dynamically generated content but still want to have the best possible SEO, at the cost of some complexity in supporting server rendering.

### redirects

You can apply server-level redirects. One serves your app with Hono in production, and so the redirect pattern is simple: your `source` will be passed to `Hono.app.get(source)`. Any matching path segments that start with `:` will be replaced into the matching segments in `destination`. And the redirect will send a response type of `301` if permanent is true, otherwise it will be `302`.

### web.linkPrefetch

Controls link prefetching strategy for improved navigation performance.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      web: {
        // Smart trajectory-based prefetching (default)
        linkPrefetch: 'intent',

        // Or choose a simpler strategy
        linkPrefetch: 'viewport', // prefetch when links enter viewport
        linkPrefetch: 'hover', // prefetch on mouseover only
        linkPrefetch: false, // disable prefetching
      },
    }),
  ],
}
```

**Options:**

- **`'intent'`** (default): Uses mouse trajectory prediction to prefetch links before you hover them. Ray-casting detects when you're moving toward a link, with winner-takes-all scoring to avoid over-fetching in dense navigation areas. Falls back to hover for immediate interactions.

- **`'viewport'`**: Prefetches links when they enter the viewport (with 100px margin). Good for content-heavy pages with many links.

- **`'hover'`**: Traditional hover-based prefetching. Prefetches when you mouseover a link.

- **`false`**: Disables link prefetching entirely.

### web.deploy

Deployment target for production builds. Determines how the server bundle is built.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      web: {
        deploy: 'vercel', // or 'node' or 'cloudflare'
      },
    }),
  ],
}
```

**Options:**

- **`'node'`** (default): Standard Node.js server using Hono
- **`'vercel'`**: Vercel serverless functions
- **`'cloudflare'`**: Cloudflare Workers

### web.inlineLayoutCSS

When enabled, inlines CSS content directly into the HTML instead of using `<link>` tags. This eliminates an extra network request for CSS, improving LCP (Largest Contentful Paint) scores.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      web: {
        inlineLayoutCSS: true,
      },
    }),
  ],
}
```

This inlines ALL CSS. For per-file control, use the [`.inline.css` extension](/docs/guides-css#inline-css-inlinecss) instead, which only inlines the files you mark.

**Default:** `false`

### Code Splitting

One uses Rolldown for production builds and configures sensible code splitting defaults out of the box. By default, chunks smaller than 42KB are merged together to reduce the number of HTTP requests on page load.

You can customize the code splitting behavior through `build.rolldownOptions.output.codeSplitting`:

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      // ... your one() config
    }),
  ],

  build: {
    rolldownOptions: {
      output: {
        codeSplitting: {
          // minimum chunk size in bytes before merging (default: 42000)
          minSize: 42_000,

          // maximum chunk size - splits chunks larger than this
          maxSize: 500_000,

          // group specific modules into named chunks
          groups: [
            {
              name: 'vendor',
              test: /node_modules/,
              priority: 1,
              minSize: 50_000,
              // only create chunk if shared by 2+ entry points
              minShareCount: 2,
            },
          ],
        },
      },
    },
  },
}
```

**Why this matters:** Without chunk merging, a large app can produce hundreds of tiny JS files (many under 1KB). Even with HTTP/2 multiplexing, this many requests saturates browser connections and increases total transfer overhead. The 42KB default strikes a balance between granular caching and fewer round-trips.

**SPA page optimization:** For SPA pages, One skips emitting `<link rel="modulepreload">` hints in the HTML since the JS module graph handles its own lazy loading and route preloading happens on hover intent. This avoids saturating the connection with hundreds of preload hints that provide no benefit for client-rendered pages.

### web.experimental_scriptLoading

Controls how scripts are loaded for improved performance. This is an experimental feature that can significantly improve Lighthouse scores by deferring non-critical JavaScript.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      web: {
        experimental_scriptLoading: 'after-lcp',
      },
    }),
  ],
}
```

**Options:**

- **`'defer-non-critical'`**: Critical scripts (framework entry, page, layouts) load immediately. Non-critical scripts (component imports, utilities) become modulepreload hints only, reducing network/CPU contention.

- **`'after-lcp'`**: Scripts download immediately via modulepreload but execution is deferred until after first paint using double requestAnimationFrame. This allows the browser to paint the SSR content before executing JavaScript. Only applies to SSG pages.

- **`'after-lcp-aggressive'`**: Omits initial JavaScript modulepreload hints so images and styles can use the connection first. The deferred bootstrap loads the entry and layout scripts; their imports load the remaining chunks. Best for pages with many chunks or slow networks. Only applies to SSG pages.

**Default:** `undefined` (all scripts load with async)

### web.sitemap

Generate a `sitemap.xml` file automatically during build. This helps search engines discover and index your pages.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      web: {
        // Simple: just enable it
        sitemap: true,

        // Or configure it
        sitemap: {
          // Base URL for sitemap entries (defaults to ONE_SERVER_URL env var)
          baseUrl: 'https://example.com',

          // Default priority for all routes (0.0 to 1.0)
          priority: 0.7,

          // Default change frequency
          changefreq: 'weekly',

          // Glob patterns for routes to exclude
          exclude: ['/admin/*', '/internal/*'],
        },
      },
    }),
  ],
}
```

The sitemap automatically:

- Includes all page routes (SSG, SPA, SSR)
- Expands dynamic routes using their `generateStaticParams` output
- Excludes API routes and not-found pages

**Route-level overrides:** Routes can export a `sitemap` object to customize their entry. See [Routing Exports](/docs/routing-exports#sitemap) for details.

**Base URL:** If `baseUrl` isn't specified, One uses the `ONE_SERVER_URL` environment variable. If neither is set, paths are relative.

### web.skewProtection

Skew protection detects when deployed assets have changed and handles stale client bundles gracefully instead of showing broken pages.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      web: {
        skewProtection: 'proactive',
      },
    }),
  ],
}
```

**Options:**

- **`true`** (default): Detects chunk load failures and auto-reloads
- **`'proactive'`**: Also polls for new versions and forces full-page navigation when a new deployment is detected
- **`false`**: Disable skew protection entirely

See the [Skew Protection guide](/docs/guides-skew-protection) for details.

### web.suspendRoutes

Wrap each route screen in a React Suspense boundary on web. By default web does not wrap routes in Suspense, to avoid flickers during navigation. Enable this if your routes use React `use()` or lazy loading and you want Suspense boundaries per route.

**Default:** `false`

### web.experimentalBundledDev

Serve the web client through Vite's bundled dev mode (Rolldown-powered full bundle dev environment) instead of unbundled ESM dev. This means fewer requests and faster parsing, especially for apps with large icon or utility imports. This is dev-only and is ignored for builds.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      web: {
        experimentalBundledDev: true,
      },
    }),
  ],
}
```

**Default:** `false`

## react

### react.compiler

Enables the new [React Compiler](https://react.dev/learn/react-compiler) for both native and web. Will slow down compilation as it uses Babel, but will improve app performance and reduce the need to write memoization.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      react: {
        compiler: true, // enable for all platforms
        // or
        compiler: 'web', // enable only for web
        // or
        compiler: 'native', // enable only for native
        // or
        compiler: /\/app\//, // enable for files matching regex
        // or
        compiler: (id, env) => {
          // env is 'ios' | 'android' | 'client' | 'ssr'
          return id.includes('/components/')
        },
        // or configure per-platform
        compiler: {
          web: true,
          native: /\/components\//,
        },
      },
    }),
  ],
}
```

**Options:**

- **`true`**: Enable for all platforms (excluding node_modules, +api files)
- **`'web'`**: Enable only for web (client and SSR)
- **`'native'`**: Enable only for native (iOS and Android)
- **`RegExp`**: Enable for files matching the pattern (full control, no default filtering)
- **`(id, env) => boolean`**: Custom function with file path and environment (`'ios' | 'android' | 'client' | 'ssr'`)
- **`{ web, native }`**: Configure each platform separately with any of the above options

When using `RegExp` or a function, you have full control over which files are compiled - the default exclusions (node_modules, +api files) are not applied. When using `true` (or `true` within the object form), the default filters apply.

**Default:** `false`

## optimization

Performance optimization options.

### optimization.autoEntriesScanning

By default, One scans your file system routes and adds them as Vite `entries`. This prevents some hard reloads on web as you navigate to new pages, but can slow down startup.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      optimization: {
        autoEntriesScanning: 'flat', // only root-level routes (default)
        // or
        autoEntriesScanning: true, // scan all routes
        // or
        autoEntriesScanning: false, // disable scanning
      },
    }),
  ],
}
```

**Options:**

- **`'flat'`** (default): Automatically adds just the routes at the root of your app but nothing nested in non-group folders below that
- **`true`**: Scans and adds all routes as entries
- **`false`**: Disables auto-scanning entirely

### autoWarmPlugin

A standalone Vite plugin that passively tracks dependencies Vite discovers during normal development. On your next dev server startup, those deps are pre-included in `optimizeDeps.include`, eliminating "optimized dependencies changed. reloading" cascades.

Import it from `one/vite` and add it as a top-level plugin:

```tsx fileName=vite.config.ts
import { autoWarmPlugin, one } from 'one/vite'

export default {
  plugins: [
    autoWarmPlugin(),
    one({ ... }),
  ],
}
```

During the first 5 minutes of each dev session, the plugin polls Vite's in-memory optimizer metadata and caches any newly discovered deps to `node_modules/.vite/one-warm-deps.json`. This file clears automatically when Vite clears its cache (lockfile or config changes).

To persist the cache permanently (surviving `node_modules` reinstalls), pass a file path:

```tsx
autoWarmPlugin('./src/dev/warmDeps.json')
```

## devtools

Development tools to help debug and understand your app. Enabled by default in development mode.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      // Enable all devtools (default)
      devtools: true,

      // Disable all devtools
      devtools: false,

      // Configure individual tools
      devtools: {
        inspector: true, // enabled by default
        editor: 'cursor', // editor CLI for the inspector, or set LAUNCH_EDITOR
        seoPreview: true, // enabled by default
      },
    }),
  ],
}
```

### devtools.inspector

The source inspector lets you quickly find the source code for any element on the page:

1. Hold **Option** (Mac) or **Alt** (Windows/Linux) for 0.8 seconds
2. Hover over any element to see its source file location
3. Click to open the file in your editor

This works automatically without any additional setup - One injects the necessary data attributes during development and handles opening files in your configured editor.

### devtools.editor

Editor CLI command for the source inspector to open files with. Examples: `'cursor'`, `'code'`, `'zed'`, `'subl'`, `'webstorm'`. You can also set it with the `LAUNCH_EDITOR` or `EDITOR` environment variable.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      devtools: {
        editor: 'cursor',
      },
    }),
  ],
}
```

### devtools.seoPreview

The SEO Preview panel helps you visualize how your pages will appear in search results and social media shares:

1. Press **Alt+S** to toggle the panel
2. View previews for:
   - **Google Search** - How your page appears in search results
   - **Open Graph** - How your page appears when shared on Facebook, LinkedIn, etc.
   - **Twitter Card** - How your page appears when shared on Twitter/X
3. See all meta tags at a glance with validation warnings for common SEO issues

The panel automatically updates as you navigate or modify meta tags, making it easy to verify your SEO configuration during development.

### devtools.plugins

Browser plugin modules to load into the One devtools overlay. Each entry is a module specifier or a path relative to the project root. The module default exports a `OneDevtoolsPlugin`, created with `defineDevtoolsPlugin` from `one/devtools`.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      devtools: {
        plugins: ['./devtools/runtime-inspector.ts'],
      },
    }),
  ],
}
```

See [Dev Tools](/docs/dev-tools#extending-the-web-panel) for the plugin shape.

### devtools.rozenite

React Native DevTools plugins hosted through [Rozenite](https://github.com/callstackincubator/rozenite). One discovers installed Rozenite packages from your dependencies and serves their panels on the native dev server, so `j` and `dt` show them without a Metro wrapper.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      devtools: {
        rozenite: {
          include: ['@rozenite/storage-plugin'],
          exclude: ['@rozenite/network-activity-plugin'],
          pluginDisplay: 'sidebar', // or 'tabs'
        },
      },
    }),
  ],
}
```

Set `rozenite: false` to keep the stock React Native DevTools frontend. When no plugin is installed, One does not mount the Rozenite host.

### devtools.routeDebug

The Route Debug panel helps you understand your current route state during development:

1. Press **Alt+R** to toggle the panel
2. View information about the current route:
   - **Pathname** - The current URL path
   - **Segments** - Breadcrumb-style view of path segments
   - **Route Params** - Dynamic route parameters (e.g., `[slug]`, `[id]`)
   - **Search Params** - Query string parameters
   - **Hash** - URL fragment (if present)
   - **Full URL** - Complete URL for reference

The panel updates automatically as you navigate, making it easy to debug routing issues and verify params are being parsed correctly.

### devtools.loaderTiming

The Loader Timing Waterfall panel shows performance metrics for your route loaders:

1. Press **Alt+L** to toggle the panel
2. View timing data for each loader:
   - **Module Load Time** (blue) - Time to download and parse the loader module
   - **Execution Time** (green) - Time for the loader function to complete
   - **Total Time** - End-to-end timing
   - **Source** - Whether this was an initial load, refetch, or preload
3. Visual waterfall shows relative timing of module load vs execution
4. Error states are highlighted with red indicators

The panel helps identify slow loaders and understand where time is being spent - whether in module loading (suggesting code splitting issues) or execution (suggesting slow data fetching).

### devtools.routePreload

The Route Preload panel shows which routes have been preloaded during navigation:

1. Press **Alt+P** to toggle the panel
2. View preloaded routes with status:
   - **Loading** - Preload in progress
   - **Loaded** - Successfully preloaded (shows duration, loader/CSS status)
   - **Error** - Preload failed (shows error message)

**Note:** Route preloading only works in production builds. In development, the panel shows a notice explaining this. Run `yarn build && yarn serve` to test preloading behavior.

Preloading is automatically triggered when users hover over links, warming the cache for likely navigation targets.

### devtools.errorPanel

The Error Panel shows errors caught by error boundaries and loaders during development:

1. Press **Alt+E** to toggle the panel
2. View error history with:
   - **Error Type** - Whether from render, loader, or hydration
   - **Route** - The route where the error occurred
   - **Error Message** - The error message with expandable stack trace
   - **Timestamp** - When the error occurred

The panel automatically appears when the first error is caught, making it easy to spot issues immediately. Click "Clear" to reset the error history. Error boundaries and loaders dispatch events that the panel listens to, giving you a unified view of all errors in your app.

## patches

While building One we found ourselves needing to apply small patches to a variety of packages. Packages in the React Native ecosystem are often published only for Metro, and so have a variety of weird setups.

We built an internal Vite plugin to easily apply patches across any package manager, but after installing One into a few larger production projects, we found ourselves wanting to have access to it.

The `patches` option lets you coerce your node_modules to be compatible with Vite.

Here's an example:

```tsx fileName=vite.config.ts
import type { UserConfig } from 'vite'
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      patches: {
        // passing an object lets you easily apply patches to your actual node_modules
        '@sentry/react-native': {
          // you can specify a semver range to only apply to certain versions:
          version: '~15.2.0',

          // you can use globs to apply to only specific files
          // if you pass an array value, One applies those transforms to matching files:
          //    swc => @swc/core will transform your file into CommonJS compatible code, removing JSX as well
          //    flow => the FlowType transformer will run to remove Flow types
          //    jsx => transforms JSX only
          '**/*.js': ['flow', 'swc'],

          // you can also apply custom transforms using a function
          'dist/js/utils/environment.js': (contents) => {
            if (typeof contents !== 'string') return
            return contents.replace(
              `import { version as RNV } from 'react-native/Libraries/Core/ReactNativeVersion';`,
              `import { Platform } from 'react-native';\nconst RNV = Platform.constants.reactNativeVersion;\n`
            )
          },
        },
      },
    })
  ]
} satisfies UserConfig
```

Any transforms that are applied to specific files in node_modules will only be applied once. The original files will be stored alongside the transformed ones.

## router

The router object allows you to configure routing behavior.

### router.root

Dangerously customize the router root directory. This may lead to unexpected behavior. Prefer the default `app` directory, or pass `--appDir` to `one generate-routes` when your routes live elsewhere.

### router.ignoredRouteFiles

An array of globs that One uses to ignore files in your routes directory. If a file matches any pattern, it will not be treated as a route.

This is particularly useful for ignoring test files, stories, or other files you want to colocate with your routes but don't want to be treated as actual routes.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      router: {
        ignoredRouteFiles: [
          '**/*.test.*',
          '**/*.spec.*',
          '**/*.stories.*',
          '**/*.utilities.*',
        ],
      },
    }),
  ],
}
```

**Limitations:** Currently, we only support patterns starting with `**/*`.

### router.experimental.typedRoutesGeneration

Auto-generate route type helpers in your route files. Route types are always generated in `app/routes.d.ts`, but this option controls whether One automatically inserts type helpers into your route files.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      router: {
        experimental: {
          typedRoutesGeneration: 'runtime', // or 'type' or false
        },
      },
    }),
  ],
}
```

**Options:**

- **`false`** (default): No auto-generation, manually add types yourself
- **`'type'`**: Auto-inserts type-only helpers:

```tsx
import type { RouteType } from 'one'
type Route = RouteType<'/your/[route]'>
```

- **`'runtime'`**: Auto-inserts runtime helpers:

```tsx
import { createRoute } from 'one'
const route = createRoute<'/your/[route]'>()
```

The insertion happens automatically when route files are created or modified, with proper spacing and without modifying your existing code.

For more information, see the [Typed Routes documentation](/docs/routing-typed-routes).

## build

Build-time options. See [one build](/docs/one-build) for the full build documentation.

### build.concurrency

Maximum number of static pages to build concurrently. The default of two keeps peak memory low; raise it on a machine with memory to spare. `ONE_BUILD_CONCURRENCY` overrides this value.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      build: {
        concurrency: 4,
      },
    }),
  ],
}
```

**Default:** the smaller of your CPU core count and 2

### build.workers

Use worker threads for parallel static page generation during build. This utilizes multiple CPU cores for faster builds. Set to `false` to disable, or use `ONE_BUILD_WORKERS=0` for a single run (`=1` forces them on).

**Default:** `true`

### build.server / build.api

`build.server` accepts VXRN server build options, or `false`. `build.api` accepts VXRN build options for API routes.

### build.securityScan

Scan client bundles for leaked secrets (API keys, tokens, passwords) after building. This catches accidental exposure of server-side secrets in client-facing JavaScript before they reach production.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      build: {
        // Log warnings when secrets are found (default)
        securityScan: 'warn',

        // Fail the build if secrets are found (recommended for production)
        securityScan: 'error',

        // Disable scanning entirely
        securityScan: false,

        // Object form for fine-grained control
        securityScan: {
          level: 'error',
          safePatterns: [
            // exact strings that are known safe
            '09259e3b-7be8-46f6-9801-106bf1866e1c',
            // regex patterns for domain-specific matches
            /my-app-test-token-[a-z]+/,
          ],
        },
      },
    }),
  ],
}
```

**Options:**

- **`'warn'`** (default): Log warnings but don't fail the build
- **`'error'`**: Fail the build if secrets are detected — **recommended for production CI/CD**
- **`false`**: Disable scanning entirely
- **`{ level, safePatterns }`**: Object form with `level` (`'warn'` or `'error'`) and `safePatterns` (array of strings or RegExp to ignore)

The scanner checks for common secret patterns including Anthropic, OpenAI, Stripe, GitHub, and AWS keys, as well as generic secret/password/token assignments. Known safe patterns (WebRTC constants, CSS tokens) are automatically excluded. Use `safePatterns` to add your own.

## server

Production server options, passed through to the underlying server. See [one serve](/docs/one-serve) for the CLI side.

### server.loggingEnabled

Log HTTP requests to the console.

**Default:** `true`

## skipEnv

Skip loading `.env` files during build. The same effect for a single run comes from the `one build --skip-env` flag, which takes precedence when passed.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      skipEnv: true,
    }),
  ],
}
```

## ssr

Server-side rendering configuration.

### ssr.autoDepsOptimization

One scans dependencies on startup and decides which ones to optimize based on known problematic dependencies. These include `react-native` and `react`, which need to be optimized to work. It finds all parent dependencies of the known bad deps and adds them to `ssr.optimizeDeps.include`.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      ssr: {
        // Enable with default include pattern (default)
        autoDepsOptimization: true,

        // Disable entirely
        autoDepsOptimization: false,

        // Configure include/exclude patterns
        autoDepsOptimization: {
          include: /node_modules/,
          exclude: /some-package/,
        },
      },
    }),
  ],
}
```

**Note:** The full path (e.g., `<your_project_path>/node_modules/<some_package>`) is used to match dependencies. If you're using a string to match a package name, you may want to add `*/` at the start and `/*` at the end.

**Default:** `{ include: /node_modules/ }`

### ssr.dedupeSymlinkedModules

In monorepos with symlinked workspace packages, Vite's SSR optimizer registers pre-bundled deps by their `node_modules` path, but the resolver follows symlinks to the real source path. This creates duplicate module instances: the optimizer serves one copy while source imports create another.

Enable this to redirect symlink-resolved paths back to `node_modules` paths so the optimizer can recognize and deduplicate them.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      ssr: {
        dedupeSymlinkedModules: true,
      },
    }),
  ],
}
```

**Default:** `false`

## environmentGuards

Configure environment guard behavior for `server-only`, `client-only`, `native-only`, and `web-only` imports.

One enforces these guard imports to prevent modules from being used in the wrong build environment. However, some libraries (like `floating-ui`) import `client-only` even though the imported code works fine on the server when you're only importing utilities.

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      // Disable the client-only guard (allows importing on server)
      environmentGuards: {
        disableGuards: ['client-only'],
      },

      // Or disable all environment guards entirely
      environmentGuards: {
        disabled: true,
      },
    }),
  ],
}
```

**Options:**

- **`disabled`**: When `true`, disables all environment guard checks. All guard imports become no-ops regardless of environment.
- **`disableGuards`**: An array of specific guard types to disable. Options are `'server-only'`, `'client-only'`, `'native-only'`, and `'web-only'`.

**Default:** All guards are enabled.

## transform

Per-file control over how code transforms. One uses OXC for worklets and React Compiler. Reanimated callbacks, gesture callbacks and explicit worklets do not require a Babel plugin. Babel still runs for configured Babel transforms and syntax that needs it.

Pass a function that receives the file id, its code, and the build context, and returns how to transform it:

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      transform: ({ id }) => {
        if (id.includes('node_modules')) return false
        return true
      },
    }),
  ],
}
```

The function receives `{ id, code, development, environment, reactForRNVersion }`, where `environment` is `'ios' | 'android' | 'client' | 'ssr'`. It returns:

- **`true`**: default transforms
- **`false`**: no transforms
- **`'babel'`**: force the Babel default transform
- **`'swc'`**: force the SWC default transform
- **`'oxc'`**: force the OXC default transform
- **`{ transform: 'babel', ... }`**: force Babel with custom options
- **`{ transform: 'swc' | 'oxc', ... }`**: force SWC or OXC with custom options

Babel defaults to preset `@babel/preset-typescript` with plugins for destructuring, runtime helpers, JSX, and async functions. SWC targets es5 for native and es2020 for web.

## The Environment API

One runs on Vite 8 and makes use of its [Environment API](https://main.vitejs.dev/guide/api-environment).

This makes it easier to target React Native, as you can specify configuration for iOS, Android and web separately. One defines `ios` and `android` environments, respectively. For web, we follow the Vite standard of having a `client` and `ssr` environments (for client-side and server-side bundles).

You'll want to lean on using the Environment API to configure as much as you can in your `vite.config.ts`. It may also mean that some plugins can cause issues by configuring things across all four environments when you really only want it for client-side, or not. We're working on some helpers to make this easier, but in general you can check for `this.environment.name` in plugin hooks (like `transform` and `load`) to conditionally exit early from their logic for environments where you don't want the plugin logic to run.

One assumes the following environments:

- `client`: Web client-side (matching Vite default)
- `ssr`: Web server-side (matching Vite default)
- `ios`: iOS
- `android`: Android

We set up platform-specific extensions based on the environment:

- client: `.web.(js|ts|tsx|mjs)`
- ssr: `.server.(ts|tsx)`, `.web.(js|ts|tsx|mjs)`
- ios: `.(ios|native).(js|ts|tsx|mjs)`
- android: `.(android|native).(js|ts|tsx|mjs)`

### Server-only files

Files with a `.server.ts` or `.server.tsx` extension are resolved only in the `ssr` environment. This lets you colocate server-only code next to shared code:

```
src/
  db.ts            # shared types/interface
  db.server.ts     # server-only implementation (database queries, secrets, etc.)
```

When you `import './db'` from a server-rendered route or loader, it resolves to `db.server.ts`. On the client or native, it resolves to `db.ts`.

If a `.server.ts` file is explicitly imported on the client or native (e.g. `import './db.server'`), the build will throw an error to prevent server code from leaking into client bundles.

## Other Exports

The `one/vite` import has a few other exports that may be useful.

### `resolvePath`

When you are setting an `alias` in Vite, it wants you to fully specify the import path. We use `resolvePath` to help with this, it's a bit like [`require.resolve`](https://nodejs.org/api/modules.html) that works in ESM or CJS. It's also similar to [`import.meta.resolve`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve), except it returns an absolute path that Vite expects rather than a file path.

## Default Configuration

One sets up some default configuration out of the box, including a variety of plugins and settings that make React Native and the ecosystem of packages that are popular "just work".

Some base configuration we set:

- `publicDir` to `"public"`
- `clearScreen` to `false` so we can output the quick actions

We set up `defines`:

- `__DEV__` to true in development mode, or false otherwise
- `process.env.NODE_ENV`

We set up `alias` based on environment:

- `react-native` => `react-native-web` for web environments

We also detect if you have a PostCSS config file (js, ts, or json) and set the `css`. The internal logic is something like:

```tsx
{
  css: postCSSConfigPath
    ? {
        postcss: postCSSConfigPath,
      }
    : {
        transformer: 'lightningcss',
        lightningcss: {
          targets: {
            safari: (15 << 16) | (2 << 8),
          },
        },
      }
}
```

---

### Apps Onestack.Dev Data Docs Dev Tools (`apps/onestack.dev/data/docs/dev-tools.mdx`)

---
title: Dev Tools
---

One has two development tool surfaces:

- A browser panel for web apps, with SEO, route, loader, error, and source inspection panels.
- React Native DevTools for iOS and Android apps, where One serves the stock debugger and can host Rozenite plugins.

Both work in development with no setup. You can add your own panels to the web overlay, and Rozenite plugin packages automatically appear in React Native DevTools.

## Opening Dev Tools

For the web overlay, press **Alt+Space** (Option+Space on Mac) to open the spotlight menu, then select a tool. You can also use keyboard shortcuts to jump directly to a specific panel:

- **Alt+S** - SEO Preview
- **Alt+R** - Route Info
- **Alt+L** - Loader Timing
- **Alt+E** - Errors

Press **Escape** to close any panel.

For React Native, press **j** or **dt** in the `one dev` terminal. That opens React Native DevTools for the connected Hermes app, with React DevTools, the JavaScript console, sources, network, and memory panels.

React Native DevTools attaches to one app at a time. If another debugger or a CDP client is attached, close it before opening DevTools again.

## SEO Preview

See how your page appears in Google search results and check for common SEO issues:

- **Google Preview** - Live preview of your search result appearance
- **Meta Tags** - View title, description, and Open Graph tags
- **Issues** - Warnings for missing or too-long meta tags

## Route Info

Debug the current route state:

- **Pathname** - The current URL path
- **Segments** - Path broken into segments
- **Route Params** - Dynamic params from the route (e.g., `[id]`)
- **Search Params** - Query string values

## Loader Timing

Visualize loader performance with a waterfall breakdown:

- **Module Load Time** - Time to import the loader module
- **Execution Time** - Time to run the loader function
- **Total Time** - Combined timing with visual bar

Useful for identifying slow loaders and optimizing data fetching.

## Errors

Track errors from error boundaries and loaders:

- **Error Type** - Whether from a boundary or loader
- **Timestamp** - When the error occurred
- **Message** - The error message

Errors are collected in real-time so you can see issues as they happen.

## Source Inspector

The inspector is injected automatically in development when `devtools.inspector` is enabled (the default). It works through the `data-one-source` attribute One adds to components during development. Hold **Option** (Mac) or **Alt** (Windows/Linux) for 0.8 seconds and hover over any element to see its source location highlighted. Click to open a picker showing:

- **Quick actions**:
  - **Open** - Open topmost component in your editor
  - **Path** - Copy all source paths to clipboard
  - **Selector** - Copy a smart CSS selector (prefers IDs)
  - **Record** - Start recording user input (see below)
- **Parent list** - Click any parent component to open that file

Files open in the editor configured via [`devtools.editor`](/docs/configuration#devtoolseditor), or the `LAUNCH_EDITOR` / `EDITOR` environment variable.

### SourceInspector component

`one` also exports a `SourceInspector` component with its props type, kept for backwards compatibility:

```tsx
import { SourceInspector } from 'one'

// deprecated: the inspector above is injected automatically.
// this component renders nothing. remove it for cleaner code.
export function Layout() {
  return <SourceInspector />
}
```

`SourceInspectorProps` accepts `hotkey` and `enabled`, but both are deprecated: the activation key and enabled state now come from the injected devtools script and the `devtools.inspector` config.

## Input Recording

Record user interactions for bug reports or AI-assisted debugging. Start recording from:

- The **Record** button in the Source Inspector picker
- **Record Input** in the Alt+Space spotlight menu

After a 2-second countdown, recording begins. Tap **Option** to stop and copy the log to clipboard.

The output is a compact, line-based format optimized for AI consumption:

```
# Recording 2024-03-02T12:30:00.000Z
# https://localhost:3000/dashboard
# components: Button < Card < Dashboard
# selector: [data-testid="submit-btn"]
# window: 1920x1080 scroll:0,0
# duration: 3200ms events: 28

0 click 450,320 btn:0 [#submit] @Button.tsx:15 "Submit"
85 keydown Tab
142 focus [input.email]
200 input val:"test@" [input.email]

# final hover: [.sidebar > nav]
# final source: Nav.tsx:42:5
```

Events captured include clicks, keyboard input, scroll, focus/blur, input changes, wheel, resize, and more.

## Panel Features

The dev tools panel is:

- **Draggable** - Drag the header to reposition
- **Edge Snapping** - Snaps to screen edges with 10px padding
- **Resizable** - Tracks window resize when snapped to edges

## Extending the web panel

A plugin is a module that default exports a `OneDevtoolsPlugin`. Use the `defineDevtoolsPlugin` helper so the editor checks the shape:

```ts
// devtools/runtime-inspector.ts
import { defineDevtoolsPlugin } from 'one/devtools'

export default defineDevtoolsPlugin({
  name: 'runtime-inspector',
  title: 'Runtime Inspector',
  category: 'Tuning',
  // An iframe keeps a devtool that already runs its own UI and server as-is.
  panel: {
    url: 'http://localhost:4577',
  },
  actions: [
    {
      id: 'open-web-panel',
      title: 'Open Runtime Inspector',
      run() {
        window.open('http://localhost:4577', '_blank')
      },
    },
  ],
})
```

Register it in `vite.config.ts`:

```ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      devtools: {
        plugins: ['./devtools/runtime-inspector.ts'],
      },
    }),
  ],
}
```

Plugin modules run in the browser. They can add a panel, actions, or both:

- **`panel.url`** loads an existing devtools page in an iframe. Use this for tools that already run their own frontend and server.
- **`panel.mount(root, context)`** renders a panel directly into the overlay. Return a cleanup function to remove listeners when the panel closes.
- **`actions`** appear in the Alt+Space spotlight and receive the same context as panels.

The context exposes `project`, `url`, `openPanel`, and `closePanel`. Panel ids are namespaced as `plugin:<name>`. Two plugins cannot claim the same name.

Plugins are loaded only in development. The module is resolved through Vite, so local TypeScript and JSX files work without a build step.

## Rozenite plugins in React Native DevTools

One hosts the [Rozenite](https://github.com/callstackincubator/rozenite) plugin convention for React Native. If a Rozenite plugin package is installed in your app, One mounts its plugin host on the native dev server and React Native DevTools shows the panels. Existing Rozenite plugins work without a Metro wrapper or a separate `withRozenite` config.

```sh
bun add -d @rozenite/storage-plugin
```

On the next `one dev`, pressing **j** or **dt** opens DevTools with the Storage panel. One logs the plugin names it found:

```text
[one] React Native DevTools plugins: @rozenite/storage-plugin (open with dt or j)
```

Discovery checks direct dependencies for a Rozenite manifest (`dist/rozenite.json`), the same convention Rozenite's Metro integration uses. To pin the list or exclude a plugin:

```ts
one({
  devtools: {
    rozenite: {
      include: ['@rozenite/storage-plugin'],
      exclude: ['@rozenite/network-activity-plugin'],
      pluginDisplay: 'sidebar', // or 'tabs'
    },
  },
})
```

Set `rozenite: false` to keep the stock React Native DevTools without the Rozenite host. When no Rozenite plugin is installed, One leaves the debugger frontend untouched.

One hosts Rozenite when One's Vite bundler serves the native app, which is the default. If you set `native.bundler: 'metro'`, keep using Rozenite's own `withRozenite` Metro wrapper instead.

The device half of a Rozenite plugin still lives in your app. Follow the plugin's install instructions and import it once from your app entry when it asks you to.

### Runtime Inspector

[Runtime Inspector](https://github.com/AmatoGiulio/runtime-inspector) is the reference adapter for this API. It already ships its semantics and layouts as a protocol-first package with a Rozenite panel. In a One app:

1. Add `@runtime-inspector/react-native` and `@runtime-inspector/panel-rozenite` to the app.
2. Import the panel once from the app entry: `import '@runtime-inspector/panel-rozenite'`.
3. Press **j** or **dt**. Runtime Inspector appears in React Native DevTools through Rozenite.

Its web panel already runs as its own server, so the web adapter is a plugin with `panel: { url }` like the example above. No Runtime Inspector protocol change is needed for either host.

## Configuration

Dev tools are enabled by default in development mode. To disable them, set:

```ts
// vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      devtools: false,
    }),
  ],
}
```

To keep the web tools but change individual features:

```ts
one({
  devtools: {
    inspector: true,
    editor: 'cursor',
    seoPreview: true,
    // browser plugin modules for the One overlay
    plugins: ['./devtools/my-panel.ts'],
    // Rozenite host for React Native DevTools
    rozenite: {
      pluginDisplay: 'tabs',
    },
  },
})
```

---

### Apps Onestack.Dev Data Docs Environment (`apps/onestack.dev/data/docs/environment.mdx`)

---
title: Environment
---

### Variables

#### Loading .env files and process.env

One handles environment variables for you in a few ways.

First, it handles loading `.env` files during development and in production. In order to ease compatibility with ESM and CJS, One makes your loaded environment variables (from `.env` and from `process.env`) automatically be defined onto both `import.meta.env` and `process.env`.

Second, One ensures that all environment variables loaded are only available to the server-side functionality, unless they are prefixed with `ONE_PUBLIC_` or `VITE_` (Vite convention).

So, for example `ONE_PUBLIC_DAYTIME=false one dev` would set both `process.env.ONE_PUBLIC_DAYTIME` and `import.meta.env.ONE_PUBLIC_DAYTIME` to be the string "false" on both server and client.

`ONE_PUBLIC_` is One's naming convention. `EXPO_PUBLIC_` is also accepted; see [Coming from Expo](/native/migrating-to-one#environment).

But, `SOME_RANDOM_VAR=2` would *not* be exposed to the client side, because it lacks the appropriate prefix.

### Preset environment variables

One also sets some environment variables for you, available on both `process.env` and `import.meta.env`.

#### Server

- `ONE_PORT` sets the port for both `one dev` and `one serve`. This is equivalent to passing `--port` on the command line, but works across both commands and can be set in your `.env` file or deployment config. If `--port` is also passed, the CLI flag takes precedence.
- `ONE_FORCE_PORT` overrides the port for both `one dev` and `one serve`, taking precedence over `--port` and `ONE_PORT`. Useful for CI or deployment environments where the port must be fixed regardless of other configuration.

Port priority (highest wins):

1. `ONE_FORCE_PORT`
2. `--port` CLI flag
3. `ONE_PORT`
4. Built-in defaults (8081 for dev, 3000 for serve)

#### Core Variables

- `ONE_CACHE_KEY` is set to a random number for each production build, or stable per dev server run. Useful for cache keys.
- `ONE_APP_NAME` is set to your `native.app.name` setting, falling back to [`native.key`](/docs/configuration#nativekey).
- `ONE_SERVER_URL` is set in development mode to the current running server URL, like `http://0.0.0.0:8081`. You are expected to set this yourself for production builds.
- `ONE_DEFAULT_RENDER_MODE` is set to either `"ssr"`, `"ssg"`, or `"spa"` based on your [defaultRenderMode setting](/docs/configuration#webdefaultrendermode).
- `ONE_RENDER_MODE` is the render mode of the current page being rendered (`"ssr"`, `"ssg"`, or `"spa"`). Set before the setup file runs so you can conditionally skip setup logic for certain modes.

#### Platform Detection

- `VITE_ENVIRONMENT` is set to `"client"` for client-side web, `"ssr"` for server-side web, or `"ios"` / `"android"` for native platforms.
- `VITE_NATIVE` is `"1"` for native platforms (iOS and Android), or `""` for web (client and SSR). Uses truthy/falsy strings to avoid polluting `process.env` types. Useful for tree-shaking native-only code.
- `ONE_PLATFORM` is `"web"`, `"ios"`, or `"android"`.
- `TAMAGUI_TARGET` is `"web"` for web builds or `"native"` for native builds. Used by Tamagui and One native route branches.

#### React Native

Available in native builds:

- `REACT_NATIVE_VERSION` contains the React Native version string.
- `REACT_NATIVE_PLATFORM` is set to `"ios"` or `"android"` in native builds.
- `REACT_NATIVE_SERVER_PUBLIC_PORT` is the dev server port for React Native.

### TypeScript Types

One provides TypeScript types for all preset environment variables. To enable them, add a triple-slash reference to your project:

```ts
/// <reference types="one/env" />
```

You can add this to any `.d.ts` file in your project (such as `env.d.ts` or `vite-env.d.ts`), or include it in a file that's part of your TypeScript compilation.

This gives you autocomplete and type checking for `import.meta.env.VITE_ENVIRONMENT`, `process.env.VITE_NATIVE`, and all other One environment variables.

---

### Apps Onestack.Dev Data Docs Exports WithLayoutContext (`apps/onestack.dev/data/docs/exports-withLayoutContext.mdx`)

---
title: withLayoutContext
description: Create your own layouts.
---

Today there are a few built-in navigators: [Slot](/docs/components-Slot), [Stack](/docs/components-Stack), etc. But you may want to create your own.

A React Navigation navigator can become a One layout through `withLayoutContext`. For example, an app can wrap a native stack with shared route sorting and protected-route support:

To make any React Navigation navigator work inside a One layout, use `withLayoutContext`:

```tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { withLayoutContext } from 'one'

const ModalNavigator = createNativeStackNavigator().Navigator

export const ModalStack = withLayoutContext(
  ModalNavigator
)
```

Now you can use `ModalStack` in any `_layout.tsx` file.

---

### Apps Onestack.Dev Data Docs Faq (`apps/onestack.dev/data/docs/faq.mdx`)

---
title: FAQ
---

### Why Vite?

It feels light and simple, runs fast and is easy to customize. It tree shakes well, has tons of web features, and has best-in-class types, documentation, type-docs, RFCs and community. It also has a huge ecosystem and responsive team.

### Is this production ready?

Yes! Thanks to many web bug fixes, tests and improvements, and One's own native bundler and modules, One is now generally production ready. See the [Status documentation](/docs/status) for more details.

### Does this use Metro?

No. One bundles native apps with Rolldown, the same bundler that builds web, so both platforms share one pipeline. [Metro mode](/docs/metro-mode) still exists as an opt-in.

### How does this relate to Expo Router?

One started as a fork of [Expo Router](https://docs.expo.dev/router/introduction/) but has since diverged significantly. We aim to maintain API compatibility where possible.

### Why use One instead of Expo Router?

One runs on Vite, which is fast, light, and has a massive ecosystem with best-in-class TypeScript support. One achieves significantly higher Lighthouse scores thanks to [render modes](/docs/render-modes), optimized loading strategies, and proper code splitting - you can hit 100 on Lighthouse while sharing nearly all your code with native.

With render modes, you can choose SSG, SSR, or SPA on a per-page basis - Expo Router is SPA-only on web. One includes a [Hono-based production server](/docs/one-serve) out of the box and deploys to [Vercel or Cloudflare](/docs/guides-deployment) with a single config option.

One also adds [dev tools](/docs/dev-tools) (Alt+Space) with SEO preview, route debugging, loader timing, error tracking, and a source inspector. Plus [middleware](/docs/routing-middlewares) for request interception and auth, and [typed loaders](/docs/routing-loader) that run on the server and tree-shake out of client bundles.

Expo Router does have features One lacks, like `use dom` and RSC support. We encourage you to evaluate both and choose based on your needs.

### Will you support RSC?

No, we find the complexity trade-off not worth it. One supports [SSG, SPA, and SSR](/docs/render-modes) with [loaders](/docs/routing-loader), and we recommend pairing with a sync engine like [Zero](https://zerosync.dev) for data. A simplified one-way RSC could work, which we'll continue to evaluate.

### What platforms does One support?

macOS (stable), Linux (stable), and Windows (early, not officially supported). See the [Status documentation](/docs/status) for details.

### Can I use One for web-only or native-only projects?

Yes. One works great as a web-only framework - you don't even need `react-native` as a dependency. For native-only, you can skip web-specific features like SSG/SSR. One is designed to be flexible whether you're targeting one platform or both.

### How does One compare to Next.js?

One takes a simpler approach: no RSC, no complex server/client boundaries. Instead, One uses [loaders](/docs/routing-loader) for data fetching (similar to Remix) and recommends pairing with a sync engine like [Zero](https://zerosync.dev) for real-time data. One also targets React Native alongside web with a single codebase, which Next.js doesn't support.

That said, One provides many things out of the box: [image optimization](/docs/guides-images) with blur placeholders, [dynamic OpenGraph images](/docs/guides-open-graph), [middleware](/docs/routing-middlewares), [automatic sitemaps](/docs/configuration#websitemap), [dev tools](/docs/dev-tools), a [production Hono server](/docs/one-serve), [API routes](/docs/render-modes#api), typed routes, scroll restoration, and a loading progress bar. See [Features](/docs/features) for the full list.

### How does One compare to TanStack Start?

Both are modern Vite-based frameworks with type safety and flexible rendering. One uses file suffixes (`page+ssg.tsx`, `page+ssr.tsx`) to set render modes while TanStack uses route property configuration. One also supports a global `defaultRenderMode`.

In One, SSG routes run loaders at build time and output static HTML. TanStack's "static prerendering" is a post-build crawl - a different model. One includes a [Hono server](/docs/one-serve) out of the box while TanStack Start requires setting up Nitro or deploying to specific platforms. One's loaders run on the server and tree-shake out of client bundles, while TanStack's loaders are isomorphic (run on server, then again on client during navigation). And One targets web and native with shared routes and loaders - TanStack Start is web-only.

Both are great choices for web - One is the better fit if you need cross-platform or prefer the simpler file-based approach.

### What's the difference between SSG, SSR, and SPA?

SSG (Static Site Generation) pre-renders pages at build time - fast and cacheable, ideal for mostly-static content. SSR (Server Side Rendering) renders on each request - good for dynamic content that needs SEO. SPA (Single Page Application) renders entirely on the client - simpler for highly dynamic dashboards. See [Render Modes](/docs/render-modes) for details.

### Can I use my existing React Native libraries?

Yes. For libraries that have issues with Vite, One provides a [`patches` system](/docs/configuration#patches) to fix common problems like Flow types or JSX in `.js` files.

### How do I deploy a One app?

One includes a [Hono server](/docs/one-serve) out of the box - just run `one build && one serve`. You can also deploy to [Vercel or Cloudflare](/docs/guides-deployment) with a single config option.

### What's the npm package?

It's `one`, thanks to Dan Maier.

### Can I develop iOS apps on Windows or Linux?

Yes. With [EAS Build](https://docs.expo.dev/build/introduction/), you can build iOS apps in the cloud from any platform. For local development, you can test on Android and web, then build iOS when ready. See [Build or Run iOS](/docs/guides-ios-native) for details.

### What's the bundle size overhead?

One itself adds some overhead, but not enough to affect Lighthouse scores in general.

The core routing and rendering logic is under 20KB gzipped, but it brings in React Navigation and generally `react-native-web`. You can swap for `@tamagui/react-native-web-lite` using an alias to save quite a bit of bundle size.

### Can I use One with an existing React Native app?

Yes. One can be added incrementally to existing React Native projects. Start by installing `one` and setting up file-based routing alongside your existing navigation.

### Does One support monorepos?

Yes. One works well in monorepos with tools like Turborepo, Nx, or pnpm workspaces. Configure your workspace's package resolution and One will respect it.

---

### Apps Onestack.Dev Data Docs Features (`apps/onestack.dev/data/docs/features.mdx`)

---
title: Features
description: One is a complete solution to cross-platform development with React and React Native. We're experimenting with a features document to offer a high-level overview of One.
---

See the [Status document](/docs/status) for the status of features.

## Vite

The One Vite Plugin automates away much of the setup & boilerplate required for cross-platform React Native & web development.

Out of the box, the One Vite plugin does the following:

- Sets up a [file system router](/docs/routing) in the `./app` folder
- Generates [types for routes](/docs/routing#types) and updates them when routes change
- Enables [loaders](/docs/routing-loader) and tree shakes them out on client using [babel-dead-code-elimination](https://github.com/pcattori/babel-dead-code-elimination)
- Supports [loader HMR](/docs/routing-loader#hot-reload-for-file-dependencies) with `watchFile` for instant content updates during development
- Enables the [`patches` option](/docs/configuration#patches) that allows for easy node_module patching
- Uses React 19 for both native and web
- Adds [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) plugin, unless it detects you've added it already
- Loads `.env` file into `process.env` before running anything using [dotenv](https://www.npmjs.com/package/dotenv)
- Adds a well-configured `tsconfig.json` if it doesn't exist
- Enables [`?imagedata` imports](/docs/guides-images) for automatic image dimensions and blur placeholders
- Supports [`.inline.css` files](/docs/guides-css#inline-css-inlinecss) to inline critical CSS and prioritize layout CSS before scripts
- Configures native to resolve "react-native" export field in package.json
- Enforces [environment guard imports](/docs/configuration#environmentguards) (`server-only`, `client-only`, `native-only`, `web-only`) that prevent modules from being used in the wrong build environment
- Configures platform-specific extensions `web|server|native|ios|android` for extensions `ts|tsx|js|mjs`, eg `.ios.ts`, `.server.ts`
- Adds a ssr-css plugin that enables perfect dev-mode CSS on first load, matching production
- Configures a wide variety of optimizeDeps presets for React Native library compatibility
- Patches a large number of known-bad node_modules to work with Vite
- Ensures optimized deps across all routes are found before first load, avoiding hard reloads and slow loads in development mode

## Universal

- Uses [React Navigation](https://reactnavigation.org/) routing and truly native layouts
- Adds [loaders](/docs/routing-loader) that work on native and web
- Adds [layout loaders](/docs/routing-layouts#layout-loaders) - layouts can export `loader()` just like pages
- Adds [`useMatches`](/docs/hooks-useMatches) hook to access all matched route data
- Adds [`useMatch`](/docs/hooks-useMatches#usematch) to find a specific route's loader data by route ID
- Adds [`usePageMatch`](/docs/hooks-useMatches#usepagematch) to get the current page's match
- Adds a [Link](/docs/components-Link) component for navigation
- Adds [`useSearchParams`](/docs/hooks-useSearchParams) for URLSearchParams API
- Adds a wide variety of hooks for routing
- Exposes [One.UI.SafeArea](/docs/components-SafeAreaView) with first-party native and web implementations
- Adds [`<Protected />`](/docs/components-Protected) component for declarative route guarding
- Adds [`useBlocker`](/docs/hooks-useBlocker) hook for blocking navigation with confirmation
- Adds [route validation](/docs/routing-loader#route-validation) with `validateParams` and `validateRoute`
- Adds [`loaderCache`](/docs/routing-loader#loader-cache) for deduplicating concurrent SSR loader calls with optional TTL

## Native

- Implements a custom bundle output with React Native compatible commonjs
- Implements custom hot reloading that works with commonjs
- Implements a custom bridge to React Native for debug logging
- Implements logic to support importing assets, like images
- Tree-shakes server code from native bundles automatically
- Auto-detects and configures NativeWind support
- Auto-detects and configures Reanimated support
- [Stack Header Composition API](/docs/components-Stack#header-composition-api) for declarative native header configuration

## Web

- Adds `serve` command that sets up a [Hono](https://hono.dev/) server for production with optional [cluster mode](/docs/one-serve#cluster-mode) for multi-core scaling
- Implements SPA, SSG, and SSR style routing on a [per-page and global basis](/docs/render-modes)
- Server rendering - find JS and CSS for all nested routes and layouts and hoists it to header to avoid waterfalls and flickering
- Smart [link prefetching](/docs/configuration#weblinkprefetch) that predicts navigation using mouse trajectory
  - Doesn't require up-front route map so no scaling cost
- Automatically handles scroll restoration and [allows configuration](/docs/components-ScrollBehavior)
- Adds [scroll position groups](/docs/hooks-useScrollGroup) for independent scroll areas
- Adds [route masks](/docs/hooks-useRouter#route-masking) for displaying different URLs
- Adds an easy [`redirects` option](/docs/configuration)
- Adds [`generateStaticParams` export](/docs/routing-exports#generatestaticparams) for SSG and SSR routes
- Implements [API routes](/docs/render-modes#api)
- Adds a [page loading progress bar](/docs/components-LoadProgressBar)
- Swaps `query-string` and `url-parse` libraries to save ~20Kb
- Adds a [Redirect component](/docs/components-Redirect)
- Supports [middleware](/docs/routing-middlewares) for request interception and shared context
- [Deploy to Vercel or Cloudflare](/docs/guides-deployment) workers with a single config option
- Automatic [sitemap generation](/docs/configuration#websitemap) with per-route overrides
- [SPA shell rendering](/docs/render-modes#layout-render-modes) - render the root layout into HTML for SPA pages so users see nav, head tags, and CSS immediately while page content loads client-side

## Devtools

- **[Dev Tools Panel](/docs/dev-tools)** - Press Alt+Space to open the spotlight menu with SEO Preview, Route Info, Loader Timing, and Errors panels
- **[Source Inspector](/docs/dev-tools#source-inspector)** - Hold Option (Mac) or Alt (Windows/Linux) and click any element to open in your editor

## Build

- [Parallel builds with worker threads](/docs/one-build#parallel-builds-with-worker-threads) for ~40% faster static page generation

## Extras

- Exports a variety of [helpful utility functions](/docs/helpers-utility-functions)
- Guide for [MDX support](/docs/guides-mdx)
- Guide for [light and dark mode](/docs/guides-dark-mode)
- Guide for [optimized images](/docs/guides-images) with `?imagedata` imports

---

### Apps Onestack.Dev Data Docs Guides Authentication (`apps/onestack.dev/data/docs/guides-authentication.mdx`)

---
title: Authentication
---

This guide covers authentication patterns for One apps, from simple session management to full OAuth flows. One's unique combination of middleware, loaders, and cross-platform support gives you flexibility in how you protect routes.

## Approaches

There are three main approaches to authentication:

| Approach | Best For | Complexity |
|----------|----------|------------|
| **Self-hosted libraries** | Most apps, full control, own database | Low-Medium |
| **Hosted providers** | Fastest setup, managed infrastructure | Low |
| **Custom auth** | Specific requirements, full control | High |

**Self-hosted libraries** like [Better Auth](https://better-auth.com) or [Auth.js](https://authjs.dev) run on your own server with your own database. You get full control over user data while the library handles the complexity of OAuth flows, session management, and security best practices. This is our recommended approach.

**Hosted providers** like [Clerk](https://clerk.com) or [Supabase Auth](https://supabase.com/auth) manage everything for you including infrastructure. Fastest to set up but user data lives on their servers.

**Custom auth** means implementing everything yourself - password hashing, session management, token validation, OAuth flows. Only recommended if you have very specific requirements.

## Session Context

The foundation of any auth system is a session context that tracks auth state across your app.

```tsx fileName=features/auth/ctx.tsx
import { createContext, useContext, useState, useEffect, type PropsWithChildren } from 'react'
import { storage } from './storage'

type AuthContextType = {
  signIn: (token: string) => void
  signOut: () => void
  session: string | null
  isLoading: boolean
}

const AuthContext = createContext<AuthContextType | null>(null)

export function useSession() {
  const ctx = useContext(AuthContext)
  if (!ctx) {
    throw new Error('useSession must be used within SessionProvider')
  }
  return ctx
}

export function SessionProvider({ children }: PropsWithChildren) {
  const [session, setSession] = useState<string | null>(null)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    storage.get('session').then((token) => {
      setSession(token)
      setIsLoading(false)
    })
  }, [])

  const signIn = (token: string) => {
    storage.set('session', token)
    setSession(token)
  }

  const signOut = () => {
    storage.remove('session')
    setSession(null)
  }

  return (
    <AuthContext.Provider value={{ signIn, signOut, session, isLoading }}>
      {children}
    </AuthContext.Provider>
  )
}
```

### Cross-Platform Storage

For token storage, use a cross-platform storage library. Popular options:

- **[react-native-mmkv](https://github.com/mrousavy/react-native-mmkv)** - Fast key-value storage (~30x faster than AsyncStorage), works on web and native
- **[Zustand](https://github.com/pmndrs/zustand) with persist middleware** - State management with built-in storage abstraction
- **[One.SecureStore](/native/secure-store)** - Keychain and Keystore storage for sensitive tokens (native only)

Create a platform-specific abstraction using One's file extensions:

```
features/auth/
├── storage.ts        # Shared interface
├── storage.web.ts    # localStorage or MMKV for web
└── storage.native.ts # MMKV or One.SecureStore for native
```

This keeps your auth context clean while each platform uses appropriate secure storage.

## Protected Routes

There are two approaches to protecting routes in One:

| Approach | Best For |
|----------|----------|
| **`<Protected />`** | Routes that shouldn't exist when unauthorized (hidden tabs, admin-only pages) |
| **Redirect guard** | Routes that redirect to login but support returning after auth |

### Declarative with `<Protected />`

The `<Protected />` component completely filters out routes when its `guard` prop is false. This is ideal when routes should be invisible to unauthorized users:

```tsx fileName=app/(app)/_layout.tsx
import { Stack, Protected } from 'one'
import { useSession } from '~/features/auth/ctx'

export default function AppLayout() {
  const { session, isLoading } = useSession()

  if (isLoading) return null

  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Protected guard={!!session}>
        <Stack.Screen name="dashboard" options={{ title: 'Dashboard' }} />
        <Stack.Screen name="settings" options={{ title: 'Settings' }} />
      </Protected>
      <Protected guard={session?.role === 'admin'}>
        <Stack.Screen name="admin" options={{ title: 'Admin' }} />
      </Protected>
    </Stack>
  )
}
```

When `session` is null, attempts to navigate to `/dashboard` or `/settings` are blocked entirely - the routes don't exist in the navigation state.

### Redirect Guard

Protect routes using a layout guard that checks auth state and redirects unauthorized users. This approach allows deep linking to protected pages - users are redirected to login, then can return to their original destination.

<RouteTree
  routes={[
    { name: '_layout.tsx', description: 'Root layout with SessionProvider' },
    { name: 'login.tsx', description: 'Public - login page' },
    { name: '(app)', children: [
      { name: '_layout.tsx', description: 'Auth guard - redirects if not logged in' },
      { name: 'index.tsx', description: 'Protected home' },
      { name: 'profile.tsx', description: 'Protected profile' },
    ] },
  ]}
/>

```tsx fileName=app/_layout.tsx
import { Slot } from 'one'
import { SessionProvider } from '~/features/auth/ctx'

export default function RootLayout() {
  return (
    <SessionProvider>
      <Slot />
    </SessionProvider>
  )
}
```

```tsx fileName=app/(app)/_layout.tsx
import { Redirect, Slot, useFocusEffect, useRouter } from 'one'
import { useSession } from '~/features/auth/ctx'

export default function AppLayout() {
  const { session, isLoading } = useSession()
  const router = useRouter()

  // Re-check auth when screen focuses (important for native)
  useFocusEffect(() => {
    if (!isLoading && !session) {
      router.replace('/login')
    }
  }, [isLoading, session])

  if (isLoading) {
    return null
  }

  if (!session) {
    return <Redirect href="/login" />
  }

  return <Slot />
}
```

The `useFocusEffect` ensures auth is re-checked when returning to a screen on native, where screens may stay mounted in memory.

### Redirect Logged-In Users

Keep authenticated users away from auth pages:

```tsx fileName=app/login.tsx
import { Redirect, useRouter } from 'one'
import { useSession } from '~/features/auth/ctx'

export default function LoginPage() {
  const { session } = useSession()
  const router = useRouter()

  if (session) {
    return <Redirect href="/" />
  }

  const handleLogin = async () => {
    // ... your login logic
    router.replace('/')
  }

  return (
    <Button onPress={handleLogin}>
      Sign In
    </Button>
  )
}
```

<Notice>
  Render `<Redirect>` declaratively from your guard. Don't replace it with a manual `useEffect` + `useRef` + `router.replace`. While auth state settles during a sign-in, the guard re-renders rapidly and may remount `<Redirect>`, but `<Redirect>` de-duplicates the navigation across those remounts. A hand-rolled effect that fires `router.replace` on every render can thrash the navigator into `Maximum update depth exceeded`.
</Notice>

## Middleware Authentication

One's middleware runs on the server before your route, making it ideal for auth checks.

### Protect API Routes

```tsx fileName=app/api/_middleware.ts
import { createMiddleware } from 'one'
import { validateToken } from '~/features/auth/server'

export default createMiddleware(async ({ request, next, context }) => {
  const token = request.headers.get('authorization')?.replace('Bearer ', '')

  if (!token) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  try {
    const user = await validateToken(token)
    context.user = user
  } catch {
    return Response.json({ error: 'Invalid token' }, { status: 401 })
  }

  return await next()
})
```

## Loader Authentication

Check auth in loaders for SSR pages. Loaders run on the server, so `redirect()` works for both direct page loads and client-side `<Link>` navigation — the protected page never renders and no sensitive data reaches the client.

```tsx fileName=app/profile+ssr.tsx
import { redirect, useLoader } from 'one'
import type { LoaderProps } from 'one'
import { validateSession } from '~/features/auth/server'

export async function loader({ request }: LoaderProps) {
  const session = await validateSession(request)

  if (!session) {
    throw redirect('/login')
  }

  const user = await getUser(session.userId)
  return { user }
}

export default function ProfilePage() {
  const { user } = useLoader(loader)

  return (
    <View>
      <Text>Welcome, {user.name}</Text>
    </View>
  )
}
```

Both `throw redirect()` and `return redirect()` work — `throw` is often cleaner since it stops execution and makes it obvious the remaining code won't run.

Use `+ssr` routes for protected pages so loaders run on every request, including client-side navigation. This ensures auth is always checked server-side.

> **Note:** Component JavaScript is always downloadable (same as every framework). Loaders protect **data** — use them to ensure sensitive data never reaches unauthorized clients.

## OAuth Integration

### GitHub OAuth Example

```tsx fileName=features/auth/server.ts
import { betterAuth } from 'better-auth'
import { jwt, bearer } from 'better-auth/plugins'

export const auth = betterAuth({
  database: process.env.DATABASE_URL,

  plugins: [
    jwt({ expirationTime: '7d' }),
    bearer(),
  ],

  socialProviders: {
    github: {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    },
  },
})
```

```tsx fileName=app/api/auth/[...rest]+api.ts
import { auth } from '~/features/auth/server'

export const GET = (req: Request) => auth.handler(req)
export const POST = (req: Request) => auth.handler(req)
```

```tsx fileName=app/login.tsx
import { authClient } from '~/features/auth/client'

export default function LoginPage() {
  const handleGitHubLogin = () => {
    authClient.signIn.social({ provider: 'github' })
  }

  return (
    <Button onPress={handleGitHubLogin}>
      Sign in with GitHub
    </Button>
  )
}
```

## Logout

```tsx fileName=features/auth/useLogout.ts
import { useRouter } from 'one'
import { useSession } from './ctx'
import { authClient } from './client'

export function useLogout() {
  const router = useRouter()
  const { signOut } = useSession()

  const logout = async () => {
    try {
      await authClient.signOut()
      signOut()
      router.replace('/login')
    } catch (error) {
      console.error('Logout failed:', error)
    }
  }

  return { logout }
}
```

## Related

- [Protected](/docs/components-Protected) - Declarative route protection
- [Middlewares](/docs/routing-middlewares) - Server-side request interception
- [Loaders](/docs/routing-loader) - Data loading with auth
- [API Routes](/docs/routing-api-routes) - Creating API endpoints

---

### Apps Onestack.Dev Data Docs Guides Css (`apps/onestack.dev/data/docs/guides-css.mdx`)

---
title: CSS Loading
description: Control CSS loading order and inlining for optimal performance with .inline.css files and layout CSS prioritization.
---

One automatically optimizes CSS loading in production builds. Layout CSS is loaded before scripts to prevent flash of unstyled content (FOUC), and you can mark individual CSS files as inline to embed them directly into the HTML.

## CSS Loading Order

In production builds, One separates CSS into two categories:

- **Layout CSS** — CSS imported by `_layout` files. Loaded before any JavaScript to prevent FOUC.
- **Page CSS** — CSS imported by page components. Loaded after scripts.

This happens automatically based on your route structure. No configuration needed.

## Inline CSS (`.inline.css`)

For CSS that must be available before the first paint, use the `.inline.css` file extension. This inlines the CSS content directly as a `<style>` tag in the HTML `<head>`, eliminating the network request entirely.

```tsx fileName=app/_layout.tsx
// inlined as <style> in HTML — zero network latency
import './critical-styles.inline.css'

// loaded via <link> tag — normal network request
import './non-critical-styles.css'
```

### When to use `.inline.css`

Use `.inline.css` for CSS that:

- Defines the page layout structure (prevents layout shift)
- Sets background colors or fonts visible above the fold
- Is small enough that inlining doesn't bloat the HTML (under ~10KB)

Don't use `.inline.css` for:

- Large CSS files (defeats the purpose — better as a cacheable `<link>`)
- CSS only needed after user interaction
- Component-specific CSS that isn't visible on first paint

### How it works

1. The `.inline.css` extension is detected during the build — Vite processes the CSS normally
2. The plugin tracks which CSS files should be inlined
3. At HTML generation time, inline CSS is read from the build output and inlined as `<style>` tags
4. Other CSS remains as `<link rel="stylesheet">` tags

### With `inlineLayoutCSS`

`.inline.css` works independently of the [`inlineLayoutCSS`](/docs/configuration#webinlinelayoutcss) config option:

- **`inlineLayoutCSS: true`** — inlines ALL CSS as `<style>` tags
- **`.inline.css`** — inlines only the marked files, leaves the rest as `<link>` tags
- **Both together** — `inlineLayoutCSS` takes precedence and inlines everything

### TypeScript

The `.inline.css` extension works with any CSS file. Since it's a side-effect import (no default export), TypeScript handles it without additional type declarations:

```tsx
import './styles.inline.css' // works out of the box
```

## Comparison

| Approach | What it does | Use case |
|----------|-------------|----------|
| Default | Layout CSS before scripts, page CSS after | Most apps |
| `.inline.css` | Specific files inlined as `<style>` | Above-the-fold CSS |
| `inlineLayoutCSS: true` | All CSS inlined as `<style>` | Small apps, maximum LCP |

---

### Apps Onestack.Dev Data Docs Guides Dark Mode (`apps/onestack.dev/data/docs/guides-dark-mode.mdx`)

---
title: Dark Mode
description: SSR safe and with meta theme-color
---

<IntroParagraph>
  Supporting SSR safe styling while also supporting Native and web requires a lot of work,
  until now.
</IntroParagraph>

There's about one cross-platform React style library that has excellent SSR support - [Tamagui](https://tamagui.dev) 👌.

Tamagui gives you styling out of the box that works perfectly with no flickers, and even renders properly with JS turned off (give it a try on this very page).

Using our style-library-agnostic `@vxrn/color-scheme` library, you can also let your users choose between three options: light, dark, or system settings - also, fully SSR safe. `one` re-exports everything from `@vxrn/color-scheme`, so you can import these from either package.

Finally, there's another wrinkle in the [theme-color meta tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color) - it also requires some fancy scripting to be SSR safe. Luckily, `@vxrn/color-scheme` helps again with the MetaTheme component.

Here's a complete example in just a few lines of code, all set up in your root `_layout.tsx` file. We're using Tamagui, but if you just want light/dark mode without SSR, you can swap out your own solution.

```tsx fileName=app/_layout.tsx
import type { ReactNode } from 'react'
import { MetaTheme, SchemeProvider, Slot, useUserScheme } from 'one'
import { TamaguiProvider, Theme } from 'tamagui'
import config from '../tamagui.config'

export default function Layout() {
  return (
    <SchemeProvider>
      <MetaTheme darkColor="#050505" lightColor="#ffffff" />
      <TamaguiRoot>
        <Theme name="yellow">
          <Slot />
        </Theme>
      </TamaguiRoot>
    </SchemeProvider>
  )
}

const TamaguiRoot = ({ children }: { children: ReactNode }) => {
  const userScheme = useUserScheme()
  return (
    <TamaguiProvider disableInjectCSS config={config} defaultTheme={userScheme.value}>
      {children}
    </TamaguiProvider>
  )
}
```

`SchemeProvider` renders a small inline script that sets a `t_dark` or `t_light` class on `<html>` before the page paints, reading the stored setting or falling back to the system preference. It accepts:

- `defaultScheme`: the scheme to use when the user has stored no preference, instead of the system preference.
- `forceScheme`: lock the page to `light` or `dark`, ignoring stored and system preferences.
- `getClassName`: change the class name for a scheme (defaults to `` (name) => `t_${name}` ``).

`MetaTheme` takes `darkColor` and `lightColor`. Its inline script picks one before hydration; after that, an optional `color` prop replaces the scheme's color while it is set. `useUserScheme()` returns `{ setting, value, set }`: the stored setting (`system`, `light`, or `dark`), the resolved scheme (`light` or `dark`), and a setter. Outside React, use `getUserScheme`, `setUserScheme`, and `onUserSchemeChange`; `useSystemScheme` and `getSystemScheme` read the operating system's scheme.

Now lets make a toggle button:

```tsx fileName=features/theme/ThemeToggleButton.tsx
import { Moon, Sun, SunMoon } from '~/components/icons'
import { useUserScheme } from 'one'
import { Button } from 'tamagui'

const schemeSettings = ['light', 'dark', 'system'] as const

export function ThemeToggleButton() {
  const { setting, set } = useUserScheme()
  const Icon = setting === 'system' ? SunMoon : setting === 'dark' ? Moon : Sun
  const next = schemeSettings[(schemeSettings.indexOf(setting) + 1) % schemeSettings.length]

  return (
    <Button
      circular
      icon={<Icon size={20} />}
      aria-label={`Color scheme: ${setting}`}
      onPress={() => set(next)}
    />
  )
}
```

Each press cycles light, dark, and system. The stored setting survives reloads, and with `system` the page follows the operating system's preference.

---

### Apps Onestack.Dev Data Docs Guides Deep Linking (`apps/onestack.dev/data/docs/guides-deep-linking.mdx`)

---
title: Deep Linking
---

Deep linking lets external URLs open a specific screen in your app on web, iOS, and Android. One handles deep links by stripping a configured prefix from the incoming URL and matching the remaining path against your file-system routes.

## Quick Start

Configure `router.linking` in your Vite config and register the same scheme in your native app manifest.

```ts fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      native: {
        app: {
          name: 'MyApp',
          scheme: 'myapp',
          ios: { bundleId: 'com.example.myapp' },
          android: { applicationId: 'com.example.myapp' },
        },
      },
      router: {
        linking: {
          scheme: 'myapp',
          prefixes: ['https://example.com/app'],
        },
      },
    }),
  ],
}
```

With this, opening `myapp://settings/profile` or `https://example.com/app/settings/profile` (after universal-link verification) routes to `app/settings/profile.tsx`.

## How Prefix Matching Works

Each entry in `prefixes` is an exact URL prefix that gets stripped from incoming deep links before route matching. A `scheme` is shorthand: `'myapp'` expands to `'myapp://'` and `'myapp:///'` automatically.

Prefixes match at clean boundaries — the URL must be exactly the prefix, or continue with `/`, `?`, or `#`:

| URL                              | Prefix                | Match | Stripped path |
| -------------------------------- | --------------------- | ----- | ------------- |
| `myapp://app/thread/1`           | `myapp://app`         | yes   | `thread/1`    |
| `myapp://app?ref=share`          | `myapp://app`         | yes   | `?ref=share`  |
| `myapp://app`                    | `myapp://app`         | yes   | (empty)       |
| `myapp://application/thread/1`   | `myapp://app`         | no    | (falls back)  |

When multiple prefixes match the same URL, the longest one wins.

The native app manifest registers URLs with iOS and Android. `router.linking` tells One how to strip those URLs before matching routes, so configure the scheme in both places.

## `OneLinkingConfig` Reference

```ts
type OneLinkingConfig = {
  scheme?: string | string[]
  prefixes?: string[]
  filter?: (url: string) => boolean
}
```

- **`scheme`** — one or more custom app schemes. Each scheme is expanded internally to `${scheme}://` and `${scheme}:///` URL prefixes. Schemes should also be registered in your native app manifest.
- **`prefixes`** — fully qualified URL prefixes to strip before route matching. Use this for universal links (`'https://example.com/app'`) or host-bearing custom scheme URLs (`'myapp://app'`).
- **`filter`** — optional predicate that gates which incoming URLs are handled. Functions can't be inlined into the generated entry, so this can only be set programmatically (see below).

## The `getLinking` Helper

`getLinking` is a typed identity helper for defining a linking config in TypeScript. It's useful when you want to share the config across files or pass it programmatically.

```ts
import { getLinking } from 'one'

export const linking = getLinking({
  scheme: 'myapp',
  prefixes: ['https://example.com/app'],
})
```

## Programmatic Configuration (with `filter`)

To use a `filter` function, write your own entry that calls `createApp({ linking })` directly. The Vite plugin's `router.linking` only accepts JSON-serializable fields.

```tsx fileName=src/entry.tsx
import { createApp, getLinking } from 'one'

const linking = getLinking({
  scheme: 'myapp',
  prefixes: ['https://example.com/app'],
  filter: (url) => !url.includes('/admin'),
})

createApp({
  routes: import.meta.glob('./app/**/*'),
  routerRoot: 'app',
  linking,
})
```

## iOS Setup

The scheme in `native.app` lands in `Info.plist` after running `one prebuild`. To verify:

```bash
plutil -p ios/MyApp/Info.plist | grep -A 4 CFBundleURLTypes
```

Test a deep link against a running simulator:

```bash
xcrun simctl openurl booted myapp://settings/profile
```

For universal links (`https://example.com/app/...`), follow Apple's [associated domains setup](https://developer.apple.com/documentation/xcode/supporting-associated-domains) and add the entitlement.

## Android Setup

`one prebuild` registers the `native.app` scheme in `AndroidManifest.xml`. Add universal-link hosts to the generated native project, then test with:

```bash
adb shell am start -W -a android.intent.action.VIEW -d "myapp://settings/profile"
```

## Web Behavior

On web, deep linking is just URL routing — no prefix-stripping is needed because the browser already provides the path via `window.location`. The configured `prefixes` and `scheme` are still accepted for API parity but are inert on web.

## Inspecting Routes with `useSitemap`

Use [`useSitemap`](/docs/hooks-useSitemap) to render a list of all routes — handy for debug screens or generating link previews while testing deep links.

## Related

- [`useSitemap`](/docs/hooks-useSitemap) — list every route in your app
- [Configuration](/docs/configuration) — full Vite plugin reference
- [Routing Overview](/docs/routing) — file-system routing fundamentals

---

### Apps Onestack.Dev Data Docs Guides Deployment (`apps/onestack.dev/data/docs/guides-deployment.mdx`)

---
title: Deployment
description: Deploy your One app to production
---

One supports multiple deployment targets for web applications. Choose the target that best fits your infrastructure needs.

## Configuration

Set your deployment target in `vite.config.ts`:

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      web: {
        deploy: 'node' // 'node' (default), 'vercel', or 'cloudflare'
      },
      build: {
        // fail the build if secrets leak into client bundles
        securityScan: 'error'
      }
    })
  ]
}
```

We recommend setting `securityScan: 'error'` for production deployments to prevent accidentally shipping API keys or tokens in client-facing JavaScript. See [Configuration: build.securityScan](/docs/configuration#buildsecurityscan) for details.

## Deployment Targets

### Node (Default)

The default deployment target outputs a Node.js server using Hono. This is suitable for any Node.js hosting environment.

```bash
# Build for production
npx one build

# Start the production server
npx one serve
```

The build output is in the `dist` directory:

- `dist/client` - Static assets and client-side bundles
- `dist/server` - Server-side rendering bundles
- `dist/api` - API route handlers

You can deploy this to any platform that supports Node.js:

- **Docker**: Create a Dockerfile that runs `npx one serve`
- **Render, Fly.io**: Point to your repo and set the start command to `npx one serve`
- **AWS, GCP, Azure**: Deploy as a containerized application or using their Node.js runtimes

### Vercel

One supports deploying to Vercel's serverless infrastructure using the [Build Output API](https://vercel.com/docs/build-output-api/v3).

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      web: {
        deploy: 'vercel'
      }
    })
  ]
}
```

#### Project Setup

1. **Add vercel.json** to your project root:

```json fileName=vercel.json
{
  "framework": null,
  "cleanUrls": true,
  "buildCommand": "npx one build",
  "installCommand": "npm install"
}
```

Setting `framework: null` tells Vercel to use the pre-built output from the Build Output API instead of auto-detecting a framework.

The `cleanUrls: true` setting is important for SSG routes - it allows URLs like `/about` to serve `about.html` without needing the `.html` extension. Without this, direct navigation to SSG pages will 404.

2. **Install the Vercel CLI** (optional, for manual deploys):

```bash
npm install vercel
```

#### Git Push Deploys

The easiest way to deploy is connecting your Git repository to Vercel:

1. Go to [vercel.com/new](https://vercel.com/new)
2. Import your Git repository
3. Vercel auto-detects settings from `vercel.json`
4. Click **Deploy**

Once connected, every push to your main branch triggers a production deploy. Pull requests get preview deployments automatically.

#### Manual CLI Deploy

For manual deploys or CI/CD pipelines:

```bash
# Build for Vercel
npx one build

# Deploy using Vercel CLI
vercel deploy --prebuilt
```

The `--prebuilt` flag tells Vercel to use the pre-generated output in `.vercel/output` instead of running its own build process.

For production deploys:

```bash
vercel deploy --prebuilt --prod
```

#### Testing Production Build Locally

You can do a basic sanity check of your production build locally:

```bash
# Build the app
npx one build

# Serve the production build
npx one serve
```

Note: This runs the Hono server from `dist/`, not the actual Vercel serverless functions. It's useful for catching build errors and checking that pages render correctly, but it's not a perfect simulation of the Vercel environment.

For true Vercel testing, use a preview deploy:

```bash
# Deploy a preview (not production)
vercel deploy --prebuilt
```

Every push to a non-main branch also triggers a preview deployment automatically when using Git integration. This is the most reliable way to test before promoting to production.

#### What Gets Built

When targeting Vercel, One generates:

- `.vercel/output/static` - Static assets served from Vercel's Edge Network
- `.vercel/output/functions` - Serverless functions for:
  - SSR pages (`+ssr.tsx` files)
  - API routes (`app/api/**`)
  - Loaders (data fetching functions)

Static routes (`+ssg.tsx`) are pre-rendered to `.vercel/output/static` as HTML files.

#### Environment Variables

Set environment variables in your Vercel project dashboard under **Settings > Environment Variables**:

```bash
# Required for SSR and API routes
ONE_SERVER_URL=https://your-app.vercel.app
```

For local development, add to `.env.local`:

```bash fileName=.env.local
ONE_SERVER_URL=http://localhost:8081
```

#### Vercel Project Settings

When using `vercel.json`, settings are auto-configured. If you need to configure manually in the Vercel dashboard:

1. **Framework Preset**: Other
2. **Build Command**: `npx one build`
3. **Output Directory**: `.vercel/output`
4. **Install Command**: `npm install` (or `bun install`, `pnpm install`)

#### Static-Only Sites

If your site only uses SSG routes (`+ssg.tsx`) without SSR, loaders, or API routes, you can simplify:

```tsx fileName=vite.config.ts
one({
  web: {
    deploy: 'vercel',
    defaultRenderMode: 'ssg',
  }
})
```

This generates only static files with no serverless functions, resulting in faster deploys and lower costs.

#### Troubleshooting

**Build fails with "Cannot find module"**: Ensure all dependencies are in `dependencies`, not `devDependencies`, if they're used at runtime.

**404 on dynamic routes**: Check that your route files use the correct suffix (`+ssr.tsx` for server-rendered, `+ssg.tsx` with `generateStaticParams` for static).

**Environment variables not available**: Variables must be added in Vercel dashboard and redeployed. Local `.env` files are not uploaded.

**Slow cold starts**: Consider using `+ssg.tsx` for routes that don't need real-time data. Static routes have no cold start.

## Static Export

If you're only using SSG and SPA routes without loaders or API routes, you can statically serve the `dist/client` directory from any static hosting:

- Netlify
- GitHub Pages
- Cloudflare Pages (static only)
- AWS S3 + CloudFront
- Any CDN or static file server

```bash
npx one build
# Upload dist/client to your static host
```

### Cloudflare Workers

One supports deploying to Cloudflare Workers with full SSR, API routes, and edge performance.

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      web: {
        deploy: 'cloudflare'
      }
    })
  ]
}
```

#### Build and Deploy

```bash
# Build for Cloudflare
npx one build

# Deploy using Wrangler
wrangler deploy --config dist/worker/wrangler.json
```

#### Continuous Deployment

Wrangler is non-interactive in CI, so authenticate with an API token and
account ID instead of `wrangler login`. Store both as secrets in your CI
provider:

- `CLOUDFLARE_API_TOKEN`: a token with the **Edit Cloudflare Workers**
  permission, scoped to the account holding the worker
- `CLOUDFLARE_ACCOUNT_ID`: the account ID the worker deploys to

A GitHub Actions job then only needs to build and deploy:

```yaml
- run: npx one build
- run: wrangler deploy --config dist/worker/wrangler.json
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
```

See [Cloudflare's GitHub Actions guide](https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/) for token setup details.

#### What Gets Built

When targeting Cloudflare, One generates:

- `dist/worker/index.js` - The Cloudflare Worker entry point
- `dist/worker/wrangler.json` - Wrangler configuration for the generated worker
- `dist/client` - Static assets served from Cloudflare's edge
- `dist/server` - Server-side bundles (lazy-loaded per route)
- `dist/api` - API route handlers (lazy-loaded per route)
- `dist/middlewares` - Middleware bundles used by the worker runtime

#### Lazy Loading

One uses a lazy loading pattern for Cloudflare Workers. Route modules are loaded on-demand when matched, not all upfront. This improves cold start times for apps with many routes.

The generated worker config lives at `dist/worker/wrangler.json` and is ready to deploy with Wrangler. The worker serves static assets from `../client` and loads route bundles from the generated `dist/server`, `dist/api`, and `dist/middlewares` outputs as requests come in.

```json
{
  "main": "index.js",
  "no_bundle": true,
  "rules": [{ "type": "ESModule", "globs": ["assets/**/*.js", "assets/**/*.mjs"] }],
  "assets": {
    "directory": "../client",
    "binding": "ASSETS",
    "run_worker_first": true
  }
}
```

#### Accessing Bindings and `waitUntil`

API route handlers receive the worker runtime's `env` and `executionCtx` on their second argument as `worker`:

```tsx fileName=app/api/track+api.ts
import { createAPIRoute } from 'one'

export const POST = createAPIRoute(async (request, { worker }) => {
  // env holds the bindings you defined in wrangler.json (KV, D1, R2, secrets, ASSETS)
  const db = worker?.env?.DB

  // waitUntil keeps the worker alive for fire-and-forget work after the
  // response is sent — useful for analytics, logging, cache warming, etc.
  worker?.executionCtx?.waitUntil(trackAsync(request))

  return Response.json({ ok: true })
})
```

`worker` is `undefined` outside the worker runtime (dev/Node), so the optional chaining keeps the handler runtime-portable. See the [API Routes](/docs/routing-api-routes) doc for full details.

#### Requirements

- Cloudflare Workers with `nodejs_compat` compatibility flag (auto-configured)
- Wrangler CLI for deployment

#### Wrangler Configuration

The generated worker config is ready to use. To customize it, create your own `wrangler.jsonc` in your project root and One will merge it into the generated `dist/worker/wrangler.json`.

One preserves build-required settings like `main`, `assets.directory`, `assets.binding`, `assets.run_worker_first`, the generated worker chunk `rules`, and it always includes `nodejs_compat` in `compatibility_flags`.

Key settings:

```json
{
  "name": "your-app-name",
  "compatibility_flags": ["nodejs_compat"],
  "assets": { "directory": "../client" }
}
```

---

### Apps Onestack.Dev Data Docs Guides Eas (`apps/onestack.dev/data/docs/guides-eas.mdx`)

---
title: Build and Deliver Native Apps with EAS
---

EAS Build and EAS Update are covered in
[Coming from Expo](/native/migrating-to-one#eas-build). For One's own
over-the-air updates, see [Over-the-air Updates](/docs/guides-ota-updates).

---

### Apps Onestack.Dev Data Docs Guides Headless Web (`apps/onestack.dev/data/docs/guides-headless-web.mdx`)

---
title: Headless Web Navigators
---

On web, `<Stack>`, `<Tabs>`, and `<Drawer>` render headless: just the matched route's element. No tab bar, no header, no wrapper elements, no styles. Native is unchanged - react-navigation still renders full chrome exactly as before.

```tsx
import { Tabs } from 'one'

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="home" options={{ title: 'Home' }} />
      <Tabs.Screen name="feed" options={{ title: 'Feed' }} />
    </Tabs>
  )
}
```

On native this renders a bottom tab bar as usual. On web it renders only the focused tab's screen - no bar. Your site's own nav is your tab bar.

## Why

One's web build doesn't ship react-native-web. Rendering full react-navigation chrome (tab bars, headers, drawers) on web meant carrying RNW just to draw them, so on web the navigators stopped rendering any of that and instead expose the underlying navigation state as data. Bring your own DOM, Tamagui, or whatever you already use to build your site's chrome.

## What each navigator renders by default

- **Stack**: the focused screen. Screens with a `presentation` (`sheet`, `modal`, `formSheet`, ...) render as overlays above the base screen - inline with no floating chrome by default, or with real chrome once you mount [Presentations](/docs/components-Presentations).
- **Tabs**: the focused tab's screen, no bar.
- **Drawer**: the focused screen. Open state lives in navigation state - build your own sidebar with [useDrawer](/docs/hooks-useDrawer).
- **Slot**: unchanged, it was always the identity.

## Keeping screens mounted

Set `keepMounted` on a screen and the navigator's default web view keeps it alive once you navigate away, hidden via React 19.2's `<Activity mode="hidden">` instead of unmounting, so scroll position and form state survive.

```tsx
<Stack.Screen name="settings" options={{ keepMounted: true }} />
```

It works the same in all three navigators. In Tabs and Drawer a screen is only kept once it has actually been focused, matching react-navigation's lazy behavior on native. Custom layouts render whatever they want, so they own this themselves - `keepMounted` is on every entry from [useStack](/docs/hooks-useStack), [useTabs](/docs/hooks-useTabs), and [useDrawer](/docs/hooks-useDrawer).

## Building your own layout

Add a component as a child of the navigator. It replaces the headless default on web.

```tsx
// app/(tabs)/_layout.tsx, works on both platforms
import { Link, Tabs, useTabs } from 'one'

function WebTabBar() {
  const { screens, focused } = useTabs()

  return (
    <>
      <nav>
        {screens.map((tab) => (
          <Link
            key={tab.name}
            href={tab.href}
            className={tab.isFocused ? 'active' : undefined}
          >
            {tab.options.title}
          </Link>
        ))}
      </nav>
      {focused.element}
    </>
  )
}

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="home" options={{ title: 'Home' }} />
      <Tabs.Screen name="feed" options={{ title: 'Feed' }} />
      <WebTabBar />
    </Tabs>
  )
}
```

The mechanical rule: **a navigator child that is not config (`Screen`, `Protected`, `Stack.Header`) replaces the headless default on web, and is ignored on native.** The same layout file works on both platforms. Native keeps rendering its usual chrome from the `Screen` config, while web renders `WebTabBar` instead. If a custom layout ever needs a different native version too, split it into a `.web.tsx` file the same way you would any other platform-specific component.

Each navigator has its own hook returning its full state: [useStack](/docs/hooks-useStack), [useTabs](/docs/hooks-useTabs), [useDrawer](/docs/hooks-useDrawer).

## Sheets and modals

Stack screens with `presentation: 'sheet'` or `presentation: 'modal'` work on web with no setup - they render inline, in place, with no floating position or backdrop (headless doesn't add styling for you).

To give them real sheet/modal chrome, mount [Presentations](/docs/components-Presentations) once in your root layout:

```tsx
// app/_layout.tsx
import { Presentations, Slot } from 'one'
import { Modal } from '~/interface/Modal'
import { Sheet } from '~/interface/Sheet'

export default function Layout() {
  return (
    <Presentations web={{ sheet: Sheet, modal: Modal }}>
      <Slot />
    </Presentations>
  )
}
```

`Sheet` and `Modal` are ordinary components receiving `open`, `onOpenChange`, `options`, `screen`, and `children`.

Any layout that declares a sheet or modal screen picks this up automatically, using the same `presentation`/`sheetAllowedDetents` vocabulary you'd write for native:

```tsx
<Stack>
  <Stack.Screen
    name="compose"
    options={{ presentation: 'sheet', sheetAllowedDetents: [0.6, 1] }}
  />
</Stack>
```

Omit `sheet` or `modal` and that kind keeps its headless default, so you only need to write the presentations you actually use.

## If your app renders react-native components on web

The `react-native` to `react-native-web` alias is still there for your own code, so `import { View } from 'react-native'` keeps working on web. What changed is that One's own web output no longer contains any of it, so an app that doesn't import react-native itself never loads it.

One also stopped extracting react-native-web's generated stylesheet during SSR, since that only mattered when the framework rendered react-native components for you. If your app renders them on web and you want their styles in the server HTML instead of after hydration, do the extraction in your root layout:

```tsx
import { Slot, useServerHeadInsertion } from 'one'
import { AppRegistry } from 'react-native'

export default function Layout() {
  useServerHeadInsertion(() => {
    AppRegistry.registerComponent('styles', () => () => null)
    // @ts-expect-error web-only api
    return AppRegistry.getApplication('styles', {}).getStyleElement()
  })

  return <Slot />
}
```

Longer term the recommendation is to drop react-native-web on web. It's in maintenance mode, and One gives you the DOM directly. Use Tamagui, React Strict DOM, or plain elements with CSS for shared primitives.

## Native

Native is unaffected by any of this. `<Stack>`, `<Tabs>`, and `<Drawer>` still render through React Navigation with real headers, a real bottom tab bar, and a real drawer. Nothing about your `Screen` config, `screenOptions`, or `Stack.Header` composition changes.

## Related

- [Presentations](/docs/components-Presentations)
- [useStack](/docs/hooks-useStack)
- [useTabs](/docs/hooks-useTabs)
- [useDrawer](/docs/hooks-useDrawer)
- [Stack](/docs/components-Stack)
- [Tabs](/docs/components-Tabs)
- [Drawer](/docs/components-Drawer)

---

### Apps Onestack.Dev Data Docs Guides Images (`apps/onestack.dev/data/docs/guides-images.mdx`)

---
title: Images
description: Optimized image loading with automatic dimensions and blur placeholders using the ?imagedata import suffix.
---

One provides a built-in way to import images with their metadata at build time using the `?imagedata` suffix. This gives you automatic width, height, and blur placeholder data without any runtime cost.

## Setup

Install `sharp` to enable image processing:

```bash
npm install sharp
```

If `sharp` is not installed, imports will still work but return `0` for dimensions and an empty blur placeholder. A console warning will be shown during build to remind you to install it.

## Usage

Import any image with the `?imagedata` suffix:

```tsx
import heroImage from '/hero.jpg?imagedata'

// heroImage = {
//   src: '/hero.jpg',
//   width: 1920,
//   height: 1080,
//   blurDataURL: 'data:image/jpeg;base64,...'
// }
```

The returned object can be spread directly onto any image component:

```tsx
import heroImage from '/hero.jpg?imagedata'

// Native img element
<img {...heroImage} alt="Hero" />

// Or with explicit props
<img
  src={heroImage.src}
  width={heroImage.width}
  height={heroImage.height}
  style={{ backgroundImage: `url(${heroImage.blurDataURL})` }}
  alt="Hero"
/>
```

## Import paths

The plugin supports both public directory and relative imports:

```tsx
// From public directory (starts with /)
import hero from '/images/hero.jpg?imagedata'

// Relative to current file
import avatar from './avatar.png?imagedata'
import logo from '../assets/logo.svg?imagedata'
```

## TypeScript

Types are included automatically. The import returns an `ImageData` object:

```tsx
import type { ImageData } from 'one'

// ImageData = {
//   src: string
//   width: number
//   height: number
//   blurDataURL: string
// }
```

## Blur placeholders

The blur placeholder is a tiny (10px wide) base64-encoded JPEG that can be used as a low-quality image placeholder (LQIP) while the full image loads:

```tsx
import hero from '/hero.jpg?imagedata'

function HeroImage() {
  const [loaded, setLoaded] = useState(false)

  return (
    <div style={{ position: 'relative' }}>
      {/* Blur placeholder */}
      <img
        src={hero.blurDataURL}
        width={hero.width}
        height={hero.height}
        style={{
          position: 'absolute',
          filter: 'blur(20px)',
          transform: 'scale(1.1)',
          opacity: loaded ? 0 : 1,
          transition: 'opacity 0.3s',
        }}
        alt=""
      />
      {/* Full image */}
      <img
        {...hero}
        onLoad={() => setLoaded(true)}
        alt="Hero"
      />
    </div>
  )
}
```

## Programmatic usage

For one-off usage or build scripts, use the `getImageData` helper:

```tsx
import { getImageData } from 'one/image'

// Paths starting with / resolve from ./public
const heroData = await getImageData('/images/hero.jpg')
// { src: '/images/hero.jpg', width: 1920, height: 1080, blurDataURL: '...' }

// Or use relative paths from your current file
const logoData = await getImageData('../public/images/logo.png')
```

## MDX frontmatter integration

When using `@vxrn/mdx`, images in frontmatter are automatically processed with `getMDX`. If your MDX file has:

```yaml
---
title: My Post
image: /images/post-hero.jpg
---
```

The frontmatter will include `imageMeta` with dimensions and blur placeholder:

```tsx
import { getMDX } from '@vxrn/mdx'

const { frontmatter } = await getMDX(source, { publicDir: './public' })
// frontmatter.image = '/images/post-hero.jpg'
// frontmatter.imageMeta = { width: 1200, height: 630, blurDataURL: '...' }
```

## Supported formats

The plugin uses [sharp](https://sharp.pixelplumbing.com/) for image processing, which supports:

- JPEG / JPG
- PNG
- WebP
- AVIF
- GIF
- TIFF
- SVG (dimensions only, no blur placeholder)

## Benefits

- **Build-time processing** - No runtime cost, dimensions are computed during build
- **Prevents layout shift** - Width and height allow browsers to reserve space before images load
- **Blur placeholders** - Improve perceived performance with LQIP technique
- **Type-safe** - Full TypeScript support with autocomplete
- **Works everywhere** - Compatible with any image component (native `<img>`, React Native `<Image>`, Tamagui, etc.)
- **MDX integration** - Frontmatter images are automatically processed

---

### Apps Onestack.Dev Data Docs Guides Ios Apis (`apps/onestack.dev/data/docs/guides-ios-apis.mdx`)

---
title: Calling iOS APIs
description: Find generated bindings for Apple's iOS frameworks.
---

The [iOS API reference](/ios-reference) lists the frameworks, types and members included in One's generated SDK bindings. It lives separately from these guides because it is regenerated from the same SDK symbol graphs and binding entries as the native code.

Start with a framework such as [CoreBluetooth](/ios-reference/CoreBluetooth), then choose a type such as [CBCentralManager](/ios-reference/CoreBluetooth/CBCentralManager). Each type page shows its Swift declarations, available TypeScript call shapes, iOS availability and any SDK documentation comments. Search the reference sidebar to find a type across frameworks.

Coverage describes the generated snapshot. A framework page also lists skipped declarations and explains why each is unavailable. Check the declaration's iOS version before calling it. Your app still needs Apple's required permissions, usage descriptions and entitlements.

For APIs with output parameters, leave those parameters out of the call. The returned object contains `$ret` for the return value and `$out` for the named output values.

## Background method calls

Use `callSDKMethodAsync` from `one/ios-runtime` to await an instance method that does blocking native work. It keeps the generated method's argument and return types:

```tsx
import type { CNContactStore, CNSaveRequest } from 'one/ios/Contacts'
import { callSDKMethodAsync } from 'one/ios-runtime'

async function saveContacts(store: CNContactStore, request: CNSaveRequest) {
  await callSDKMethodAsync(store, 'execute', request)
}
```

A synchronous method runs on a background queue when it has no main actor requirement. Main actor methods and native async methods retain their declared execution rules. Choose a background call only for APIs that support background access. Methods requiring synchronous JavaScript callbacks are rejected before native execution.

Native failures throw an `Error` with `code: 'E_SDK'`, the original NSError `domain`, and its numeric `nativeCode`. Use these fields to classify a failure independently of its localized message. Calls, returned functions, and subscription errors preserve the same metadata.

## React state from native objects

Two hooks from `one/ios-runtime` turn changing native values into React state. Each returns `{ status, value, error }`, where `status` is `'pending'`, `'value'` or `'error'`. Subscriptions start when the component mounts and stop when it unmounts or its arguments change.

`useSDKProperty(object, property)` rerenders when a native property changes:

```tsx
import { Text } from 'react-native'
import { Progress } from 'one/ios/Foundation'
import { useSDKProperty } from 'one/ios-runtime'

function DownloadProgress({ progress }: { progress: Progress }) {
  const state = useSDKProperty(progress, 'fractionCompleted')
  return <Text>{state.value ?? 0}</Text>
}
```

The generator chooses how to watch each property. Objective-C properties use key-value observing, and properties of Swift classes that adopt `Observable` use Observation tracking. TypeScript only accepts properties that can be watched one of these ways. Key-value observing reports a change only when the framework sends one, so check Apple's documentation to see whether a property supports it.

`useSDKSequence(handle)` follows an `AsyncSequence` or a Combine publisher and holds its latest element:

```tsx
import { Text } from 'react-native'
import { NotificationCenter, NSNotification_Name } from 'one/ios/Foundation'
import { useSDKSequence } from 'one/ios-runtime'

const name = NSNotification_Name.init('app.refresh')
const notifications = NotificationCenter.default.notifications(name)

function LastRefresh() {
  const state = useSDKSequence(notifications)
  return <Text>{state.status === 'value' ? String(state.value.object) : 'waiting'}</Text>
}
```

The element type comes from the declaration, so `state.value` above is a `Notification`. An async sequence keeps iterating while no component listens, and a remounted component starts from its latest element. A publisher subscription starts fresh on each mount. Keep a handle you create in `useMemo` or module scope; a new handle on every render starts a new subscription. Pass `null` or `undefined` to leave the hook pending.

For app setup and custom native modules, see [One Native setup](/docs/native-setup) and [Swift and Kotlin](/docs/native-source).

---

### Apps Onestack.Dev Data Docs Guides Ios Native (`apps/onestack.dev/data/docs/guides-ios-native.mdx`)

---
title: Build or Run the Native iOS App
description: Run your iOS app in development with custom dependencies, or build it for App Store and TestFlight
---

This guide explains how to build and upload your iOS app to the App Store or TestFlight, or run it locally during development with custom native dependencies.

You'll use the `prebuild` command to generate the native code and Xcode project for iOS, open it with Xcode, and either run the app or archive it for distribution.

`one prebuild` generates the Xcode project from `native.app` in
`vite.config.ts`, and `one run:ios` launches it on a simulator. Apps that keep
Expo prebuild follow [Coming from Expo](/native/migrating-to-one#keeping-expo-prebuild).

<Notice>
  **Prerequisites**: To build and run the iOS app, you'll need a Mac with Xcode installed. To upload the app to the App Store or TestFlight, you'll additionally need a paid Apple Developer account.
</Notice>

<Notice theme="blue">
  If you are starting with an existing React Native project without using one of our templates, please follow the instructions in the [Project Setup](#project-setup) section to configure your project first.
</Notice>


## Prebuild

Before you can run or build your iOS app, you'll need to generate the native code and Xcode project for iOS.

Ensure the `prebuild:native` script is defined in your `package.json`:

```json
{
  "scripts": {
    "prebuild:native": "one prebuild"
  }
 }
```

Then run:

```bash
yarn prebuild:native
```

This command may prompt you with some questions and could take a few minutes to install CocoaPods dependencies on its first run.

Once finished, your iOS project will be available in the `./ios` directory and ready for use.


## Running the App in Development

### Using `one run:ios`

The simplest way to run your app after prebuild is using the CLI:

```bash
one run:ios
```

This will build and launch the app on your default iOS simulator. For Android, use `one run:android`.

<Notice theme="blue">
  Make sure the One dev server is running (`one dev`) before launching the native app so it can connect to the bundler.
</Notice>

### Using Xcode

Alternatively, you can open the `.xcworkspace` file in the `./ios` directory. The filename will vary based on your app's name, but you can generally open it by running this command:

```bash
open ios/*.xcworkspace
```

Once Xcode opens, ➊ choose a simulator or physical device, and ➋ click the run button to build and launch the app.




## Building for Production

Open the .xcworkspace file from the ios directory. You can do this by running the following command:

```bash
open ios/*.xcworkspace
```

Once Xcode opens, follow these steps:

* ➊ Click your app project in the Project Navigator.
* ➋ Under “TARGETS,” select your app.
* ➌ Go to the “Signing & Capabilities” tab.
* ➍ Ensure “Automatically manage signing” is checked.
* ➎ Select a development team.




<Notice>
  You need a paid [Apple Developer Program](https://developer.apple.com/programs/) account to upload apps to the App Store or TestFlight. If you're not signed into your Apple Developer account in Xcode, select "Add an Account…" from the dropdown to log in.
</Notice>

<Notice>
  If you encounter any other problems, try to follow the instructions that Xcode give you to fix them.
</Notice>

Once code signing is set up, click "Product" →  "Archive" on the menu bar to start the Archive process.



After the archive process completes, Xcode will automatically open the Organizer window with your build. From there, click "Distribute App" to upload the app to the App Store or TestFlight.



<Notice>
  You can also open the Organizer window by clicking "Window" → "Organizer" on the menu bar.
</Notice>


## Project Setup

<Notice>
  We've set this up in most project templates, so you can skip this section if you've created your project with `npx one`.
</Notice>

### React Native

You should add a `react-native.config.cjs` file in your project root to configure React Native to use Vite as the JS bundler during the native build process:

```js
module.exports = require('one/react-native-config')
```

This routes native build bundling through One and links One's native SwiftUI,
Compose, effects, and safe area implementation through community autolinking.

<Notice>
  If you are using React Native 0.74.x or below, we rely on a patch to the `@react-native-community/cli` package to make this work. For the patch to apply, you'll need to start the dev server at least once.
</Notice>

### One Prebuild

Configure the native application in `vite.config.ts`, then generate the React Native community projects with `one prebuild`:

```ts
one({
  native: {
    app: {
      name: 'MyApp',
      icon: {
        source: './public/app-icon.png',
        backgroundColor: '#000000',
      },
      splash: {
        source: './public/splash.png',
        backgroundColor: '#000000',
        width: 200,
      },
      ios: {
        bundleId: 'com.example.myapp',
        usesNonExemptEncryption: false,
        fileSharing: true,
      },
      android: { applicationId: 'com.example.myapp' },
      imagePicker: {
        camera: 'Take profile photos.',
      },
    },
  },
})
```

One reads this manifest directly. It generates the iOS and Android app icons
and launch screens from the shared sources. `imagePicker.camera` is the iOS camera usage description; prebuild
writes it to `NSCameraUsageDescription` and declares the Android camera permission,
both required for `One.ImagePicker.launchCamera`. The icon must be a square image at least 1024 pixels wide. Splash
artwork is centered at `width` points on iOS and density-independent pixels on
Android. One removes a solid outer border matching `backgroundColor` before sizing
the artwork, then generates density-specific Android assets inside a 288 dp square. The launch screen
stays up until React Native draws its first content, so it never gives way to an
empty root.

`ios.fileSharing` sets `UIFileSharingEnabled` plus
`LSSupportsOpeningDocumentsInPlace` in the generated Info.plist. The user sees
the app's Documents as a folder in the Files app and in document pickers under
On My iPhone, and files the app saves there can be opened in place.

### App appearance, links and store settings

The rest of `native.app` covers appearance, links, and store settings:

- `orientation`: `portrait`, `landscape` or `default` (all four). Unset keeps a
  portrait-only phone.
- `userInterfaceStyle`: `light` or `dark` locks the appearance; `automatic`,
  like unset, follows the system.
- `splash.resizeMode`: `cover` fills the iOS launch screen with untrimmed
  artwork; `contain` (the default) centers it at `width`.
- `splash.dark`: `backgroundColor`, and optionally `source` and
  `backgroundImage`, for a launch in dark appearance.
- `splash.backgroundImage`: a full-bleed image, such as a gradient, under the
  artwork on the launch screen: iOS aspect-fills it, Android stretches it to
  fill (a gradient renders the same either way). Android's system splash
  itself shows only the color and icon.
- The launch screen stays up until the first screen draws. To reveal the app
  later (after a cache loads), call `One.LaunchScreen.preventAutoHide()` while
  modules evaluate and `One.LaunchScreen.hide({ fade: true })` when ready.
- `notifications.apsEnvironment`: `production` for a build signed for
  TestFlight or the App Store; push uses `development` otherwise.
- `ios.accentColor`: `{ light, dark }` hex colors for the app's tint (tab
  selection, text cursor, system controls), read in JS as
  `PlatformColor('AccentColor')`.
- `fonts`: project-relative `.ttf` or `.otf` files bundled into the app. iOS
  lists them in `UIAppFonts`; Android loads them from `assets/fonts`. Use the
  font's own family name in styles.
- `android.adaptiveIcon`: `foreground`, then `background` (an image) or
  `backgroundColor`, and an optional `monochrome` layer for Android 13 themed
  icons. The foreground is a 108 dp square with the artwork inside the central
  66 dp.
- `ios.associatedDomains` (such as `applinks:example.com`) and
  `android.appLinks` (`{ host, pathPrefix }`, verified with `autoVerify`) route
  https links into the app.
- `ios.usesAppleSignIn` adds the Sign in with Apple entitlement.
- `android.permissions` adds manifest permissions (a bare name means
  `android.permission.<name>`); `android.blockedPermissions` removes ones a
  library merges in.
- `android.targetSdk` and `android.compileSdk` set the Gradle SDK levels.
- `ios.googleServicesFile` and `android.googleServicesFile` bundle a Firebase
  `GoogleService-Info.plist` and `google-services.json`; Android also gets the
  `google-services` Gradle plugin. For Google Sign-In, add the plist's
  `REVERSED_CLIENT_ID` to `scheme`.
- `android.minify` runs R8 on release builds, `android.shrinkResources` (which
  needs `minify`) drops unused resources, and `android.proguardRules` appends
  keep rules to `proguard-rules.pro`.
- `ios.infoPlist` and `ios.entitlements` add keys `native.app` does not model,
  such as `NSUserTrackingUsageDescription`. A key the template or another field
  already writes fails prebuild; set it through that field.

### iOS widgets and Live Activities

Set `ios.widgets` to have `one prebuild --platform ios` generate a WidgetKit extension,
App Group entitlements for both targets, and a small SwiftUI widget and Live Activity.
The app writes a typed payload to the App Group. The extension reads that payload
without running the app's JavaScript.

```ts
ios: {
  bundleId: 'com.example.myapp',
  deploymentTarget: '17.0',
  widgets: {
    appGroup: 'group.com.example.myapp',
    kind: 'MyAppStatus',
    displayName: 'My status',
    description: 'Shows the latest status.',
  },
}
```

```ts
import { One } from 'one'

await One.Widgets.write({ title: 'Order', value: '2 of 3', subtitle: 'On the way' })
const id = await One.LiveActivities.start('Order', { status: 'Preparing', value: '1 of 3' })
await One.LiveActivities.update(id, { status: 'On the way', value: '2 of 3' })
const token = await One.LiveActivities.pushToken(id)
const unsubscribe = One.LiveActivities.onPushToken(({ id, token }) => {
  // send the ActivityKit token to your server
})
await One.LiveActivities.end(id)
unsubscribe()
```

Pass `true` as the third argument to `start` to request an ActivityKit push token.
Set `ios.widgets.pushNotifications: true` before prebuild to add the APNs
entitlement; the signing profile must also enable Push Notifications.
If `notifications.push` is already enabled, One includes that entitlement in
the same app entitlement file as the widget's App Group.
`pushToken` returns the current token or `null` while iOS has not issued one;
`onPushToken` reports later token changes. These APIs are separate from notification
permissions and device tokens. App Groups and Live Activities need the corresponding
capabilities in the app's signing profile on a physical device.

One also accepts JSX layouts through `One.iOS.WidgetUI`. The app serializes the
layout to the same typed App Group contract or ActivityKit state. The generated
SwiftUI extension renders it without a JavaScript runtime or an extra package.

```tsx
import { One } from 'one'

const W = One.iOS.WidgetUI

function OrderStatus({ progress, step }: { progress: string; step: number }) {
  return (
    <W.VStack style={{ padding: 12, spacing: 8 }}>
      <W.HStack>
        <W.Image systemName="shippingbox.fill" style={{ color: '#1685B1' }} />
        <W.Text style={{ fontSize: 18, fontWeight: 'bold' }}>Order</W.Text>
      </W.HStack>
      <W.Text>{progress}</W.Text>
      <W.Progress value={step} total={3} style={{ color: '#1685B1' }} />
    </W.VStack>
  )
}

await One.Widgets.writeView(<OrderStatus progress="2 of 3" step={2} />)
const id = await One.LiveActivities.startView('Order', {
  lockScreen: <OrderStatus progress="1 of 3" step={1} />,
  compactLeading: <W.Text>Order</W.Text>,
  compactTrailing: <W.Text>1/3</W.Text>,
  expandedBottom: <OrderStatus progress="1 of 3" step={1} />,
})
await One.LiveActivities.updateView(id, {
  lockScreen: <OrderStatus progress="2 of 3" step={2} />,
  compactLeading: <W.Text>Order</W.Text>,
  compactTrailing: <W.Text>2/3</W.Text>,
  expandedBottom: <OrderStatus progress="2 of 3" step={2} />,
})
await One.LiveActivities.end(id)
```

The activity view also accepts `expandedLeading`, `expandedTrailing`, and
`expandedBottom` for the expanded Dynamic Island. The compact slots and
`minimal` cover its other presentations.

`WidgetUI` supports `Text`, `Image` (SF Symbols), `VStack`, `HStack`,
`ZStack`, `Spacer`, `Divider`, `Progress`, `Gauge`, `Circle`, `Rectangle`,
`RoundedRectangle`, and `Link`. Styles include color, background color, font size,
weight and design, padding, corner radius, spacing, size, opacity, line limit,
and alignment. Colors use six-digit hex values. JSX layouts can contain pure
function components; compute hook state in the app and pass it as props. The
`write` and `writeView` methods update the same widget. Android widgets require
a separate Glance target, storage contract, and lifecycle; this setting
generates only iOS targets.

### iOS Project Setup

<Notice>
  `one prebuild` applies this configuration to generated projects. Follow the manual step only for an existing native project that One did not generate.
</Notice>

In the "Bundle React Native code and images" phase, make the following change in the shell script:

```bash
# ⬇️ Add this line directly above the line that calls react-native-xcode.sh
export CLI_PATH="$("$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/cli.js'")"

`"$NODE_BINARY" --print "require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'"`
```




## Troubleshooting

### `ios/*.xcworkspace` isn't there

Try `cd ios && pod install && cd ..`.

### Error: `node: No such file or directory` during Xcode build

This often comes with `Node found at: /private/var/folders/.../T/xfs-.../node
` and then `/private/var/folders/.../T/xfs-.../node: No such file or directory`.

Try to delete `ios/.xcode.env.local`.

### Error: `No Metro config found`

1. Make sure there's a `react-native.config.cjs` in your project which contains something like this:

    ```js
    module.exports = require('one/react-native-config')
    ```
2. Confirm `native.app.ios.bundleId` is set in `vite.config.ts`.
3. Delete the generated `ios` directory and run `one prebuild` again.

---

### Apps Onestack.Dev Data Docs Guides Isr (`apps/onestack.dev/data/docs/guides-isr.mdx`)

---
title: Incremental Static Regeneration (ISR)
description: Cache SSR pages at the edge for static-like performance
---

Incremental Static Regeneration (ISR) lets you serve SSR pages with static-like performance by caching them at the CDN edge. Pages are generated on the first request, cached, then served instantly to subsequent visitors while being regenerated in the background.

## How ISR Works

ISR uses HTTP cache headers to tell CDNs when to cache and when to revalidate:

1. First visitor requests a page → SSR renders it → CDN caches the result
2. Subsequent visitors get the cached version instantly
3. After the cache expires, the next visitor still gets the cached version while the CDN fetches a fresh one in the background
4. Once regenerated, the new version replaces the cached one

No one ever waits for rendering - the CDN always serves something immediately.

## Setting Up ISR

Use `setResponseHeaders` in your SSR loader to set cache headers:

```tsx fileName=app/blog/[slug]+ssr.tsx
import { setResponseHeaders, useLoader } from 'one'

export async function loader({ params }) {
  // Set ISR cache headers
  await setResponseHeaders((headers) => {
    headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400')
  })

  const post = await fetchPost(params.slug)
  return { post }
}

export default function BlogPost() {
  const { post } = useLoader(loader)
  return <article>{post.content}</article>
}
```

## Cache Header Patterns

### Basic ISR (1 hour cache, 1 day stale)

```tsx
headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400')
```

- `public` - Allow CDN caching
- `s-maxage=3600` - CDN caches for 1 hour (3600 seconds)
- `stale-while-revalidate=86400` - Serve stale content for up to 1 day while revalidating

### Aggressive caching (1 day cache)

For content that rarely changes:

```tsx
headers.set('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=604800')
```

### Short cache (5 minutes)

For frequently updated content:

```tsx
headers.set('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=3600')
```

### No caching (dynamic)

For user-specific or real-time content:

```tsx
headers.set('Cache-Control', 'private, no-store')
```

## Conditional Caching

Cache public pages, skip for authenticated users:

```tsx fileName=app/dashboard+ssr.tsx
export async function loader({ request }) {
  const cookies = request?.headers.get('Cookie') || ''
  const isAuthenticated = cookies.includes('session=')

  if (isAuthenticated) {
    // Don't cache user-specific content
    await setResponseHeaders((headers) => {
      headers.set('Cache-Control', 'private, no-store')
    })
    return { user: await getUser(cookies) }
  }

  // Cache the public version
  await setResponseHeaders((headers) => {
    headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400')
  })
  return { user: null }
}
```

## API Routes

For API routes, set headers directly on the Response:

```tsx fileName=app/api/posts+api.ts
export async function GET(request: Request) {
  const posts = await fetchPosts()

  return new Response(JSON.stringify(posts), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=3600',
    },
  })
}
```

## CDN Compatibility

ISR requires a CDN that supports `stale-while-revalidate`:

| Platform | Support |
|----------|---------|
| Vercel | Full support |
| AWS CloudFront | Full support |
| Fastly | Full support |
| Cloudflare | No `stale-while-revalidate` support |

For Cloudflare, use `s-maxage` only - pages will be fully regenerated after cache expires rather than served stale during revalidation.

## When to Use ISR vs SSG vs SSR

| Strategy | Best For | Trade-offs |
|----------|----------|------------|
| **SSG** (`+ssg.tsx`) | Content known at build time | No runtime cost, but requires rebuild for updates |
| **ISR** (`+ssr.tsx` + cache headers) | Content that changes occasionally | Fast after first request, fresh within cache window |
| **SSR** (`+ssr.tsx`, no cache) | User-specific or real-time content | Always fresh, but slower |

### Use ISR when:

- Content updates periodically but doesn't need instant updates
- You have many pages that can't all be pre-built (e.g., large blog, product catalog)
- You want SSR flexibility with near-static performance

### Use SSG when:

- Content is known at build time
- Updates can wait for a rebuild
- You want zero runtime cost

### Use SSR (no cache) when:

- Content is user-specific
- Data must be real-time
- Content varies per request (A/B tests, geolocation)

## Example: Blog with ISR

```tsx fileName=app/blog/[slug]+ssr.tsx
import { setResponseHeaders, useLoader } from 'one'
import { getPost } from '../lib/posts'

export async function loader({ params }) {
  const post = await getPost(params.slug)

  if (!post) {
    throw new Response('Not Found', { status: 404 })
  }

  // Cache for 1 hour, serve stale for up to 1 week
  await setResponseHeaders((headers) => {
    headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=604800')
  })

  return { post }
}

export default function BlogPost() {
  const { post } = useLoader(loader)

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <div>{post.content}</div>
    </article>
  )
}
```

First visitor to `/blog/my-post` triggers SSR. The CDN caches the result. For the next hour, all visitors get the cached version instantly. After an hour, the next visitor still gets the cached version while the CDN regenerates in the background.

---

### Apps Onestack.Dev Data Docs Guides Loader Redirects (`apps/onestack.dev/data/docs/guides-loader-redirects.mdx`)

---
title: Loader Redirects
---

Loader redirects let you protect routes server-side on both web and native. When a loader calls `redirect()`, unauthorized users are redirected before any sensitive data or page content reaches the client — for direct page loads, client-side `<Link>` navigation, and React Native navigation.

## Basic Usage

Use the [`redirect()`](/docs/helpers-redirect) helper in a loader to guard a route. Use `+ssr` so the loader runs on every request:

```tsx fileName=app/dashboard+ssr.tsx
import { redirect, useLoader } from 'one'
import type { LoaderProps } from 'one'

export async function loader({ request }: LoaderProps) {
  const session = await getSession(request)

  if (!session) {
    throw redirect('/login')
  }

  return { user: session.user }
}

export default function Dashboard() {
  const { user } = useLoader(loader)
  return <Text>Welcome, {user.name}</Text>
}
```

Both `throw redirect()` and `return redirect()` work. `throw` is often cleaner since it stops execution immediately.

## How It Works

### Direct Page Load

When a user navigates directly to a protected URL (typing it in, refreshing, or following an external link), the loader runs on the server. If it calls `redirect()`, the server returns a standard HTTP 302 response and the browser redirects to the target page. The protected page never renders.

### Client-Side Navigation

When a user clicks a `<Link>` to a protected route, the behavior is more nuanced. Loaders run on the server, and the client fetches the result as a JavaScript module. Without special handling, the browser's module loader would silently follow a raw 302, try to parse the HTML login page as JavaScript, and fail.

One handles this differently:

1. The server detects the redirect response from the loader
2. Instead of returning a raw HTTP 302, it transforms the response into a JS module containing only redirect metadata — something like `{ __oneRedirect: "/login", __oneRedirectStatus: 302 }`
3. The client receives this module and detects the redirect signal before navigating or rendering anything
4. The client navigates to the redirect target using a `REPLACE` action

The protected page never appears. No loader data, no component JavaScript output, no flash of unauthorized content. The server only sends back the redirect path.

### React Native

On native, loaders are fetched from the dev/production server and evaluated on the device. The same redirect mechanism applies:

1. The native app fetches the loader JavaScript from the server
2. The server runs the loader, detects the redirect, and transforms it into a module with redirect metadata
3. The native client detects the `__oneRedirect` signal after evaluating the loader
4. The router performs a `replace` navigation to the redirect target

The component never renders with protected data. This works for both `<Link>` navigation and programmatic navigation via `router.push()` / `router.navigate()`.

## Protecting Multiple Routes

Use a shared auth helper to keep loaders clean:

```tsx fileName=features/auth/server.ts
import { redirect } from 'one'
import type { LoaderProps } from 'one'

export async function requireAuth(request: LoaderProps['request']) {
  const session = await getSession(request)

  if (!session) {
    throw redirect('/login')
  }

  return session
}
```

```tsx fileName=app/dashboard+ssr.tsx
import { useLoader } from 'one'
import { requireAuth } from '~/features/auth/server'

export async function loader({ request }) {
  const session = await requireAuth(request)
  return { user: session.user }
}

export default function Dashboard() {
  const { user } = useLoader(loader)
  return <Text>Welcome, {user.name}</Text>
}
```

```tsx fileName=app/settings+ssr.tsx
import { useLoader } from 'one'
import { requireAuth } from '~/features/auth/server'

export async function loader({ request }) {
  const session = await requireAuth(request)
  const prefs = await getUserPreferences(session.userId)
  return { prefs }
}

export default function Settings() {
  const { prefs } = useLoader(loader)
  return <PreferencesForm defaults={prefs} />
}
```

## Redirect Status Codes

The `redirect()` helper accepts an optional status code:

```tsx
import { redirect } from 'one'

// 302 (default) - temporary redirect
throw redirect('/login')

// 301 - permanent redirect
throw redirect('/new-path', 301)

// 307 - temporary redirect, preserves HTTP method
throw redirect('/login', 307)
```

For auth guards, the default 302 is usually what you want. Use 307 if you need to preserve the original HTTP method (e.g., a POST request should remain a POST after redirect).

## SSR vs SSG Routes

Loader redirects work best with `+ssr` routes because the loader runs on every request with access to the `request` object (cookies, headers, etc.):

| Route Type | Loader Runs | Auth Check |
|---|---|---|
| `+ssr` | Every request | Checks auth per-request via cookies/headers |
| `+ssg` | Once at build time | No per-request auth — only checks build-time state |
| `+spa` | Every request | Same as SSR for loader execution |

For protected routes, always use `+ssr` so auth is checked on every navigation.

When an SSG route redirects at build time, One generates a static redirect file. If a runtime server is present in production, it re-evaluates the loader per-request — so authenticated users see the real page while unauthenticated users get redirected.

## What Gets Protected

Loader redirects protect **data**. When a redirect fires:

- The loader data is never sent to the client
- The page component never renders on the server for that request
- The client receives only the redirect target path

Component JavaScript is always downloadable (this is true in every framework). Loaders protect the sensitive data your components would display — user profiles, account details, admin data, etc.

## Loader Redirects vs Other Approaches

| Approach | Best For | Platform |
|---|---|---|
| **Loader redirects** | SSR data protection, server-side auth | Web + Native |
| **`<Protected />`** | Hiding routes from navigation entirely | Web + Native |
| **`<Redirect />`** | Client-side redirect in component render | Web + Native |
| **Middleware** | URL rewrites, API auth, request-level checks | Web (server) |

You can combine these approaches. For example, use loader redirects to protect data on SSR routes, and `<Protected />` to hide those routes from the tab bar on native.

## Related

- [redirect](/docs/helpers-redirect) - API reference for the redirect helper
- [Loaders](/docs/routing-loader) - Full loader documentation
- [Authentication](/docs/guides-authentication) - Complete authentication patterns
- [Middlewares](/docs/routing-middlewares) - Server-side request interception
- [Protected](/docs/components-Protected) - Declarative route protection component

---

### Apps Onestack.Dev Data Docs Guides Mdx (`apps/onestack.dev/data/docs/guides-mdx.mdx`)

---
title: MDX Guide
description: Setting up MDX for web
---

In building out this beautiful website, and the equally beautiful [tamagui.dev](https://tamagui.dev), one of the trickier
parts was getting MDX to work well.

Since many blogs, apps, and documentation sites want to display Markdown content, we figured a guide showing how
we did it will be useful.

The `@vxrn/mdx` package bundles together [Rehype](https://github.com/rehypejs/rehype) plugins alongside [mdx-bundler](https://github.com/kentcdodds/mdx-bundler) to provide a batteries-included MDX experience with:

- **Syntax highlighting** via refractor
- **GitHub Flavored Markdown** support
- **Smart typography** via remark-smartypants (with code blocks properly preserved)
- **Auto-linked headings** with slugified IDs
- **Image metadata extraction** including blur placeholders
- **Reading time** calculation
- **Frontmatter** with heading extraction for table of contents

### Setting up your data dir

The first step is to create a directory for your mdx files to live:

<RouteTree
  routes={[
    { name: 'data', children: [
      { name: 'hello-world.mdx', description: 'Your MDX content goes here' },
    ] },
  ]}
/>

Throw some stuff in that file:

```
---
title: MDX Guide
description: Setting up MDX for web
---

In building out this beautiful website, and the equally beautiful [tamagui.dev](https://tamagui.dev), the surprising
most difficult part was getting MDX to work well...
```

### Setting up `@vxrn/mdx`

Next, we add our mdx dependencies:

```bash
yarn add @vxrn/mdx mdx-bundler
```

We set up our MDX components (using Tamagui in this example):

```tsx fileName=features/MDXComponents.tsx
import { Text } from "react-native";

export const components = {
  h1: ({ children }) => (
    <Text style={{ fontSize: 24, fontWeight: "bold", marginBottom: 10 }}>
      {children}
    </Text>
  ),
  h2: ({ children }) => (
    <Text style={{ fontSize: 20, fontWeight: "bold", marginBottom: 8 }}>
      {children}
    </Text>
  ),
  h3: ({ children }) => (
    <Text style={{ fontSize: 18, fontWeight: "bold", marginBottom: 6 }}>
      {children}
    </Text>
  ),
  p: ({ children }) => <Text style={{ marginBottom: 10 }}>{children}</Text>,
};

```

And then all we need is a new route:

```tsx fileName=app/docs/[slug].tsx
import { getMDXComponent } from "mdx-bundler/client";
import { useMemo } from "react";
import { useLoader } from "one";
import { Text, View } from "react-native";
import { components } from "~/features/MDXComponents";

export async function generateStaticParams() {
  const { getAllFrontmatter } = await import("@vxrn/mdx");
  const frontmatters = getAllFrontmatter("data");
  const paths = frontmatters.map(({ slug }) => ({
    slug: slug.replace(/.*docs\//, ""),
  }));
  return paths;
}

export async function loader({ params }) {
  const { getMDXBySlug } = await import("@vxrn/mdx");
  const { frontmatter, code } = await getMDXBySlug("data", params.slug);
  return {
    frontmatter,
    code,
  };
}

export function DocsPage() {
  const { code, frontmatter } = useLoader(loader);
  const Component = useMemo(() => getMDXComponent(code), [code]);

  return (
    <View style={{ padding: 20 }}>
      <Text style={{ fontSize: 48, fontWeight: "bold", marginBottom: 10 }}>
        {frontmatter.title}
      </Text>
      <Component components={components} />
    </View>
  );
}

```

Note that we `await import` the `@vxrn/mdx` library - this is because One by default builds out your server
and API routes to CommonJS, but some of the rehype/remak dependencies are ESM. Long story short - this makes it work.

### Configuring Vite for MDX Support

To ensure that MDX works correctly with your One project, you'll need to configure Vite. Update your `vite.config.ts` file with the following content:

```ts fileName=vite.config.ts
import { defineConfig } from 'vite'
import { one } from 'one/vite'

export default defineConfig({
  ssr: {
    noExternal: true,
    external: ['@vxrn/mdx'],
  },
  plugins: [
    one({
      web: {
        defaultRenderMode: 'ssg',
      },
    }),
  ],
})
```

This configuration does a few important things:

1. It sets `noExternal: true` in the SSR options, which tells Vite to bundle all dependencies for server-side rendering.
2. It explicitly marks `@vxrn/mdx` as an external dependency, which is necessary because it contains ESM modules that shouldn't be bundled.



---

That *should* do it. We say should, because there's a lot going on here, and many sub-deps inside `@vxrn/mdx` that need to work together. Let us know if this works for you!

---

### Apps Onestack.Dev Data Docs Guides Migrating Create React App Cra To Vite With One (`apps/onestack.dev/data/docs/guides-migrating-create-react-app-cra-to-vite-with-one.mdx`)

---
title: Migrate from Create React App (CRA) to Vite with One
description: One makes a great replacement for Create React App. With a few simple steps, you can move over to One and enjoy the power of the Vite ecosystem.
---

Migrating from Create React App (CRA) to One is easy. In this guide we'll use the default CRA output and show how you can move over to One.

One is a React framework for web, iOS, and Android, built on the power of Vite - it's actually just a single Vite plugin. One saves you a lot of complexity when moving from CRA, it offers faster build times, built-in TypeScript support, SPA or server rendering, file system routes, and seamless cross-platform development capabilities.

Let's walk through the migration process step by step.

### Step 1: Add One to Your Project

First, let's add One and its dependencies to your project:

```bash
yarn add one
```

### Step 2: Update Your Project Structure

One uses a file-based routing system, which means we need to adjust our project structure slightly. Here's what we'll do:

<RouteTree
  routes={[
    { name: 'package.json' },
    { name: 'vite.config.ts', add: true, highlight: true, description: 'Add a vite.config.ts file to configure our build system.'},
    { name: 'public', children: [
      { name: 'favicon.ico' },
      { name: 'index.html', delete: true, description: 'Remove index.html as One will handle app rendering.' },
    ] },
    { name: 'app', add: true, description: 'Add an app directory for our file system routes.', children: [
      { name: 'index.tsx', add: true, highlight: true, description: 'Move and rename App.js to index.tsx.' },
    ] },
    { name: 'src', children: [
      { name: 'App.css' },
      { name: 'App.js', delete: true, description: 'Move App.js content to app/index.tsx.' },
      { name: 'App.test.js' },
      { name: 'index.css' },
      { name: 'index.js', delete: true, description:'Remove index.js. One\'s file system router replaces this.'  },
      { name: 'reportWebVitals.js', delete: true, description: 'Remove reportWebVitals.js.' },
      { name: 'setupTests.js', delete: true, description: 'Remove setupTests.js. We\'ll use Vitest for testing.' }
    ] }
  ]}
/>

### Step 3: Configure Vite

Create a new `vite.config.ts` file in your project root with the following content:

```tsx fileName=vite.config.ts
import type { UserConfig } from 'vite'
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      web: {
        defaultRenderMode: 'spa'
      }
    }),
  ],
} satisfies UserConfig
```

This minimal configuration sets up One with Vite, enabling all the goodness that comes with it. Note that we're setting `defaultRenderMode` to 'spa' (Single Page App) for compatibility with Create React App. This ensures that your app behaves similarly to how it did with CRA, rendering entirely on the client side.

The 'spa' mode is the simplest strategy, where your app is not rendered on the server at all. Instead, only the JavaScript to render your page is served to the user's browser. This avoids some of the complexity associated with server-side rendering, making it a good starting point for migrating from CRA.

If you want to explore other rendering modes later, such as Static Site Generation (SSG) or Server-Side Rendering (SSR), you can change this setting. Each mode has its own trade-offs in terms of performance, SEO, and complexity. For more information on the available render modes and their implications, refer to the [One configuration documentation](/docs/configuration#webdefaultrendermode).

### Step 4: Update package.json Scripts

Let's update our `package.json` scripts to use One's commands:

```json fileName=package.json
{
  "scripts": {
    "start": "one dev",
    "build": "one build",
    "test": "vitest run"
  }
}
```

These new scripts replace the CRA equivalents, giving you access to One's optimized development and build processes.

### Step 5: Update Your Main Component

Now, let's update our main component. We'll move `src/App.js` to `app/index.tsx` and make a few adjustments:

```tsx fileName=app/index.tsx
import '~/src/App.css'
import logo from '~/src/logo.svg'

export default function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>app/index.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  )
}
```

Notice how we've updated the import paths using the `~` alias. One supports `tsconfig` by default, and will also add a `tsconfig.json` file if you don't have one already, with the `~` path alias configured. Of course, you can change this to suit your needs.

### Step 6: Remove Unnecessary Files

You can now safely remove the following files:

- `public/index.html`
- `src/index.js`
- `src/reportWebVitals.js`
- `src/setupTests.js`

One handles app rendering and provides its own optimized setup for these functionalities.

### Step 7: Run Your Migrated App

You're all set! Now you can start your newly migrated One app:

```bash
yarn start
```

Visit [http://localhost:8081](http://localhost:8081) to see your app in action. Use the `--port` flag if you need to change the port.

## Next Steps

Congratulations! You've successfully migrated from Create React App to One. Here are some next steps to consider:

1. **Explore One's features**: Check out the [One documentation](/docs/introduction) to learn about its powerful features like SSR, file-based routing, and more.

2. **Optimize for production**: One provides excellent production optimizations out of the box. Run `yarn build` to create an optimized production build.

3. **Add Tamagui**: For a powerful, fully type-safe UI kit that works seamlessly with One, consider adding [Tamagui](/docs/guides-tamagui). It provides SSR-safe styling, animations, and themes.

4. **Implement dark mode**: One makes it easy to add SSR-safe dark mode. Check out our [Dark Mode guide](/docs/guides-dark-mode) for a quick setup.

---

### Apps Onestack.Dev Data Docs Guides Open Graph (`apps/onestack.dev/data/docs/guides-open-graph.mdx`)

---
title: Dynamic OpenGraph Images
description: Generate beautiful social sharing images dynamically using API routes and @vercel/og.
---

OpenGraph images are the preview images that appear when you share a link on social media. One supports dynamic OG image generation using API routes and the [`@vercel/og`](https://vercel.com/docs/functions/og-image-generation) library.

## Setup

Install the `@vercel/og` package:

```bash
npm install @vercel/og
```

## Basic API Route

Create an API route that generates images dynamically:

```tsx fileName=app/api/og+api.tsx
import { ImageResponse } from '@vercel/og'

export const GET = async (request: Request) => {
  const url = new URL(request.url)
  const title = url.searchParams.get('title') || 'One Framework'

  return new ImageResponse(
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        width: '100%',
        height: '100%',
        backgroundColor: '#000',
        color: '#fff',
        fontSize: 60,
        fontWeight: 700,
      }}
    >
      {title}
    </div>,
    {
      width: 1200,
      height: 630,
    }
  )
}
```

This creates an endpoint at `/api/og` that generates PNG images on the fly.

## Using in Pages

Reference the dynamic image in your page's meta tags:

```tsx fileName=app/blog/[slug].tsx
import { Head, useParams } from 'one'

export default function BlogPost() {
  const { slug } = useParams()
  const title = 'My Blog Post'
  const ogUrl = `/api/og?title=${encodeURIComponent(title)}`

  return (
    <>
      <Head>
        <meta property="og:image" content={ogUrl} />
        <meta property="og:image:width" content="1200" />
        <meta property="og:image:height" content="630" />
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:image" content={ogUrl} />
      </Head>
      {/* page content */}
    </>
  )
}
```

## Custom Fonts

Load custom fonts for better typography:

```tsx fileName=app/api/og+api.tsx
import { ImageResponse } from '@vercel/og'
import { readFile } from 'fs/promises'
import { join } from 'path'

// Load font once at module level
const interBold = readFile(join(process.cwd(), 'public/fonts/Inter-Bold.ttf'))

export const GET = async (request: Request) => {
  const url = new URL(request.url)
  const title = url.searchParams.get('title') || 'One Framework'

  return new ImageResponse(
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        width: '100%',
        height: '100%',
        backgroundColor: '#000',
        color: '#fff',
        fontFamily: 'Inter',
        fontSize: 60,
      }}
    >
      {title}
    </div>,
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Inter',
          data: await interBold,
          weight: 700,
        },
      ],
    }
  )
}
```

## With Background Images

Include images in your OG graphics:

```tsx fileName=app/api/og+api.tsx
import { ImageResponse } from '@vercel/og'
import { readFile } from 'fs/promises'
import { join } from 'path'

export const GET = async (request: Request) => {
  const url = new URL(request.url)
  const title = url.searchParams.get('title') || 'One Framework'

  // Read local image as base64
  const bgImage = await readFile(join(process.cwd(), 'public/og-background.png'))
  const bgBase64 = `data:image/png;base64,${bgImage.toString('base64')}`

  return new ImageResponse(
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        width: '100%',
        height: '100%',
        backgroundImage: `url(${bgBase64})`,
        backgroundSize: 'cover',
      }}
    >
      <div
        style={{
          fontSize: 60,
          fontWeight: 700,
          color: '#fff',
          textShadow: '0 2px 10px rgba(0,0,0,0.5)',
        }}
      >
        {title}
      </div>
    </div>,
    { width: 1200, height: 630 }
  )
}
```

## Helper Function

Create a utility to generate OG URLs consistently:

```tsx fileName=features/og.ts
import { getURL } from 'one'

type OgParams = {
  title: string
  type?: 'blog' | 'docs' | 'default'
  author?: string
  section?: string
}

export function ogUrl(params: OgParams): string {
  const base = getURL()
  const search = new URLSearchParams()

  Object.entries(params).forEach(([key, value]) => {
    if (value) search.set(key, value)
  })

  return `${base}/api/og?${search.toString()}`
}
```

Use it in pages:

```tsx
import { ogUrl } from '~/features/og'

const imageUrl = ogUrl({
  title: 'My Blog Post',
  type: 'blog',
  author: 'Jane Doe',
})
```

## Caching

Add cache headers for better performance:

```tsx fileName=app/api/og+api.tsx
import { ImageResponse } from '@vercel/og'
import { setResponseHeaders } from 'one'

export const GET = async (request: Request) => {
  // Cache for 1 day on CDN, 1 week stale-while-revalidate
  await setResponseHeaders((headers) => {
    headers.set('Cache-Control', 'public, s-maxage=86400, stale-while-revalidate=604800')
  })

  const url = new URL(request.url)
  const title = url.searchParams.get('title') || 'One Framework'

  return new ImageResponse(
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        width: '100%',
        height: '100%',
        backgroundColor: '#000',
        color: '#fff',
        fontSize: 60,
      }}
    >
      {title}
    </div>,
    { width: 1200, height: 630 }
  )
}
```

## Debugging

Test your OG images directly in the browser:

```
http://localhost:8081/api/og?title=Test+Title
```

Use the [OpenGraph debugger](https://www.opengraph.xyz/) or social platform preview tools to verify how images appear when shared.

---

### Apps Onestack.Dev Data Docs Guides Ota Updates (`apps/onestack.dev/data/docs/guides-ota-updates.mdx`)

---
title: Over-the-air Updates
---

Over-the-air (OTA) updates ship JavaScript and asset changes to installed native apps without going through TestFlight or the Play Store.

## One.Updates

One ships its own OTA client. Set the update url and runtime version in `native.app`:

```ts fileName=vite.config.ts
one({
  native: {
    app: {
      // ...
      updates: {
        url: 'https://updates.example.com',
        runtimeVersion: '1.0',
      },
    },
  },
})
```

Prebuild stamps both into the binary. Publish from the app's source and serve the output directory at `<url>/<platform>/<runtimeVersion>/`:

```bash
one updates publish --platform ios --out ./ota-out
one updates publish --platform android --out ./ota-out-android
```

The launcher picks the newest downloaded update for the binary's runtime version before React Native starts, and rolls back when an update fails before first render. See [Updates](/native/updates) for the client API and publish options.

## hot-updater

[hot-updater](https://hot-updater.dev) is a self-hosted alternative (Supabase, Cloudflare R2, S3, custom). Install and init:

```bash
bun add @hot-updater/react-native react-native-mmkv
bun add -D hot-updater @hot-updater/bare @hot-updater/supabase
npx hot-updater init
```

Wrap your app:

```tsx
import { HotUpdater } from '@hot-updater/react-native'

export default HotUpdater.wrap({
  source: process.env.ONE_PUBLIC_HOT_UPDATER_URL,
})(App)
```

Publish:

```bash
npx hot-updater deploy -p ios -c production
npx hot-updater deploy -p android -c production
```

See [hot-updater.dev/docs](https://hot-updater.dev/docs/) for full backend setup. Apps on EAS Update follow [Coming from Expo](/native/migrating-to-one#eas-update).

## What's OTA-safe

Only publish OTA updates for changes that don't touch the native binary:

- ✅ JavaScript, TypeScript, JSX, styles, copy, bundled assets
- ❌ native dependencies, `native.app` fields that affect native output, any native iOS/Android file

For those, bump `runtimeVersion`, build a fresh TestFlight/Play binary, and only then publish OTA updates against the new runtime. The runtime version gates delivery so devices on the old binary don't receive incompatible updates.

## Owning the config files yourself

If you'd rather commit `babel.config.cjs` / `metro.config.cjs` and customize them (custom Metro resolvers, watcher options, etc.), generate them once:

<Notice theme="yellow">
  Metro mode runs no Babel, so `babel.config.cjs` has no effect on the bundle. To replace a Babel plugin, use `native.bundlerOptions.nativeTransformModules` — see [Metro mode](/docs/metro-mode#no-babel).
</Notice>


```bash
npx one metro-eject
```

This writes shim files (delegating to `one/babel-preset` and `one/metro-config`) **without** the `@one/generated` marker comment. After ejecting:

- The files are yours — edit freely and commit them.
- The CI postinstall hook detects the missing marker and won't touch them.
- You stay in charge of keeping them in sync with future One releases.

A typical ejected `metro.config.cjs` looks like:

```js
const { withOne } = require('one/metro-config')
const oneBundlerOptions = {
  routerRoot: 'app',
}

module.exports = (async () => {
  const config = await withOne(__dirname, oneBundlerOptions)
  // your customizations here
  return config
})()
```

Pass `--force` to overwrite existing files.

## Reference

- [Updates](/native/updates)
- [hot-updater documentation](https://hot-updater.dev/docs/)

---

### Apps Onestack.Dev Data Docs Guides Skew Protection (`apps/onestack.dev/data/docs/guides-skew-protection.mdx`)

---
title: Skew Protection
description: Handle stale client bundles gracefully after deployments
---

After a deployment, users with open tabs still have old JavaScript bundles. When they navigate, dynamic imports reference chunk URLs that no longer exist on the server, causing 404s, broken UI, or blank pages.

One has built-in skew protection that detects these failures and recovers automatically.

## Configuration

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      web: {
        skewProtection: true // true (default) | 'proactive' | false
      }
    })
  ]
}
```

## Error Recovery (default)

With `skewProtection: true` (the default), One detects chunk load failures and automatically reloads the page. This covers two failure paths:

- **Vite preload errors** — caught via the `vite:preloadError` event when Vite's module preloader fails to fetch a chunk
- **Dynamic import errors** — caught when One's own `dynamicImport()` calls fail with browser-specific chunk load error messages (Chrome, Firefox, Safari)

When a failure is detected, the page reloads so the browser fetches fresh HTML with the new asset URLs. A sessionStorage guard prevents infinite reload loops — if a reload already happened within the last 10 seconds, the guard stops further reloads.

No configuration needed. This is always on unless you explicitly set `skewProtection: false`.

## Proactive Mode

```tsx fileName=vite.config.ts
one({
  web: {
    skewProtection: 'proactive'
  }
})
```

Proactive mode adds version polling on top of error recovery. Instead of waiting for a chunk to fail, the client periodically checks whether a new version has been deployed and forces a full-page navigation on the next link click.

### How it works

1. At build time, One emits a `version.json` file containing the build's cache key
2. The client polls `version.json` every 2 minutes with cache-busting headers
3. When the polled version doesn't match the running bundle's version, the app is marked as stale
4. On the next client-side navigation, instead of doing an SPA transition, One does a full `window.location.href` navigation so the server returns fresh HTML with new assets

This is similar to how SvelteKit handles version skew. The user never sees a broken page — they just get a slightly slower (full-page) navigation that loads the new deployment.

### Events

When a version mismatch is detected, One dispatches a `one-version-update` custom event on `window`:

```ts
window.addEventListener('one-version-update', (e) => {
  const version = (e as CustomEvent<{ version?: string }>).detail.version
  console.log('new version available:', version)
  // show a toast, banner, etc.
})
```

You can use this to show users a "new version available" banner with a manual refresh button instead of waiting for the next navigation.

### Window flag

The `window.__oneVersionStale` flag is set to `true` when a version mismatch is detected. You can check this from anywhere in your app:

```ts
if ((window as any).__oneVersionStale) {
  // app has a newer deployment available
}
```

## Resilient imports

One also exports the helpers behind this recovery from `'one'`, for your own lazy boundaries and dynamic imports:

- **`lazyWithRetry(loader, options?)`**: a drop-in replacement for `React.lazy`. It retries a transient chunk failure in place before React caches the result, so one failed fetch does not poison the component for the whole session.
- **`loadWithRetry(loader, options?)`**: wraps any async loader with retry and backoff. On an unrecoverable chunk-load error it triggers the debounced reload instead of rejecting into a surviving tree.
- **`isChunkLoadError(err)`**: returns true when the error matches a browser chunk-load failure message.
- **`handleSkewError()`**: schedules the debounced one-time reload. Returns true when a reload was scheduled, false on the server or when a reload already happened in the last 10 seconds.

```tsx
import { Suspense } from 'react'
import { handleSkewError, isChunkLoadError, lazyWithRetry, loadWithRetry } from 'one'

// resilient lazy component: retries transient failures, reloads on deploy skew
const SettingsPage = lazyWithRetry(() => import('../components/SettingsPage'))

export function SettingsRoute() {
  return (
    <Suspense fallback={null}>
      <SettingsPage />
    </Suspense>
  )
}

// resilient data loader with custom retry budget
const stats = await loadWithRetry(() => fetch('/api/stats').then((res) => res.json()), {
  attempts: 5,
  delayMs: 1000,
})

// manual recovery in your own dynamic import
try {
  await import('../components/HeavyPanel')
} catch (err) {
  if (isChunkLoadError(err)) handleSkewError()
}
```

## Disabling

To turn off skew protection entirely:

```tsx fileName=vite.config.ts
one({
  web: {
    skewProtection: false
  }
})
```

This disables both the error recovery reload and the proactive polling. Chunk load failures will be silently swallowed as they were before this feature existed.

## CDN Edge Caching

With skew protection enabled, One automatically sets cache headers that allow CDN edge caching for SSG and SPA pages:

```
cache-control: public, s-maxage=60, stale-while-revalidate=120
```

This means:
- **CDN caches pages for 60 seconds** — subsequent requests are served from edge, cutting TTFB to ~50ms
- **Stale-while-revalidate for 120 seconds** — even after 60s, the CDN serves stale content while fetching fresh in the background
- **SSR pages are not cached** — they return `no-cache` since content is dynamic per request

### Why this is safe

Previously, CDN caching HTML was risky: after a deploy, cached HTML references old JS bundles that may not exist, causing 404s and broken pages.

Skew protection solves this. If a cached page tries to load a missing chunk:
1. The chunk request 404s
2. Skew protection detects the failure via `vite:preloadError`
3. The page auto-reloads, fetching fresh HTML from the origin
4. User sees the new deployment

The worst case is a brief flash on reload — much better than a broken page.

### Overriding cache headers

If you need custom cache behavior, set headers in your loader or middleware:

```tsx
import { setResponseHeaders } from 'one'

export async function loader() {
  // cache for 5 minutes at the edge
  await setResponseHeaders((headers) => {
    headers.set('cache-control', 'public, s-maxage=300')
  })
  return { data: '...' }
}
```

Custom headers take precedence — One only sets defaults when no `cache-control` header is present.

---

### Apps Onestack.Dev Data Docs Guides Tamagui (`apps/onestack.dev/data/docs/guides-tamagui.mdx`)

---
title: Tamagui and One
description: Setting up Tamagui on One
---

Tamagui and One go together so well you'd swear they were made by the same person. When targeting both native and web, and wanting to share UI, you don't have many options.

Tamagui does many things that no other universal libraries do: all of its features work with SSR without flashing of styles, it supports spring and CSS animations (also SSR safe), it supports light and dark mode (also SSR safe), it's fully type safe, it supports container queries, and it outputs nearly ideal code for each platform.

<Notice>
  Try concrete Tamagui examples by running `npx one`.
</Notice>

We're going to support SSR-safe light and dark mode with the `@vxrn/color-scheme` helpers that `one` re-exports:

```
npm add tamagui @tamagui/config @tamagui/vite-plugin
```

Be sure to check out the [Dark mode guide](/docs/guides-dark-mode) for more detail there.

Add your `config/tamagui.config.ts`:

```tsx fileName=config/tamagui.config.ts
import { defaultConfig } from '@tamagui/config/v5'
import { animations } from '@tamagui/config/v5-css'
import { createTamagui } from 'tamagui'

export const config = createTamagui({
  ...defaultConfig,
  animations,
})

type Conf = typeof config

declare module 'tamagui' {
  interface TamaguiCustomConfig extends Conf {}
}

export default config
```

The reason we separate this file out is that it lets the Tamagui compiler optimize things. You don't have to do this, but it's pretty easy. The `declare module` block types your tokens and themes across the app.

This preset config we recommend as it lets you just get working on your app, but if you want a custom configuration, the Takeout starter in `npx one` has a [nice custom configuration example](https://github.com/tamagui/takeout2/blob/main/tamagui/tamagui.config.ts).

Next, update your root `_layout.tsx`. `one` re-exports the `@vxrn/color-scheme` helpers, so `SchemeProvider` and `useUserScheme` come straight from `one`:

```tsx fileName=app/_layout.tsx
import type { ReactNode } from 'react'
import { SchemeProvider, Slot, useUserScheme } from 'one'
import { TamaguiProvider } from 'tamagui'
import config from '../config/tamagui.config'

export default function Layout() {
  return (
    <SchemeProvider>
      <ThemeProvider>
        <Slot />
      </ThemeProvider>
    </SchemeProvider>
  )
}

// this is in its own component so it can access the useUserScheme hook
const ThemeProvider = ({ children }: { children: ReactNode }) => {
  const userScheme = useUserScheme()
  return (
    <TamaguiProvider config={config} defaultTheme={userScheme.value}>
      {children}
    </TamaguiProvider>
  )
}
```

Next for the Vite plugin. You don't need to set up the Tamagui Vite plugin, but it adds a lot of nice stuff: debug attributes, broader react-native compatibility, and the optimizing compiler. The compiler runs for every component listed in `components`; pass `disableExtraction: true` to turn it off.

```tsx fileName=vite.config.ts
import { tamaguiPlugin } from '@tamagui/vite-plugin'
import { one } from 'one/vite'
import type { UserConfig } from 'vite'

export default {
  plugins: [
    one(),
    tamaguiPlugin({
      components: ['tamagui'],
      config: './config/tamagui.config.ts',
    }),
  ],
} satisfies UserConfig
```

### Optimizing further

If you want to optimize things a bit further, we can output the Tamagui design system CSS to a stylesheet by changing our plugin configuration:

```tsx fileName=vite.config.ts
import { tamaguiPlugin } from '@tamagui/vite-plugin'
import { one } from 'one/vite'
import type { UserConfig } from 'vite'

export default {
  plugins: [
    one(),
    tamaguiPlugin({
      components: ['tamagui'],
      config: './config/tamagui.config.ts',
      outputCSS: './public/tamagui.css',
    }),
  ],
} satisfies UserConfig
```

And then changing our root layout to add `disableInjectCSS`, and instead importing the CSS at the top:

```tsx fileName=app/_layout.tsx
import '../public/tamagui.css'

import type { ReactNode } from 'react'
import { SchemeProvider, Slot, useUserScheme } from 'one'
import { TamaguiProvider } from 'tamagui'
import config from '../config/tamagui.config'

export default function Layout() {
  return (
    <SchemeProvider>
      <ThemeProvider>
        <Slot />
      </ThemeProvider>
    </SchemeProvider>
  )
}

const ThemeProvider = ({ children }: { children: ReactNode }) => {
  const userScheme = useUserScheme()
  return (
    <TamaguiProvider config={config} defaultTheme={userScheme.value} disableInjectCSS>
      {children}
    </TamaguiProvider>
  )
}
```

That should be about it! You now have SSR safe styling, light and dark mode, and a bunch of advanced styling features.

Check out the [Tamagui site](https://tamagui.dev) for more documentation.

---

### Apps Onestack.Dev Data Docs Guides (`apps/onestack.dev/data/docs/guides.mdx`)

---
title: Guides
---

Guides to common patterns are here. If you'd like to contribute a Guide, please do! Check with us in the [Discord](https://discord.gg/YpUKRqaFtm), then [open a PR](https://github.com/onejs/one).

<CardCol>
  <Card href="/docs/guides-open-graph" title="Dynamic OpenGraph Images" category="SEO" />
  <Card href="/docs/guides-isr" title="Incremental Static Regeneration (ISR)" category="Performance" />
  <Card href="/docs/guides-mdx" title="Setting up MDX for web" category="Content" />
  <Card href="/docs/guides-dark-mode" title="Light and Dark mode" category="UI" />
  <Card href="/docs/guides-tamagui" title="Setting up Tamagui" category="UI" />
  <Card href="/docs/guides-ios-native" title="Build or Run the Native iOS App" category="Native" />
  <Card href="/docs/guides-ota-updates" title="Over-the-air Updates" category="Native" />
  <Card href="/docs/guides-migrating-create-react-app-cra-to-vite-with-one" title="Migrate from Create React App to One" category="Migrating" />
  <Card href="https://github.com/onejs/skills" title="AI Agent Skills" category="AI" />
</CardCol>

---

### Apps Onestack.Dev Data Docs Helpers GetURL (`apps/onestack.dev/data/docs/helpers-getURL.mdx`)

---
title: getURL
---

A function that takes no arguments and returns a string of the current URL of the running application on client or server.

```tsx
import { getURL } from 'one'

const url = getURL()
// Dev: "http://127.0.0.1:8081"
// Prod: Your ONE_SERVER_URL value
```

For example, in dev-mode by default this would be a string of `http://127.0.0.1:8081`. In non-development environments you will need to set `process.env.ONE_SERVER_URL` to your production URL with no trailing `/`.

---

### Apps Onestack.Dev Data Docs Helpers Href (`apps/onestack.dev/data/docs/helpers-href.mdx`)

---
title: href
---

A simple function allows for creating typed route strings. It's a type-level only check, at runtime it only validates that it is a string.

```tsx
import { href } from 'one'

const postPageLink = href(`/post/hello-world`) // will type error if invalid Href
```

This is useful for ensuring your links are valid routes in your application at compile time.

---

### Apps Onestack.Dev Data Docs Helpers IsResponse (`apps/onestack.dev/data/docs/helpers-isResponse.mdx`)

---
title: isResponse
---

One uses `Request`/`Response` type objects for API routes, but for some environments doing an `instanceof Response` can fail.

`isResponse` takes any value and returns true if it is a Response-like object. Useful for API utility functions.

```tsx
import { isResponse } from 'one'

export async function handleResult(result: unknown) {
  if (isResponse(result)) {
    return result
  }
  return Response.json(result)
}
```

---

### Apps Onestack.Dev Data Docs Helpers Redirect (`apps/onestack.dev/data/docs/helpers-redirect.mdx`)

---
title: redirect
---

A function that takes two arguments: a string `path`, and an optional number `status`.

```tsx
import { redirect } from 'one'

export function redirectToLogin() {
  return redirect('/login')
}
```

It accepts relative paths by using the [`getURL`](/docs/helpers-getURL) helper internally.

On the server (including in loaders) it returns a `Response.redirect` object. On the client it calls `router.navigate` and returns void.

## Usage in Loaders

Use `redirect()` in loaders to protect routes. When a user navigates via `<Link>`, the loader runs on the server and the redirect is detected on the client before the protected page ever renders — no flash of unauthorized content, no data leaks.

```tsx fileName=app/dashboard+ssr.tsx
import { redirect, useLoader } from 'one'
import type { LoaderProps } from 'one'

export function loader({ request }: LoaderProps) {
  const session = getSession(request)

  if (!session) {
    throw redirect('/login')
  }

  return { user: session.user }
}

export default function Dashboard() {
  const { user } = useLoader(loader)
  return <Text>Welcome, {user.name}</Text>
}
```

Both `throw redirect()` and `return redirect()` work. `throw` is often cleaner since it stops execution immediately.

This works for both direct page loads (server returns a 302) and client-side `<Link>` navigation (server returns a redirect signal that the client intercepts before rendering).

### How It Works

During client-side navigation, loaders run on the server and the client fetches the result as a JavaScript module. When a loader returns `redirect()`:

1. The server detects the redirect response
2. Instead of returning a raw HTTP 302 (which the browser's module loader would silently follow), the server transforms it into a JS module containing redirect metadata
3. The client detects the redirect signal before navigating or rendering anything
4. The client navigates to the redirect target using `REPLACE` — the protected page never appears

This means **no sensitive data reaches the client** when a redirect occurs. The server only sends back the redirect path.

## Related

- [Loaders](/docs/routing-loader) - Data loading for routes
- [Authentication](/docs/guides-authentication) - Full authentication guide

---

### Apps Onestack.Dev Data Docs Helpers ServerData (`apps/onestack.dev/data/docs/helpers-serverData.mdx`)

---
title: setServerData / getServerData
---

<Status is="developing" />

For improving performance of client hydration on the web, you can pass data from the server to the client. Data must be serializable to JSON.

Here's an example of a simple `useFetch`:

```tsx fileName=app/index.tsx
import { setServerData, getServerData } from 'one'

type SafeURLs = `${`https://tamagui.dev` | `http://localhost`}${string}

const useFetch = async (url: SafeURLs) => {
  if (process.env.VITE_ENVIRONMENT === 'ssr') {
    // on the server, fetch and store during render, then return directly:
    // getServerData throws on the server
    const data = await fetch(url).then(res => res.json())
    setServerData(url, data)
    return data
  }

  // on the client, read the data the server stored
  return getServerData(url)
}

export default async (props) => {
  const serverData = await useFetch(props.url)
  return <div />
}

// can use it in loaders, too

export const loader = async ({ params }) => {
  await doSomething()
  setServerData(params.id, 'data')
  return {}
}
```

`setServerData` only runs on the server and throws elsewhere. `getServerData` only runs on the client and throws on the server. Keys can be any string; override the `One.ClientData` type with `declare module 'one'` to type them.

---

### Apps Onestack.Dev Data Docs Helpers SetResponseHeaders (`apps/onestack.dev/data/docs/helpers-setResponseHeaders.mdx`)

---
title: setResponseHeaders
---

Set HTTP response headers from anywhere during server-side rendering - loaders, components, or middleware. Headers are accumulated and merged onto the final response.

```tsx
import { setResponseHeaders } from 'one'

await setResponseHeaders((headers) => {
  headers.set('X-Custom-Header', 'value')
})
```

The `headers` parameter is a standard [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object.

## Caching and ISR

Use `setResponseHeaders` in loaders to enable CDN caching and [Incremental Static Regeneration](/docs/guides-isr):

```tsx fileName=app/blog/[slug]+ssr.tsx
import { setResponseHeaders, useLoader } from 'one'

export async function loader({ params }) {
  await setResponseHeaders((headers) => {
    headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400')
  })

  const post = await fetchPost(params.slug)
  return { post }
}

export default function BlogPost() {
  const { post } = useLoader(loader)
  return <article>{post.content}</article>
}
```

Common cache header patterns:

- `s-maxage=3600` - CDN caches for 1 hour
- `stale-while-revalidate=86400` - Serve stale content while revalidating in background (up to 1 day)
- `max-age=0, must-revalidate` - Always revalidate with origin
- `private, no-store` - Never cache (for user-specific data)

Note: `stale-while-revalidate` requires CDN support (Vercel, CloudFront, Fastly). Cloudflare does not currently support it.

## Cookies

Set cookies using the standard `Set-Cookie` header:

```tsx fileName=app/auth/login+api.ts
import { setResponseHeaders } from 'one'

export async function POST(request: Request) {
  const { token } = await authenticateUser(request)

  await setResponseHeaders((headers) => {
    headers.append('Set-Cookie', `session=${token}; HttpOnly; Secure; SameSite=Strict; Path=/`)
  })

  return Response.json({ success: true })
}
```

## In Middleware

```tsx fileName=app/_middleware.ts
import { createMiddleware, setResponseHeaders } from 'one'

export default createMiddleware(({ request }) => {
  setResponseHeaders((headers) => {
    headers.set('X-Request-Id', crypto.randomUUID())
  })
})
```

## Important Notes

- Only works server-side (SSR routes and loaders)
- Can be called multiple times - headers are merged
- Must be called during the request lifecycle (not in event handlers or timeouts)

---

### Apps Onestack.Dev Data Docs Helpers Utility Functions (`apps/onestack.dev/data/docs/helpers-utility-functions.mdx`)

---
title: Utility Functions
---

Small helpers exported directly by `one`. Each has its own page:

- [redirect](/docs/helpers-redirect) - navigate on the client, return a redirect response on the server
- [getURL](/docs/helpers-getURL) - the current app URL on client or server
- [isResponse](/docs/helpers-isResponse) - check for a Response-like value
- [href](/docs/helpers-href) - build typed route strings
- [setServerData / getServerData](/docs/helpers-serverData) - pass data from the server to the client
- [setResponseHeaders](/docs/helpers-setResponseHeaders) - set headers during server rendering
- [watchFile](/docs/helpers-watchFile) - register loader file dependencies for hot reload

---

### Apps Onestack.Dev Data Docs Helpers WatchFile (`apps/onestack.dev/data/docs/helpers-watchFile.mdx`)

---
title: watchFile
---

A function that registers a file dependency for hot reload during development. When the file changes, the loader will automatically re-run and refresh data on the client without a full page reload.

```tsx fileName=app/blog/[slug].tsx
import { watchFile } from 'one'
import { readFile } from 'fs/promises'

export async function loader({ params: { slug } }) {
  const filePath = `./content/${slug}.mdx`
  watchFile(filePath)
  const content = await readFile(filePath, 'utf-8')
  return { content }
}
```

- No-op in production and on the client
- Pass the same path you use with `fs.readFile` or similar
- See [Loaders - Hot Reload](/docs/routing-loader#hot-reload-for-file-dependencies) for more details

---

### Apps Onestack.Dev Data Docs Hooks UseActiveParams (`apps/onestack.dev/data/docs/hooks-useActiveParams.mdx`)

---
title: useActiveParams
---

Returns URL parameters globally, updating even when the route using it is not focused.

Unlike [useParams](/docs/hooks-useParams), this hook updates on every navigation regardless of which route is active. Use this for analytics, background operations, or global UI that needs to react to all route changes.

## Basic Usage

```tsx
import { useActiveParams } from 'one'

export default function AnalyticsTracker() {
  const params = useActiveParams()

  useEffect(() => {
    analytics.track('page_view', params)
  }, [params])

  return null
}
```

## When to Use

**Use `useActiveParams` for:**
- Analytics tracking from a root component
- Global breadcrumbs or navigation UI
- Background data sync based on current route
- Debugging route state

**Use `useParams` for:**
- Most component use cases
- Avoiding unnecessary re-renders
- Stack navigators where background screens shouldn't update

## Comparison

```tsx
// Stack: /home -> /users/123 -> /users/123/edit
// Current screen: /users/123/edit

// In /users/123 component (background):
useParams()        // { id: '123' } - no updates
useActiveParams()  // Updates on every navigation

// In /users/123/edit component (focused):
useParams()        // { id: '123' }
useActiveParams()  // { id: '123' }
```

## Example: Global Breadcrumbs

```tsx
import { useActiveParams, usePathname } from 'one'

function Breadcrumbs() {
  const pathname = usePathname()
  const params = useActiveParams<{ id?: string }>()

  // Always shows current route, even from a layout component
  return (
    <nav>
      <span>{pathname}</span>
      {params.id && <span> - ID: {params.id}</span>}
    </nav>
  )
}
```

## Related

- [useParams](/docs/hooks-useParams) - Focus-aware params (recommended for most uses)
- [usePathname](/docs/hooks-usePathname) - Get just the pathname

---

### Apps Onestack.Dev Data Docs Hooks UseBlocker (`apps/onestack.dev/data/docs/hooks-useBlocker.mdx`)

---
title: useBlocker
---

The `useBlocker` hook allows you to block navigation when certain conditions are met, such as when a user has unsaved changes in a form. This provides a way to prompt users before they accidentally leave a page with unsaved work.

## Basic Usage

```tsx
import { useBlocker } from 'one'
import { useState } from 'react'

export default function EditPage() {
  const [isDirty, setIsDirty] = useState(false)
  const blocker = useBlocker(isDirty)

  return (
    <>
      <form onChange={() => setIsDirty(true)}>
        <input type="text" />
        <button type="submit">Save</button>
      </form>

      {blocker.state === 'blocked' && (
        <div className="modal">
          <p>You have unsaved changes. Leave anyway?</p>
          <button onClick={blocker.reset}>Stay</button>
          <button onClick={blocker.proceed}>Leave</button>
        </div>
      )}
    </>
  )
}
```

## Function-Based Blocking

For more control, pass a function that receives location information:

```tsx
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
  // Only block when leaving the /edit section
  return currentLocation.startsWith('/edit') && !nextLocation.startsWith('/edit')
})
```

The function receives:

- `currentLocation` - The current pathname and search params
- `nextLocation` - Where the user is trying to navigate
- `historyAction` - The type of navigation (`'push'`, `'pop'`, or `'replace'`)

## Blocker States

The blocker returns an object with a `state` property:

### `'unblocked'`

Navigation is not blocked. The blocker object has no additional properties.

```tsx
if (blocker.state === 'unblocked') {
  // Normal state, navigation is allowed
}
```

### `'blocked'`

Navigation has been blocked and is waiting for user decision.

```tsx
if (blocker.state === 'blocked') {
  blocker.location  // The destination the user tried to navigate to
  blocker.reset()   // Cancel the navigation, stay on current page
  blocker.proceed() // Allow the navigation to continue
}
```

### `'proceeding'`

The user has chosen to proceed and navigation is in progress.

```tsx
if (blocker.state === 'proceeding') {
  blocker.location  // Where we're navigating to
}
```

## Platform Behavior

### Web

On web, `useBlocker` intercepts:

- **Browser back/forward buttons** - Via popstate events
- **Programmatic navigation** - Via pushState/replaceState interception
- **Page close/refresh** - Via beforeunload (shows browser's default prompt)

### Native

On native platforms, `useBlocker` uses React Navigation's `beforeRemove` event. This blocks:

- **Back navigation** - Hardware back button, swipe gestures, programmatic back
- **Screen removal** - When the screen would be removed from the stack

Note: Native blocking cannot prevent the app from being closed or backgrounded.

## TypeScript Types

```tsx
import type { Blocker, BlockerFunction, BlockerState } from 'one'

// BlockerState is 'unblocked' | 'blocked' | 'proceeding'

// BlockerFunction signature
type BlockerFunction = (args: {
  currentLocation: string
  nextLocation: string
  historyAction: 'push' | 'pop' | 'replace'
}) => boolean
```

## Example: Form with Confirmation Dialog

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Best Practices

1. **Reset dirty state on save** - Clear the blocking condition when the user saves their work
2. **Provide clear messaging** - Tell users what will be lost if they navigate away
3. **Make actions obvious** - Clearly label the "stay" and "leave" options
4. **Test both platforms** - Behavior differs slightly between web and native

---

### Apps Onestack.Dev Data Docs Hooks UseDrawer (`apps/onestack.dev/data/docs/hooks-useDrawer.mdx`)

---
title: useDrawer
---

`useDrawer` returns the full state of the nearest `<Drawer>` navigator, for building a completely custom sidebar.

`<Drawer>` already renders headless on web by default (just the focused screen, no sidebar); reach for `useDrawer` when you want to render the sidebar yourself. See the [headless web navigators guide](/docs/guides-headless-web) for the full picture.

`Drawer` ships from `one/drawer` to keep bundle size down for apps that don't use it. `useDrawer` ships from `one` directly.

## Usage

Place your component as a child of `<Drawer>`. It replaces the headless default on web and is ignored on native, so the same layout file works everywhere:

```tsx
// app/_layout.tsx, works on both platforms
import { Drawer } from 'one/drawer'
import { Link, useDrawer } from 'one'

function WebDrawerLayout() {
  const { screens, focused, isOpen, close, toggle } = useDrawer()

  return (
    <>
      <button onClick={toggle}>Menu</button>

      {isOpen && (
        <nav>
          {screens.map((screen) => (
            <Link key={screen.key} href={screen.href} onPress={close}>
              {screen.options.drawerLabel ?? screen.options.title ?? screen.name}
            </Link>
          ))}
        </nav>
      )}

      {focused.element}
    </>
  )
}

export default function Layout() {
  return (
    <Drawer>
      <Drawer.Screen name="index" options={{ drawerLabel: 'Home' }} />
      <Drawer.Screen name="settings" options={{ drawerLabel: 'Settings' }} />
      <WebDrawerLayout />
    </Drawer>
  )
}
```

## Return value

| Property | Type | Description |
|----------|------|--------------|
| `screens` | `ScreenEntry[]` | Every screen in the drawer. |
| `focused` | `ScreenEntry` | The currently focused screen. |
| `isOpen` | `boolean` | Whether the drawer is currently open. |
| `open()` | `() => void` | Open the drawer. |
| `close()` | `() => void` | Close the drawer. |
| `toggle()` | `() => void` | Toggle the drawer. |

`ScreenEntry` is the same shape [`useStack`](/docs/hooks-useStack#return-value) returns: `key`, `name`, `params`, `href`, `isFocused`, `keepMounted`, `options`, `element`.

## Related

- [Drawer](/docs/components-Drawer)
- [useTabs](/docs/hooks-useTabs)
- [Headless web navigators](/docs/guides-headless-web)

---

### Apps Onestack.Dev Data Docs Hooks UseFocusEffect (`apps/onestack.dev/data/docs/hooks-useFocusEffect.mdx`)

---
title: useFocusEffect
---

Similar to React `useEffect`, but only runs when the current route is focused. The effect runs when the screen comes into focus and the cleanup runs when it goes out of focus.

Unlike [React Navigation's useFocusEffect](https://reactnavigation.org/docs/use-focus-effect/) which requires wrapping your callback in `useCallback`, One's version accepts a dependency array as the second argument directly.

### Usage

```tsx
import { useFocusEffect } from 'one'

function Profile({ userId }) {
  const [user, setUser] = useState(null)

  useFocusEffect(
    () => {
      const unsubscribe = API.subscribe(userId, (user) => setUser(user))
      return () => unsubscribe()
    },
    [userId]
  )

  return <ProfileContent user={user} />
}
```

### Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `effect` | `() => void \| (() => void)` | The effect function to run. Can optionally return a cleanup function. |
| `deps` | `any[]` | Dependency array, similar to `useEffect`. The effect re-runs when dependencies change. |

### Behavior

- **Focus**: The effect runs when the screen becomes focused
- **Blur**: The cleanup function (if returned) runs when the screen loses focus
- **Dependencies**: The effect also re-runs when dependencies change, but only if the screen is focused

---

### Apps Onestack.Dev Data Docs Hooks UseHeaderHeight (`apps/onestack.dev/data/docs/hooks-useHeaderHeight.mdx`)

---
title: useHeaderHeight
---

Returns the height of the navigator header drawn above the current screen, in points, status bar included. Returns `0` where the screen shows no header.

```tsx
import { ScrollView } from 'react-native'
import { useHeaderHeight } from 'one'

export default function FeedScreen() {
  const headerHeight = useHeaderHeight()

  return <ScrollView contentContainerStyle={{ paddingTop: headerHeight }} />
}
```

It reads the same header height React Navigation's own headers provide, without importing `@react-navigation/elements` into your app. Unlike React Navigation's `useHeaderHeight`, it never throws outside a header.

---

### Apps Onestack.Dev Data Docs Hooks UseIsFocused (`apps/onestack.dev/data/docs/hooks-useIsFocused.mdx`)

---
title: useIsFocused
---

Re-export of [useIsFocused from React Navigation](https://reactnavigation.org/docs/use-is-focused/).

Returns a boolean, `true` if the current screen is active.

```tsx
import { useIsFocused } from 'one'

export default function ProfileScreen() {
  const isFocused = useIsFocused()

  return (
    <View>
      <Text>{isFocused ? 'Screen is focused' : 'Screen is not focused'}</Text>
    </View>
  )
}
```

Note: If you need to run side effects when a screen becomes focused rather than just checking focus state, consider using [useFocusEffect](/docs/hooks-useFocusEffect) instead.

---

### Apps Onestack.Dev Data Docs Hooks UseLinkTo (`apps/onestack.dev/data/docs/hooks-useLinkTo.mdx`)

---
title: useLinkTo
---

Used to create props that can be passed to a React Native or web component for creating a custom link.

```tsx
import { useLinkTo } from 'one'
import { Pressable, Text } from 'react-native'

export default function CustomLinkButton() {
  const linkProps = useLinkTo({ href: '/profile' })

  return (
    <Pressable {...linkProps}>
      <Text>Go to Profile</Text>
    </Pressable>
  )
}
```

## Parameters

Accepts a single argument of type:

```tsx
type LinkToProps = {
  href: Href
  replace?: boolean
}
```

Note that `Href` is a string that is typed based on your file system routes.

## Return Value

Returns an object of type:

```tsx
type LinkToResult = {
  href: string
  role: 'link'
  onPress: (event?: React.MouseEvent<HTMLAnchorElement, MouseEvent> | GestureResponderEvent) => void
}
```

---

### Apps Onestack.Dev Data Docs Hooks UseLoader (`apps/onestack.dev/data/docs/hooks-useLoader.mdx`)

---
title: useLoader
---

Returns the data from a route loader. The loader must be defined in the same file as the component using it.

## Basic Usage

```tsx
import { useLoader } from 'one'

export function loader() {
  return { hello: 'world' }
}

export default function Page() {
  const data = useLoader(loader)

  return (
    <>{data.hello}</>
  )
}
```

## Automatic Refetching

Loaders automatically refetch when:
- The pathname changes (e.g., `/home` → `/about`)
- Dynamic params change (e.g., `/user/1` → `/user/2`)
- Search params change (e.g., `/search?q=hello` → `/search?q=world`)

## When to Use

Use `useLoader` when:
- You only need to access the loader data
- Automatic refetching based on route changes is sufficient
- You don't need loading states or manual refetch control

For more advanced use cases like manual refetching, loading states, or accessing refetch from child components, see [useLoaderState](/docs/hooks-useLoaderState).

## TypeScript

The hook is fully typed based on your loader's return type:

```tsx
export function loader() {
  return {
    user: { id: 1, name: 'John' },
    posts: [] as Post[]
  }
}

export default function Page() {
  const data = useLoader(loader)
  // TypeScript knows: data.user.name is string
  // TypeScript knows: data.posts is Post[]
}
```

## Related

- [useLoaderState](/docs/hooks-useLoaderState) - Advanced loader hook with refetch and state
- [useMatches](/docs/hooks-useMatches) - Access loader data from any matched route
- [Loaders](/docs/routing-loader) - Defining route loaders
- [Layout Loaders](/docs/routing-layouts#layout-loaders) - Adding loaders to layouts

---

### Apps Onestack.Dev Data Docs Hooks UseLoaderState (`apps/onestack.dev/data/docs/hooks-useLoaderState.mdx`)

---
title: useLoaderState
---

A hook that provides manual refetch control and loading state for loaders. It extends the functionality of `useLoader` by adding the ability to manually refetch data and track loading states.

## Basic Usage

`useLoaderState` can be used in two different ways:

### With a loader (replaces useLoader)

When you pass a loader function, you get back the data along with refetch and state:

```tsx
import { useLoaderState } from 'one'

export function loader({ path }) {
  const url = new URL(path, 'http://localhost')
  const query = url.searchParams.get('q') || ''

  return fetch(`/api/search?q=${query}`).then(r => r.json())
}

export default function SearchPage() {
  const { data, refetch, state } = useLoaderState(loader)

  return (
    <div>
      <h1>Search Results</h1>
      {state === 'loading' ? (
        <Spinner />
      ) : (
        <Results data={data} />
      )}
      <button onClick={refetch} disabled={state === 'loading'}>
        Refresh Results
      </button>
    </div>
  )
}
```

### Without a loader (access from anywhere)

When called without arguments, you can access the refetch function and state from any component in the tree:

```tsx
import { useLoaderState } from 'one'

function RefreshButton() {
  const { refetch, state } = useLoaderState()

  return (
    <button
      onClick={refetch}
      disabled={state === 'loading'}
    >
      {state === 'loading' ? 'Refreshing...' : 'Refresh Page'}
    </button>
  )
}

// This button can be placed anywhere in your component tree
// and will refetch the current route's loader
```

## Return Values

### When called with a loader

```tsx
const { data, refetch, state } = useLoaderState(loader)
```

- **`data`**: The data returned by the loader (same as `useLoader`)
- **`refetch`**: A function to manually trigger a fresh loader fetch
- **`state`**: Current loading state - either `'idle'` or `'loading'`

### When called without a loader

```tsx
const { refetch, state } = useLoaderState()
```

- **`refetch`**: A function to manually trigger a fresh loader fetch
- **`state`**: Current loading state - either `'idle'` or `'loading'`

## How It Works

All `useLoaderState` hooks on the same route share state through an internal subscription system. This means:

1. When `refetch()` is called from any component, the loader cache for that route is cleared
2. All components subscribed to that route are notified and re-render
3. The loader is executed again with fresh data
4. All components receive the updated data simultaneously

This architecture enables powerful patterns like having a refresh button in your header that can refetch data for the current page, without needing to pass props down through your component tree.

## Common Use Cases

### Pull-to-Refresh

```tsx
function PullToRefreshWrapper({ children }) {
  const { refetch, state } = useLoaderState()

  return (
    <PullToRefresh onRefresh={refetch} refreshing={state === 'loading'}>
      {children}
    </PullToRefresh>
  )
}
```

### Polling for Live Data

```tsx
function useLivePolling(intervalMs = 5000) {
  const { refetch, state } = useLoaderState()

  useEffect(() => {
    const interval = setInterval(() => {
      // Only refetch if not already loading
      if (state === 'idle') {
        refetch()
      }
    }, intervalMs)

    return () => clearInterval(interval)
  }, [refetch, state, intervalMs])
}

export default function LiveDashboard() {
  const { data } = useLoaderState(loader)
  useLivePolling(3000) // Poll every 3 seconds

  return <Dashboard data={data} />
}
```

### Error Recovery

```tsx
export default function DataPage() {
  const { data, refetch, state } = useLoaderState(loader)

  if (data?.error) {
    return (
      <ErrorBoundary
        error={data.error}
        onRetry={refetch}
        retrying={state === 'loading'}
      />
    )
  }

  return <PageContent data={data} />
}
```

### Form Revalidation

```tsx
function CommentForm({ postId }) {
  const { refetch } = useLoaderState()

  const handleSubmit = async (formData) => {
    await submitComment(postId, formData)
    // Refetch the loader to get updated comments
    refetch()
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
    </form>
  )
}
```

## Comparison with useLoader

| Feature | useLoader | useLoaderState |
|---------|-----------|----------------|
| Returns loader data | ✅ | ✅ |
| Manual refetch | ❌ | ✅ |
| Loading state | ❌ | ✅ |
| Can be called without loader | ❌ | ✅ |
| Can be used in child components | ❌ | ✅ |

Use `useLoader` when you only need the data and automatic refetching is sufficient.

Use `useLoaderState` when you need:
- Manual refresh capabilities
- Loading indicators
- Error retry mechanisms
- To trigger refetches from child components

## TypeScript

The hook is fully typed based on your loader's return type:

```tsx
export function loader() {
  return {
    users: [] as User[],
    total: 0
  }
}

// TypeScript knows the shape of data
const { data, refetch, state } = useLoaderState(loader)
//     ^? { users: User[], total: number }

// Without a loader, data is not available
const { refetch, state } = useLoaderState()
//     ^? { refetch: () => void, state: 'idle' | 'loading' }
// Note: no 'data' property when called without loader
```

## Migration from useLoader

Migrating from `useLoader` to `useLoaderState` is straightforward:

```tsx
// Before
import { useLoader } from 'one'

export default function Page() {
  const data = useLoader(loader)
  return <div>{data.content}</div>
}

// After
import { useLoaderState } from 'one'

export default function Page() {
  const { data, refetch, state } = useLoaderState(loader)
  return (
    <div>
      {state === 'loading' && <LoadingSpinner />}
      {data.content}
      <button onClick={refetch}>Refresh</button>
    </div>
  )
}
```

## Working with Different Rendering Modes

`useLoaderState` works consistently across all rendering modes:

- **SPA (`+spa`)**: Loaders execute on the server for each request, refetch triggers client-side fetching
- **SSR (`+ssr`)**: Initial load on server, refetch executes on client
- **SSG (`+ssg`)**: Build-time data as initial state, refetch provides fresh client data

The refetch functionality always executes on the client, ensuring consistent behavior regardless of the initial rendering mode.

## Loader Timing History

For debugging and performance monitoring, you can access the timing history of loader executions:

```tsx
import { getLoaderTimingHistory } from 'one'

function DebugPanel() {
  const timings = getLoaderTimingHistory()
  
  return (
    <div>
      {timings.map((entry) => (
        <div key={`${entry.path}:${entry.startTime}`}>
          <strong>{entry.path}</strong>: {entry.totalTime}ms
          {entry.error && <span> (Error: {entry.error})</span>}
        </div>
      ))}
    </div>
  )
}
```

The `LoaderTimingEntry` type contains:

```ts
type LoaderTimingEntry = {
  path: string                      // Route path (e.g., '/users/123')
  startTime: number                 // Timestamp when loader started
  moduleLoadTime?: number           // Time to load the loader module
  executionTime?: number            // Time to execute the loader
  totalTime?: number                // Total time from start to finish
  error?: string                    // Error message if loader failed
  source: 'preload' | 'initial' | 'refetch'  // What triggered the load
}
```

This function is useful for:
- Performance monitoring in development
- Debugging slow loaders
- Building loader timing visualizations in devtools

The history is only populated in development mode and stores the last 50 entries to avoid memory growth.

## Related

- [useLoader](/docs/hooks-useLoader) - Basic loader data access
- [useMatches](/docs/hooks-useMatches) - Access loader data from any matched route
- [Loaders](/docs/routing-loader) - Defining route loaders
- [Layout Loaders](/docs/routing-layouts#layout-loaders) - Adding loaders to layouts

---

### Apps Onestack.Dev Data Docs Hooks UseMatches (`apps/onestack.dev/data/docs/hooks-useMatches.mdx`)

---
title: useMatches
---

Returns an array of all matched routes from the root layout down to the current page. Each match includes the route's loader data, params, and route ID.

This is useful for:
- Building breadcrumbs from parent routes
- Accessing layout loader data from any component
- Building table of contents from page data in a layout

## Basic Usage

`loaderData` is typed `unknown`, since a match can come from any route. Give it the type of the loader that produced it:

```tsx
import { useMatches } from 'one'

type PageData = { title?: string }

function Breadcrumbs() {
  const matches = useMatches()

  return (
    <nav>
      {matches.map((match) => (
        <a key={match.routeId} href={match.pathname}>
          {(match.loaderData as PageData | undefined)?.title ?? match.routeId}
        </a>
      ))}
    </nav>
  )
}
```

## Accessing Layout Data from a Page

A common use case is accessing a layout's loader data from a page component:

```tsx
import { useMatches } from 'one'
import type { DocsLayoutData } from './_layout'

// in a page component
function DocsPage() {
  const matches = useMatches()

  // find the docs layout match
  const docsLayout = matches.find((m) => m.routeId.includes('docs/_layout'))
  const navItems = (docsLayout?.loaderData as DocsLayoutData | undefined)?.navItems

  return (
    <div>
      <Sidebar items={navItems} />
      {/* page content */}
    </div>
  )
}
```

## Accessing Page Data from a Layout

Layouts can access their child page's loader data to render things like a table of contents:

```tsx fileName=app/docs/_layout.tsx
import { Slot, useMatches } from 'one'

export async function loader() {
  return {
    navItems: await fetchDocsNav(),
  }
}

export type DocsLayoutData = Awaited<ReturnType<typeof loader>>

type DocsPageData = { headings: { id: string; title: string }[] }

export default function DocsLayout() {
  const matches = useMatches()

  // the last match is the current page
  const pageMatch = matches[matches.length - 1]
  const headings = (pageMatch?.loaderData as DocsPageData | undefined)?.headings

  return (
    <div>
      <Sidebar />
      <main>
        <Slot />
      </main>
      <TableOfContents headings={headings} />
    </div>
  )
}
```

## RouteMatch Type

Each match in the array has this shape:

```ts
type RouteMatch = {
  routeId: string // e.g., './docs/_layout.tsx'
  pathname: string // e.g., '/docs/intro'
  params: Record<string, string | string[]> // URL params like { slug: 'intro' }
  loaderData: unknown // data from the route's loader
}
```

## Related Hooks

### useMatch

Find a specific route's match by its route ID:

```tsx
import { useMatch } from 'one'
import type { DocsLayoutData } from './docs/_layout'

function Component() {
  // find a specific layout's data
  const docsMatch = useMatch('./docs/_layout.tsx')
  const navItems = (docsMatch?.loaderData as DocsLayoutData | undefined)?.navItems
}
```

Returns `undefined` if the route is not in the current match list.

### usePageMatch

Get the current page's match (the last/deepest match):

```tsx
import { Head, usePageMatch } from 'one'

type PageMeta = { title?: string; description?: string }

function Layout() {
  const pageMatch = usePageMatch()
  const { title, description } = (pageMatch?.loaderData as PageMeta | undefined) ?? {}

  return (
    <Head>
      <title>{title}</title>
      <meta name="description" content={description} />
    </Head>
  )
}
```

## Server vs Client Behavior

- **Server (SSR/SSG)**: Returns matches computed during the request with fresh loader data
- **Client (hydration)**: Returns the same matches from the server for consistent hydration
- **Client (navigation)**: Returns updated matches - page loader data is fresh, layout loader data is cached from the initial load

## Related

- [Layout Loaders](/docs/routing-layouts#layout-loaders) - Adding loaders to layout files
- [useLoader](/docs/hooks-useLoader) - Basic loader data access
- [useLoaderState](/docs/hooks-useLoaderState) - Advanced loader with refetch

---

### Apps Onestack.Dev Data Docs Hooks UseNavigation (`apps/onestack.dev/data/docs/hooks-useNavigation.mdx`)

---
title: useNavigation
---

Returns the React Navigation object for the current route. This gives you low-level access to the underlying navigation state and methods.

> **Tip**: Prefer [`useRouter`](/docs/hooks-useRouter) for most navigation needs. It provides a simpler, strongly-typed API that's designed specifically for One. Use `useNavigation` only when you need React Navigation-specific features.

## Usage

```tsx
import { useNavigation } from 'one'

function MyComponent() {
  const navigation = useNavigation()

  // Access navigation state
  const isFocused = navigation.isFocused()

  // Add event listeners
  useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      console.log('Screen focused')
    })
    return unsubscribe
  }, [navigation])

  return <View />
}
```

## Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `parent` | `string` | Optional. Path to a parent navigator to get its navigation object |

### Parent Path

You can access a parent navigator's navigation object by providing a path:

```tsx
import { useNavigation } from 'one'

function NestedScreen() {
  // Get the root navigator's navigation object
  const rootNavigation = useNavigation('/(root)')

  // Or use relative paths
  const parentNavigation = useNavigation('../')
  const grandparentNavigation = useNavigation('../../')

  return <View />
}
```

## Return Value

Returns the React Navigation [`NavigationProp`](https://reactnavigation.org/docs/navigation-prop). Key methods include:

| Method | Description |
|--------|-------------|
| `navigate(name, params)` | Navigate to a screen |
| `goBack()` | Go back to the previous screen |
| `isFocused()` | Check if the screen is focused |
| `addListener(event, callback)` | Subscribe to navigation events |
| `setOptions(options)` | Update screen options |
| `getParent(id)` | Get a parent navigator |
| `getState()` | Get the navigation state |
| `dispatch(action)` | Dispatch a navigation action |

## Examples

### Screen Focus Events

Run code when a screen gains or loses focus:

```tsx
import { useNavigation } from 'one'
import { useEffect } from 'react'

function ProfileScreen() {
  const navigation = useNavigation()

  useEffect(() => {
    const unsubscribeFocus = navigation.addListener('focus', () => {
      // Refresh data when screen comes into focus
      fetchUserData()
    })

    const unsubscribeBlur = navigation.addListener('blur', () => {
      // Cleanup when leaving screen
      cancelPendingRequests()
    })

    return () => {
      unsubscribeFocus()
      unsubscribeBlur()
    }
  }, [navigation])

  return <View />
}
```

### Dynamic Screen Options

Update screen options at runtime:

```tsx
import { useNavigation } from 'one'
import { useEffect } from 'react'

function ChatScreen({ chatName }) {
  const navigation = useNavigation()

  useEffect(() => {
    navigation.setOptions({
      title: chatName,
      headerRight: () => <SettingsButton />,
    })
  }, [navigation, chatName])

  return <View />
}
```

### Before Remove Event

Prevent navigation (e.g., for unsaved changes):

```tsx
import { useNavigation } from 'one'
import { useEffect, useState } from 'react'
import { Alert } from 'react-native'

function EditorScreen() {
  const navigation = useNavigation()
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)

  useEffect(() => {
    const unsubscribe = navigation.addListener('beforeRemove', (e) => {
      if (!hasUnsavedChanges) return

      // Prevent default behavior of leaving the screen
      e.preventDefault()

      Alert.alert(
        'Discard changes?',
        'You have unsaved changes. Are you sure you want to leave?',
        [
          { text: 'Cancel', style: 'cancel' },
          {
            text: 'Discard',
            style: 'destructive',
            onPress: () => navigation.dispatch(e.data.action),
          },
        ]
      )
    })

    return unsubscribe
  }, [navigation, hasUnsavedChanges])

  return <View />
}
```

### Accessing Parent Navigators

Access methods from parent navigators in nested screens:

```tsx fileName=app/(tabs)/home/details.tsx
import { useNavigation } from 'one'

function DetailsScreen() {
  // Get the tabs navigator to switch tabs programmatically
  const tabsNavigation = useNavigation('/(tabs)')

  const goToProfile = () => {
    // Jump to the profile tab
    tabsNavigation.navigate('profile')
  }

  return (
    <Button onPress={goToProfile}>
      Go to Profile Tab
    </Button>
  )
}
```

### Navigation State

Access the current navigation state:

```tsx
import { useNavigation } from 'one'

function DebugComponent() {
  const navigation = useNavigation()
  const state = navigation.getState()

  console.log('Current route index:', state.index)
  console.log('Route history:', state.routes.map(r => r.name))

  return <View />
}
```

## Navigation Events

Available events you can listen to:

| Event | Description |
|-------|-------------|
| `focus` | Screen is focused |
| `blur` | Screen loses focus |
| `beforeRemove` | Screen is about to be removed (can be prevented) |
| `state` | Navigation state changes |

## When to Use useNavigation vs useRouter

| Use Case | Recommended Hook |
|----------|-----------------|
| Navigate to a route | `useRouter` |
| Go back | `useRouter` |
| Replace current route | `useRouter` |
| Listen to focus/blur events | `useNavigation` |
| Set screen options dynamically | `useNavigation` |
| Prevent navigation (beforeRemove) | `useNavigation` |
| Access navigation state | `useNavigation` |
| Access parent navigator | `useNavigation` |

## TypeScript

Type the navigation object for better autocomplete:

```tsx
import { useNavigation } from 'one'
import type { NavigationProp } from '@react-navigation/native'

type RootStackParamList = {
  Home: undefined
  Profile: { userId: string }
  Settings: undefined
}

function MyComponent() {
  const navigation = useNavigation<NavigationProp<RootStackParamList>>()

  // Now fully typed
  navigation.navigate('Profile', { userId: '123' })
}
```

## Related Hooks

### useRootNavigationState

Returns the root navigation state object from the NavigationContainer. This gives you access to the top-level navigation tree.

```tsx
import { useRootNavigationState } from 'one'

function Component() {
  const rootState = useRootNavigationState()
  
  // Access the current route index and history
  console.log('Current index:', rootState.index)
  console.log('Routes:', rootState.routes.map(r => r.name))
}
```

Use this when you need to access the root-level navigation state directly. For most use cases, `useNavigation()` is preferable.

## Related Pages

- [useRouter](/docs/hooks-useRouter) - Recommended hook for navigation
- [useBlocker](/docs/hooks-useBlocker) - Block navigation with a simpler API
- [React Navigation docs](https://reactnavigation.org/docs/navigation-prop) - Full navigation prop reference

---

### Apps Onestack.Dev Data Docs Hooks UseParams (`apps/onestack.dev/data/docs/hooks-useParams.mdx`)

---
title: useParams
---

Returns URL parameters for the focused route, including dynamic segments and query string values.

On native, this hook reads the mounted screen's own route parameters, including inherited layout parameters. Retained screens keep their own dynamic values when another screen becomes active. On web, dynamic parameters follow the current URL. Use `useActiveParams` to read the active route globally.

## Basic Usage

```tsx
import { useParams } from 'one'

// Route: app/posts/[id].tsx
// URL: /posts/123?sort=date

export default function PostPage() {
  const { id, sort } = useParams()
  // id = '123', sort = 'date'

  return <Text>Post {id}, sorted by {sort}</Text>
}
```

## TypeScript

### Using `createRoute` (Recommended)

The best way to get fully typed params is with the `createRoute` helper:

```tsx
import { createRoute } from 'one'

const route = createRoute<'/posts/[id]'>()

export default function PostPage() {
  const { id } = route.useParams()
  // id is automatically typed as string!

  return <Text>Post {id}</Text>
}
```

Enable auto-generation in your vite config to have this inserted automatically:

```tsx
// vite.config.ts
one({
  router: {
    experimental: {
      typedRoutesGeneration: 'runtime', // auto-inserts createRoute
    },
  },
})
```

See [Typed Routes](/docs/routing-typed-routes) for full setup details.

### Manual Typing

You can also type params manually:

```tsx
export default function PostPage() {
  const { id, sort } = useParams<{
    id: string
    sort?: 'date' | 'title'
  }>()

  return <Text>Post {id}</Text>
}
```

## Catch-All Routes

For catch-all routes like `[...slug].tsx`, params are returned as arrays:

```tsx
// Route: app/docs/[...slug].tsx
// URL: /docs/api/hooks/useParams

export default function DocsPage() {
  const { slug } = useParams<{ slug: string[] }>()
  // slug = ['api', 'hooks', 'useParams']

  return <Text>{slug.join(' / ')}</Text>
}
```

## URL Decoding

Parameters are automatically URL-decoded:

```tsx
// URL: /search?q=hello%20world
const { q } = useParams()
// q = 'hello world' (decoded)
```

## useParams vs useActiveParams

| Hook | Updates when... | Best for |
|------|----------------|----------|
| `useParams` | Route is focused | Most components |
| `useActiveParams` | Any route changes | Analytics, global UI |

In a stack navigator, if you push a new screen, `useParams` in the previous screen won't update. Use [useActiveParams](/docs/hooks-useActiveParams) if you need global parameter updates.

## Related

- [useActiveParams](/docs/hooks-useActiveParams) - Global params that update even when unfocused
- [usePathname](/docs/hooks-usePathname) - Get just the pathname
- [useSegments](/docs/hooks-useSegments) - Get route segments as array

---

### Apps Onestack.Dev Data Docs Hooks UsePathname (`apps/onestack.dev/data/docs/hooks-usePathname.mdx`)

---
title: usePathname
---

Returns the current pathname without query parameters or hash.

```tsx
import { usePathname } from 'one'

export default function CurrentPath() {
  const pathname = usePathname()
  // URL: /blog/hello-world?ref=twitter#comments
  // Returns: "/blog/hello-world"

  return <Text>Current path: {pathname}</Text>
}
```

## Common Uses

### Active Link Styling

```tsx
import { usePathname, Link } from 'one'

function NavLink({ href, children }) {
  const pathname = usePathname()
  const isActive = pathname === href

  return (
    <Link
      href={href}
      style={{ fontWeight: isActive ? 'bold' : 'normal' }}
    >
      {children}
    </Link>
  )
}
```

### Conditional Rendering

```tsx
function Layout({ children }) {
  const pathname = usePathname()
  const showSidebar = !pathname.startsWith('/auth')

  return (
    <div>
      {showSidebar && <Sidebar />}
      {children}
    </div>
  )
}
```

### Analytics

```tsx
function PageTracker() {
  const pathname = usePathname()

  useEffect(() => {
    analytics.trackPageView(pathname)
  }, [pathname])

  return null
}
```

## What's Not Included

The pathname does **not** include:
- Query parameters (`?foo=bar`)
- Hash fragments (`#section`)

For URL parameters, use [useParams](/docs/hooks-useParams) or [useActiveParams](/docs/hooks-useActiveParams).

## Related

- [useParams](/docs/hooks-useParams) - Get URL parameters
- [useSegments](/docs/hooks-useSegments) - Get pathname as segments array
- [useActiveParams](/docs/hooks-useActiveParams) - Global URL parameters

---

### Apps Onestack.Dev Data Docs Hooks UseRouter (`apps/onestack.dev/data/docs/hooks-useRouter.mdx`)

---
title: useRouter
---

Returns a static object used for routing. This hook will never update, and is only used for imperatively controlling the router.

It takes no arguments and returns object of type:

```tsx
type Router = {
  /** Go back in the history. */
  back: () => void
  /** If there's history that supports invoking the `back` function. */
  canGoBack: () => boolean
  /** Navigate to the provided href using a push operation if possible. */
  push: (href: Href, options?: LinkToOptions) => void
  /** Navigate to the provided href. */
  navigate: (href: Href, options?: LinkToOptions) => void
  /** Navigate to route without appending to the history. */
  replace: (href: Href, options?: LinkToOptions) => void
  /** Navigate to the provided href using a push operation if possible. */
  dismiss: (count?: number) => void
  /** Navigate to first screen within the lowest stack. */
  dismissAll: () => void
  /** If there's history that supports invoking the `dismiss` and `dismissAll` function. */
  canDismiss: () => boolean
  /** Update the current route query params. */
  setParams: <T = ''>(
    params?: T extends '' ? Record<string, string | undefined | null> : InputRouteParams<T>
  ) => void
  /** Subscribe to state updates from the router */
  subscribe: (listener: RootStateListener) => () => void
  /** Subscribe to loading state updates */
  onLoadState: (listener: LoadingStateListener) => () => void
}

type LinkToOptions = {
  /** Disable scroll restoration on navigation */
  scroll?: boolean
  /** Display a different URL in the browser (web only) */
  mask?: {
    href: Href
  }
}
```

## Route Masking

You can display a different URL in the browser address bar while navigating to a specific route.
This is useful for modals, side panels, or keeping URLs clean:

```tsx
const router = useRouter()

// Navigate to /photos/5/modal but show /photos/5 in the URL bar
router.push('/photos/5/modal', {
  mask: { href: '/photos/5' }
})
```

When users click back/forward, the actual route state is restored from history.
This feature is web-only.

## Related Hooks

### useNavigationContainerRef

Returns a ref to the NavigationContainer component. This allows direct access to the React Navigation container.

```tsx
import { useNavigationContainerRef } from 'one'

function Component() {
  const navigationContainerRef = useNavigationContainerRef()
  
  // The ref.current is null until the NavigationContainer mounts
  if (navigationContainerRef.current) {
    // Access React Navigation API
    const state = navigationContainerRef.current.getRootState()
  }
}
```

This is primarily useful for advanced React Navigation integration. Most use cases should use `useRouter()` instead.

### useUnstableGlobalHref

Returns the current global pathname with query parameters attached. This is an internal API and may change.

```tsx
import { useUnstableGlobalHref } from 'one'

function Component() {
  const href = useUnstableGlobalHref()
  // e.g., '/docs/routing?tab=settings'
}
```

<Notice type="warning">
This hook is marked unstable and subject to change. Use `usePathname()` and `useSearchParams()` for stable alternatives.
</Notice>

---

### Apps Onestack.Dev Data Docs Hooks UseScrollGroup (`apps/onestack.dev/data/docs/hooks-useScrollGroup.mdx`)

---
title: useScrollGroup
---

Register the current route as a scroll group. Child routes will preserve scroll position when navigating between them.

This is useful for layouts where you want to maintain scroll position while users navigate between child routes, such as:
- Tab layouts where switching tabs shouldn't reset the parent scroll
- Dashboard layouts with multiple sections
- Any nested layout that wants independent scroll restoration

## Usage

Use this hook in a layout component to create a scroll group for all its children:

```tsx
// app/dashboard/_layout.tsx
import { useScrollGroup, Slot } from 'one'

export default function DashboardLayout() {
  // All routes under /dashboard will share scroll position
  useScrollGroup()

  return (
    <div>
      <nav>Dashboard Navigation</nav>
      <Slot />
    </div>
  )
}
```

When users navigate between `/dashboard/overview`, `/dashboard/settings`, and `/dashboard/reports`, the scroll position of the dashboard layout will be preserved.

## Custom Group Path

By default, `useScrollGroup` uses the current pathname. You can specify a custom group path:

```tsx
// Create a scroll group for a specific path prefix
useScrollGroup('/products')
```

## Programmatic Scroll Groups

You can also specify a scroll group when navigating programmatically:

```tsx
import { useRouter } from 'one'

function MyComponent() {
  const router = useRouter()

  const goToSettings = () => {
    router.navigate('/dashboard/settings', {
      scrollGroup: '/dashboard'
    })
  }

  return <button onClick={goToSettings}>Go to Settings</button>
}
```

## How It Works

1. When a layout calls `useScrollGroup()`, it registers the current path as a scroll group
2. When navigating, if both the previous and new routes are within the same scroll group, the scroll position is preserved
3. If navigating to a route outside the scroll group, scroll resets to the top

## Platform Support

This hook only affects web scroll behavior. On native platforms, it's a no-op since React Navigation handles scroll restoration differently.

## Type

```tsx
function useScrollGroup(groupPath?: string): void
```

### Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `groupPath` | `string` (optional) | Custom path to use as the scroll group. Defaults to current pathname. |

---

### Apps Onestack.Dev Data Docs Hooks UseSearchParams (`apps/onestack.dev/data/docs/hooks-useSearchParams.mdx`)

---
title: useSearchParams
---

Returns URL search parameters as a read-only `URLSearchParams` object. This provides the standard web API for working with query strings.

## Basic Usage

```tsx
import { useSearchParams } from 'one'

// URL: /products?sort=price&category=electronics

export default function Products() {
  const searchParams = useSearchParams()

  const sort = searchParams.get('sort')           // 'price'
  const category = searchParams.get('category')   // 'electronics'
  const missing = searchParams.get('missing')     // null

  return <Text>Sorted by {sort}</Text>
}
```

## URLSearchParams Methods

The returned object supports all standard `URLSearchParams` read methods:

```tsx
const searchParams = useSearchParams()

// Get a single value
searchParams.get('key')        // string | null

// Check if param exists
searchParams.has('key')        // boolean

// Get all values for a key (for repeated params)
searchParams.getAll('tag')     // string[]

// Iterate over all entries
for (const [key, value] of searchParams) {
  console.log(key, value)
}

// Convert to string
searchParams.toString()        // 'key=value&other=123'
```

## Repeated Parameters

Use `getAll()` when a parameter appears multiple times:

```tsx
// URL: /filter?tag=react&tag=typescript&tag=native

const searchParams = useSearchParams()
const tags = searchParams.getAll('tag')
// tags = ['react', 'typescript', 'native']
```

## Global Option

By default, `useSearchParams` only updates when the current route is focused. Pass `{ global: true }` to update on any route change:

```tsx
// Updates even when this screen is not focused
const searchParams = useSearchParams({ global: true })
```

<Notice>
The `global` option cannot change between renders.
</Notice>

## Read-Only

The returned `URLSearchParams` is read-only. Calling `set()`, `append()`, or `delete()` will throw an error:

```tsx
const searchParams = useSearchParams()

// ❌ These will throw
searchParams.set('key', 'value')
searchParams.append('key', 'value')
searchParams.delete('key')
```

To update search params, use the router:

```tsx
import { useRouter, useSearchParams } from 'one'

function Filters() {
  const router = useRouter()
  const searchParams = useSearchParams()

  const updateSort = (sort: string) => {
    // Build new search string
    const params = new URLSearchParams(searchParams)
    params.set('sort', sort)

    router.push(`/products?${params.toString()}`)
  }
}
```

## useSearchParams vs useParams

| Hook | Returns | Use for |
|------|---------|---------|
| `useParams` | Plain object | Quick access to params as object |
| `useSearchParams` | URLSearchParams | Standard web API, repeated params |

Both hooks include path params and query params. Choose based on which API you prefer:

```tsx
// Object style
const { sort, page } = useParams()

// URLSearchParams style
const searchParams = useSearchParams()
const sort = searchParams.get('sort')
const page = searchParams.get('page')
```

## Expo Router Compatibility

For easier migration from Expo Router, the following aliases are available (but deprecated):

- `useGlobalSearchParams` - Alias to `useActiveParams()`. Returns params that update even when the route is unfocused.
- `useLocalSearchParams` - Alias to `useParams()`. Returns focus-aware params.

Prefer the non-aliased names in new code.

## Related

- [useParams](/docs/hooks-useParams) - Get params as a plain object
- [useActiveParams](/docs/hooks-useActiveParams) - Global params that update even when unfocused
- [usePathname](/docs/hooks-usePathname) - Get just the pathname

---

### Apps Onestack.Dev Data Docs Hooks UseSegments (`apps/onestack.dev/data/docs/hooks-useSegments.mdx`)

---
title: useSegments
---

Returns the current route as an array of path segments. Segments match the file path structure, not the resolved URL values.

```tsx
import { useSegments } from 'one'

// File: app/users/[id]/settings.tsx
// URL: /users/123/settings

export default function SettingsPage() {
  const segments = useSegments()
  // Returns: ['users', '[id]', 'settings']
  // Note: Returns '[id]' not '123'

  return <Text>Route: /{segments.join('/')}</Text>
}
```

## When to Use

Use `useSegments` when you need to know which route file is active, rather than the resolved URL:

- **Route matching** - Check which section of the app is active
- **Navigation guards** - Protect routes based on segments
- **Breadcrumbs** - Build navigation from route structure

For resolved URL values, use [usePathname](/docs/hooks-usePathname) or [useParams](/docs/hooks-useParams).

## TypeScript

You can strictly type segments for better type safety:

```tsx
// Given file structure:
// app/
//   settings.tsx
//   [user]/
//     index.tsx
//     followers.tsx

type AppSegments =
  | ['settings']
  | ['[user]']
  | ['[user]', 'followers']

function RouteHandler() {
  const segments = useSegments<AppSegments>()

  if (segments[0] === 'settings') {
    return <SettingsPage />
  }

  if (segments[0] === '[user]') {
    if (segments[1] === 'followers') {
      return <FollowersPage />
    }
    return <UserPage />
  }
}
```

## Examples

### Auth Guard

```tsx
function AuthGuard({ children }) {
  const segments = useSegments()
  const { user } = useAuth()

  const inAuthGroup = segments[0] === '(auth)'

  useEffect(() => {
    if (!user && !inAuthGroup) {
      router.replace('/login')
    } else if (user && inAuthGroup) {
      router.replace('/')
    }
  }, [user, inAuthGroup])

  return children
}
```

### Tab Highlighting

```tsx
function TabBar() {
  const segments = useSegments()
  const activeTab = segments[0]

  return (
    <div>
      <Tab active={activeTab === 'home'} href="/home">Home</Tab>
      <Tab active={activeTab === 'search'} href="/search">Search</Tab>
      <Tab active={activeTab === 'profile'} href="/profile">Profile</Tab>
    </div>
  )
}
```

## Segments vs Pathname

| Hook | Returns | Use Case |
|------|---------|----------|
| `useSegments()` | `['users', '[id]']` | Route matching, guards |
| `usePathname()` | `'/users/123'` | Display, analytics |

## Related

- [usePathname](/docs/hooks-usePathname) - Resolved pathname string
- [useParams](/docs/hooks-useParams) - Dynamic segment values

---

### Apps Onestack.Dev Data Docs Hooks UseServerHeadInsertion (`apps/onestack.dev/data/docs/hooks-useServerHeadInsertion.mdx`)

---
title: useServerHeadInsertion
---

Register elements to be inserted into the document `<head>` during server rendering. This hook is server-only and is used for adding custom head content from anywhere in your component tree.

## Usage

```tsx
import { useServerHeadInsertion } from 'one'

function CustomHeadContent() {
  useServerHeadInsertion(() => (
    <>
      <meta name="custom-tag" content="my-value" />
      <link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" />
    </>
  ))

  return <div>Page content</div>
}
```

## When to Use

Use `useServerHeadInsertion` when you need to:
- Add head elements from a nested component (not just from the page root)
- Conditionally insert head elements based on component state
- Keep head content close to the component that needs it

For static head content or when you have a clear layout, prefer the `<Head>` component.

## Server-Only

This hook only runs on the server during SSR/SSG. On the client, it has no effect. One calls every registered callback after the page finishes rendering and adds the markup to `<head>`, so a callback can return what the tree collected during render, such as CSS-in-JS styles.

```tsx
function MyComponent() {
  useServerHeadInsertion(() => (
    <title>Dynamic Title</title>
  ))

  // This hook runs on server, but not on client
  return <div>Content</div>
}
```

## Callback Function

The callback receives no parameters and should return a React element or fragment containing the head elements:

```tsx
useServerHeadInsertion(() => (
  <>
    <title>Page Title</title>
    <meta name="description" content="Page description" />
    <meta property="og:title" content="My Page" />
  </>
))
```

## Type

The hook accepts a callback that returns a `ServerHeadInsertionCallback`:

```ts
type ServerHeadInsertionCallback = () => React.ReactElement
```

## Related

- [`<Head />`](/docs/components-Head) - Component for inserting head elements
- [Dynamic OpenGraph Images](/docs/guides-open-graph) - Generate dynamic OG images

---

### Apps Onestack.Dev Data Docs Hooks UseSitemap (`apps/onestack.dev/data/docs/hooks-useSitemap.mdx`)

---
title: useSitemap
---

Returns the full route tree of your app as a structured object. Useful for debug screens, route picker UIs, and generating link previews.

```tsx
import { useSitemap } from 'one'

export default function Sitemap() {
  const sitemap = useSitemap()
  if (!sitemap) return null

  return <SitemapNode node={sitemap} />
}

function SitemapNode({ node }) {
  return (
    <View>
      <Text>{node.filename} → {node.href}</Text>
      {node.children.map((child) => (
        <SitemapNode key={child.contextKey} node={child} />
      ))}
    </View>
  )
}
```

## Return Value

`useSitemap()` returns a recursive `SitemapType` rooted at the top-level layout, or `null` if the route tree hasn't been initialized yet (briefly, on first render).

```ts
type SitemapType = {
  contextKey: string      // module key for the route file (e.g. './(app)/_layout.tsx')
  filename: string        // human-readable filename (e.g. '(app)/_layout')
  href: string            // route pattern (e.g. '/(app)/thread/[id]')
  isInitial: boolean      // true if this is the initial route of its layout
  isInternal: boolean     // true for One-internal routes
  isGenerated: boolean    // true for in-memory routes (not user files)
  children: SitemapType[] // nested routes
}
```

`href` is the **route pattern** with dynamic segments left as `[id]` / `[...slug]`, not a concrete URL. Fill in your own params if you want clickable links:

```tsx
const docsHref = node.href.replace('[...slug]', 'getting-started/install')
```

## Filtering Internal/Generated Routes

User-defined routes have `isInternal === false` and `isGenerated === false`. Skip the rest in your UI to avoid showing One's internals:

```tsx
function visibleChildren(node: SitemapType) {
  return node.children.filter((child) => !child.isInternal && !child.isGenerated)
}
```

## Related

- [Routing Overview](/docs/routing)
- [Deep Linking](/docs/guides-deep-linking)
- [Typed Routes](/docs/routing-typed-routes)

---

### Apps Onestack.Dev Data Docs Hooks UseStack (`apps/onestack.dev/data/docs/hooks-useStack.mdx`)

---
title: useStack
---

`useStack` returns the full state of the nearest `<Stack>` navigator on web, for building a completely custom stack layout - your own header, transitions, back button, anything.

`<Stack>` already renders headless on web by default (just the focused screen); reach for `useStack` when you want to render something yourself instead. See the [headless web navigators guide](/docs/guides-headless-web) for the full picture.

## Usage

Place your component as a child of `<Stack>`. It replaces the headless default on web and is ignored on native, so the same layout file works everywhere:

```tsx
// app/_layout.tsx, works on both platforms
import { Stack, useRouter, useStack } from 'one'

function WebStackLayout() {
  const { screens, focused } = useStack()
  const router = useRouter()

  return (
    <>
      {screens.length > 1 && <button onClick={router.back}>← Back</button>}
      <h1>{focused.options.title}</h1>
      {focused.element}
    </>
  )
}

export default function Layout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: 'Feed' }} />
      <Stack.Screen name="[id]" options={{ title: 'Post' }} />
      <WebStackLayout />
    </Stack>
  )
}
```

## Return value

| Property | Type | Description |
|----------|------|--------------|
| `screens` | `ScreenEntry[]` | Every screen currently in the stack, in stack order. |
| `focused` | `ScreenEntry` | The currently focused screen. |

`useStack` is a state reader. To navigate, use [`useRouter`](/docs/hooks-useRouter) / `router` and [`Link`](/docs/components-Link) as you would anywhere else in the app - every entry carries an `href`.

### `ScreenEntry`

| Property | Type | Description |
|----------|------|--------------|
| `key` | `string` | Unique per-instance key. |
| `name` | `string` | The route name, matching the file name in `app/`. |
| `params` | `object` | The route's params. |
| `href` | `string` | The resolved path for this screen. |
| `isFocused` | `boolean` | Whether this is the focused screen. |
| `keepMounted` | `boolean` | Whether `options.keepMounted` was set. |
| `options` | `object` | The compiled options for this screen (`title`, `presentation`, and anything else set via `Stack.Screen`). |
| `element` | `ReactElement` | The screen's rendered content. Rendering it mounts the route. |

## Presentation screens

Screens with a `presentation` option (`sheet`, `modal`, `formSheet`, ...) still show up in `screens` alongside regular pushed screens - `useStack` doesn't separate them out. If your custom layout needs to treat them differently, check `focused.options.presentation`. Most apps handle presentation screens with [Presentations](/docs/components-Presentations) instead, which is scoped specifically to them.

## Related

- [Stack](/docs/components-Stack)
- [Presentations](/docs/components-Presentations)
- [Headless web navigators](/docs/guides-headless-web)

---

### Apps Onestack.Dev Data Docs Hooks UseTabs (`apps/onestack.dev/data/docs/hooks-useTabs.mdx`)

---
title: useTabs
---

`useTabs` returns the full state of the nearest `<Tabs>` navigator, for building a completely custom tab bar.

`<Tabs>` already renders headless on web by default (just the focused tab, no bar); reach for `useTabs` when you want to render the tab bar yourself. See the [headless web navigators guide](/docs/guides-headless-web) for the full picture.

## Usage

Place your component as a child of `<Tabs>`. It replaces the headless default on web and is ignored on native, so the same layout file works everywhere:

```tsx
// app/(tabs)/_layout.tsx, works on both platforms
import { Link, Tabs, useTabs } from 'one'

function WebTabBar() {
  const { screens, focused } = useTabs()

  return (
    <>
      <nav>
        {screens.map((tab) => (
          <Link
            key={tab.name}
            href={tab.href}
            className={tab.isFocused ? 'active' : undefined}
          >
            {tab.options.title}
          </Link>
        ))}
      </nav>
      {focused.element}
    </>
  )
}

export default function Layout() {
  return (
    <Tabs>
      <Tabs.Screen name="home" options={{ title: 'Home' }} />
      <Tabs.Screen name="feed" options={{ title: 'Feed' }} />
      <WebTabBar />
    </Tabs>
  )
}
```

## Return value

| Property | Type | Description |
|----------|------|--------------|
| `screens` | `ScreenEntry[]` | Every tab, in the order they were declared. |
| `focused` | `ScreenEntry` | The currently focused tab. |

`useTabs` is a state reader. To switch tabs, link to the tab's `href` with [`Link`](/docs/components-Link) or `router.navigate(href)`.

`ScreenEntry` is the same shape [`useStack`](/docs/hooks-useStack#return-value) returns: `key`, `name`, `params`, `href`, `isFocused`, `keepMounted`, `options`, `element`.

## Keeping inactive tabs mounted

Set `keepMounted` on a screen and the default headless view keeps it alive after you switch away, hidden via React 19.2's `<Activity>`, so scroll position and form state survive. A tab is only kept once it has actually been focused, matching react-navigation's lazy behavior on native.

```tsx
<Tabs.Screen name="feed" options={{ title: 'Feed', keepMounted: true }} />
```

A custom layout renders whatever it wants, so it owns this itself. `keepMounted` is on every entry if you want to honor it the same way.

## Related

- [Tabs](/docs/components-Tabs)
- [useDrawer](/docs/hooks-useDrawer)
- [Headless web navigators](/docs/guides-headless-web)

---

### Apps Onestack.Dev Data Docs Installation (`apps/onestack.dev/data/docs/installation.mdx`)

---
title: Installation
description: A quick tutorial from start to finish
---

Getting started with One is as easy as:

```bash
npx one
```

The CLI will guide you through the process of bootstrapping one of our starter templates. See the [CLI reference](/docs/cli) for every command.

If this is your first One app, we recommend taking this route, as you can choose between bare-bones example all the way up to a full stack with a user system and authentication. We will be launching more starters as the project matures.

<Notice>
  **Using an AI agent?** Install the official One skills so Claude Code, Cursor, Codex, etc. know how to build with One:

  ```bash
  npx skills add onejs/skills
  ```

  Full list and per-agent install instructions: [github.com/onejs/skills](https://github.com/onejs/skills).
</Notice>

### 5-minute tutorial

While we recommend using a preset to get started, it's helpful to walk through creating a new One app from scratch.

For those of you who prefer to learn by doing, we've put together a 5-minute tutorial that will have you building a basic app in no time.

Let's begin with our package.json:

```json fileName=package.json
{
  "name": "one-app",
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "one dev",
    "build": "one build",
    "serve": "one serve",
    "prebuild:native": "one prebuild",
    "ios": "one run:ios",
    "android": "one run:android"
  },
  "dependencies": {
    "one": "latest",
    "react": "19.3.0",
    "react-native": "0.88.0",
    "react-native-web": "^0.21.2",
    "@react-navigation/native": "8.0.0-alpha.51",
    "react-native-screens": "~4.30.0",
    "react-native-reanimated": "~4.7.1",
    "react-native-worklets": "~0.13.0"
  },
  "devDependencies": {
    "@react-native-community/template": "^0.88.0",
    "vite": "^8"
  }
}
```

It is strongly recommended to pin the `one` version for production stability:

```bash
# npm
npm install one@latest --save-exact

# yarn
yarn add one@latest --exact

# pnpm
pnpm add one@latest --save-exact

# bun
bun add one@latest --exact
```

This ensures consistent behavior across different environments and deployments.

If using pnpm or a package manager that symlinks dependencies, you'll want to disable that setting.
One has (just a few) patches that fix dependencies. This is because the React Native ecosystem has many weirdly published packages (think JSX or Flowtype published in `.js` files, or non-standard legacy babel experimental transforms, etc). We are working with package authors to improve this, but we've found patches to be more reliable, simpler and less mysterious than other solutions.

<Notice>
  **Platform Support**: One currently doesn't support Windows or Bun with the `--bun`
  runtime for development. We're happy to accept contributions that add support for these
  platforms. For now, please use macOS or Linux with Node.js or other supported package
  managers.
</Notice>

### Vite Config

Next, let's add our ultra-simple `vite.config.ts`:

```tsx fileName=vite.config.ts
import type { UserConfig } from 'vite'
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      native: {
        app: {
          name: 'OneApp',
          ios: { bundleId: 'com.example.oneapp' },
          android: { applicationId: 'com.example.oneapp' },
        },
      },
    }),
  ],
} satisfies UserConfig
```

### TSConfig

One will create a TSConfig for you when you first start the dev server, with some defaults that work well. Of course you can create your own if you'd prefer. The default looks like this:

```json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "~/*": ["./*"]
    },
    "strict": true,
    "rootDir": ".",
    "module": "Preserve",
    "moduleResolution": "Bundler",
    "preserveSymlinks": true,
    "skipLibCheck": true,
    "jsx": "react-jsx",
    "noImplicitAny": false,
    "types": ["node", "react", "vite/client"],
    "lib": ["dom", "esnext"]
  },
  "exclude": ["node_modules", "**/test", "**/dist", "**/types", "**/__tests__"]
}
```

For TypeScript types on One's [preset environment variables](/docs/environment#preset-environment-variables), add `/// <reference types="one/env" />` to any `.d.ts` file in your project.

### File System Routes

One expects your file system routes to be placed in an `app` directory.

`app/_layout.tsx` serves as the top-level wrapper for all routes, ideal for:

1. Consistent page structure
2. Global styles or themes
3. Persistent navigation
4. App-wide state management

One uses the `<Slot />` component to indicate where child components should render, allowing for nested layouts and fine-grained control over your UI structure.

Let's create a root layout `app/_layout.tsx`:

```tsx fileName=app/_layout.tsx
import { Slot } from 'one'

export default function Layout() {
  return <Slot />
}
```

And then create a simple index page `app/index.tsx`:

```tsx fileName=app/index.tsx
import { View, Text } from 'react-native'

export default function MyApp() {
  return (
    <View
      style={{
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        minHeight: '100%',
      }}
    >
      <Text>Hello from One</Text>
    </View>
  )
}
```

You can now install your node modules and run your app. 🎉

```bash
bun install && bun run dev
```

### Running your app

If the port isn't taken, you should be able to load [http://localhost:8081](http://localhost:8081) and to see your website (use `--port` to change the port).

For native, generate the React Native projects once and launch the platform you want:

```bash
bun run prebuild:native
bun run ios
# or
bun run android
```

One's default native path uses its Vite and Rolldown pipeline and does not require Expo or
a Babel config.

Note that for web-only projects you don't need `react-native`, and further note that if you want something that has a much broader styling featureset and much lighter bundle-size on web, you can use [Tamagui](https://tamagui.dev), which has no dependency on React Native.

---

### Apps Onestack.Dev Data Docs Introduction (`apps/onestack.dev/data/docs/introduction.mdx`)

---
title: Introduction
description: One is a React framework that aims to make full stack development as simple as possible - whether you're making a website, an app, or sharing code between the two.
---

One lets you target both [React Native](https://reactnative.dev) and web with a single codebase, replacing both Metro and a separate web framework with a single framework, with shared file-system routing. One runs on Vite, which brings [many benefits](/docs/faq#why-vite).

For native builds, One uses Rolldown, the same bundler as web.

<CardCol>
  <Card href="/docs/installation" title="Get started" />
  <Card href="/docs/features" title="Features" />
  <Card href="/docs/status" title="Status" />
</CardCol>

## Highlights

- Target native and web at once with typed [file system routes](/docs/routing).
- [Loaders](/docs/routing-loader) ala Remix that tree-shake away on the client.
- [Render modes](/docs/render-modes) on a per-page basis (SSG, SSR, SPA, and API).
- Preloads all JS for initial load and when hovering links, without need for a route map.
- Includes a [production server](/docs/one-serve) out of the box.

## Use Cases

One works well as:

- A uniquely good way to ship cross-platform apps with shared code.
- A light, fast and opinionated framework for web-only or native-only.

For a few more answers to common questions, [see our FAQ](/docs/faq).

## Our Full-Stack Philosophy

One lets you customize your stack as you please but recommends Zero as the first-class solution to data. We think the client-side "sync engine" model it uses is a clear win-win for simpler code and better UX.

Today, we have a starter with `npx one` that sets up an early alpha of Zero, while we work on a more official solution that neatly integrates One and Zero out of the box.

## Acknowledgements

One started as an experiment in porting Expo Router to our [vxrn library](https://vxrn.dev). We since changed much of the internals and added render modes and a variety of features. We&nbsp;owe the Expo team a huge thanks for their pioneering efforts.

One was built thanks to some amazing open source technologies and contributors:

- [React Navigation](https://reactnavigation.org) provides the base routing library.
- [Expo Router](https://docs.expo.dev/router/introduction) for large parts of the initial One router.
- [Software Mansion](https://swmansion.com) for React Native Screens and React Native Gesture Handler.
- [Remix](https://remix.run) for inspiration for the `loader` APIs.
- [Vite](https://vitejs.dev), of course.

And a special thanks to the following individuals:

- [Matias Capeletto](https://github.com/patak-dev) for inviting us to speak at ViteConf, giving the initial spark.
- [Fatih Aygün](https://github.com/cyco130) for freely helping at length, getting us past many hurdles.
- [Hiroshi Ogawa](https://github.com/hi-ogawa) for his expert consulting for which One wouldn't exist without.
- Dan Maier, for graciously gifting us the `one` npm package.

---

### Apps Onestack.Dev Data Docs Metro Mode (`apps/onestack.dev/data/docs/metro-mode.mdx`)

---
title: Metro Mode
description: Opting into Metro for native builds
---

## Overview

One uses Rolldown for native builds by default. Metro is available when you select it explicitly.

<Notice theme="blue">
  New apps should stay on One's default bundler. Metro mode exists for projects that still depend on Metro-specific resolver or transformer settings while they move over.
</Notice>


## Enabling Metro Mode

Configure the native bundler to Metro in your `vite.config.ts`:

Metro is not part of One's default install. Add One's Metro plugin and the Expo Babel preset to the app first:

```sh
bun add -d @vxrn/vite-plugin-metro babel-preset-expo
```

One reports an installation error naming the missing package when Metro starts without them. The default Rolldown path loads neither.

```tsx fileName=vite.config.ts
import type { UserConfig } from 'vite'
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      native: {
        // Enable Metro bundler for native builds
        bundler: 'metro',
      },
    }),
  ],
} satisfies UserConfig
```


## Limitations

When using Metro mode, only some of the Vite features are supported. One provides only a subset of Vite functionality in Metro mode to maintain compatibility and stability.

<Notice theme="blue">
  Metro mode intentionally supports only a subset of Vite configurations and features. This approach helps avoid implementation differences between Vite and Metro that could cause compatibility issues with React Native packages.
  <br />
  We are actively working on expanding Vite feature support, but for any features we don't yet provide through Vite configuration, you can always use the standard React Native way to change the configuration using `metro.config.js`.
</Notice>

Only the following Vite features and configurations are supported in Metro mode for now:

- [`import.meta.env` Built-in Constants](https://vite.dev/guide/env-and-mode.html#built-in-constants) <Status is="mostly-stable" text="Partially Supported" />: supported but may not work as expected for web specific variables.
- [`import.meta.env` Env Variables](https://vite.dev/guide/env-and-mode.html#env-variables) <Status is="stable" text="Supported" />
- `tsconfig-paths` <Status is="mostly-stable" text="Partially Supported" />: As one automatically configures [`vite-tsconfig-paths`](https://www.npmjs.com/package/vite-tsconfig-paths) to support path mappings defined in `tsconfig.json` for your project, the same applies to Metro mode. However, if you are using the `vite-tsconfig-paths` plugin manually, the custom options you provide there will not be applied for Metro.

## Configuration

By default, One starts from React Native's Metro configuration and applies the same One router, environment, and server transforms used by the native Rolldown path.

<Notice theme="green">
  We recommend using the standard Vite way for configuring if possible, as it provides a more consistent experience across web and native builds, and will be more future-proof to switch between bundlers.
</Notice>

Like ordinary React Native apps, you can still configure Metro using `metro.config.js` for more advanced settings, such as resolver or watcher options:

```js fileName=metro.config.js
const { withOne } = require('one/metro-config')

module.exports = withOne(__dirname)
```

`metro.config.js` works exactly as it would in a standard React Native project, giving you full control over the bundling process.

### No Babel

By default, One uses its own Metro worker built around Oxc. The worker combines Oxc's Rust parser and transforms with TypeScript implementations of One's native transforms, Hermes parsing for Flow, and esbuild for syntax lowering. This pipeline runs without Babel.

A Babel plugin One has no port for would silently do nothing, so instead the build fails and names it. You have three options:

- Port it and list it in `native.bundlerOptions.nativeTransformModules`. Each entry is a module id (Metro runs transforms in separate worker processes, so it has to be serializable) that default-exports `(code, ctx) => string | null`. Your transforms run first, on the original source, with an absolute `ctx.filename`.
- Remove it from `native.bundlerOptions.babelConfigOverrides`.
- Set `native.bundlerOptions.nativeTransforms: false` to use Babel, or `ONE_METRO_NATIVE_TRANSFORMS=0` for a single run. Install `@react-native/babel-preset` and the Babel packages One's preset uses (`bun add -d @react-native/babel-preset @babel/core @babel/generator @babel/parser @babel/plugin-transform-export-namespace-from @babel/traverse @babel/types babel-dead-code-elimination babel-plugin-module-resolver`), then run `one metro-eject` to create a Babel config that delegates to `one/babel-preset`, which builds on React Native's preset. Expo presets are optional app configuration and are never loaded by One.

A native transform receives source text and returns updated source text. It can use Oxc or another tool suited to the transformation:

```js fileName=scripts/my-transform.cjs
module.exports = function myTransform(code, ctx) {
  // ctx: { filename, platform, dev, projectRoot }
  if (!code.includes('__BUILD_ID')) return null
  return code.replace(/__BUILD_ID/g, JSON.stringify(process.env.BUILD_ID))
}
```

```tsx fileName=vite.config.ts
one({
  native: {
    bundler: 'metro',
    bundlerOptions: {
      nativeTransformModules: ['./scripts/my-transform.cjs'],
    },
  },
})
```

Return `null` to leave the file alone. Transforms run in order, before One's built-in transforms. The first receives the original file contents; each subsequent transform receives the previous transform's output. A transform that records original source positions should run first.

## Lazy Startup

By default, Metro starts as soon as the Vite dev server is ready. If you're primarily doing web development but have Metro configured, you can defer Metro startup until a native client actually connects:

```tsx fileName=vite.config.ts
import type { UserConfig } from 'vite'
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      native: {
        bundler: 'metro',
        bundlerOptions: {
          startup: 'lazy',
        },
      },
    }),
  ],
} satisfies UserConfig
```

Or via environment variable:

```sh
ONE_METRO_LAZY=1 one dev
```

With lazy startup:
- Metro won't start when you run `one dev`
- When a simulator or device connects and checks `/status`, it gets an immediate response while Metro starts in the background
- The first bundle request waits for Metro to finish initializing

This can significantly speed up dev server startup when you don't always need native builds.

## Performance

Metro can use more mature React Native tooling. One's default Rolldown path remains the recommended choice for new apps. Both native bundlers use Oxc by default; Metro can opt back into Babel.

---

### Apps Onestack.Dev Data Docs Native Apis (`apps/onestack.dev/data/docs/native-apis.mdx`)

---
title: Native Platform APIs
description: Find the shipped shared and iOS service APIs, permissions, and lifecycle contracts
---

Service namespaces live on `One`, including services with platform-specific
operations. The camera view and widget layout helpers below use `One.iOS`.
Hooks are named imports from `one`. Use each reference for the complete
signature, configuration, permission behavior, event cleanup, and errors.

## Shared services

| API | What you can do |
| --- | --- |
| [One.Notifications](/native/notifications) | Request permission, schedule and dismiss notifications, handle responses, obtain push tokens, and configure Android channels |
| [One.Clipboard](/native/clipboard) | Read, write, and check clipboard text |
| [One.Network, useNetworkState](/native/network) | Read and subscribe to connection state |
| [One.Browser](/native/browser) | Open a system browser or authentication session and dismiss it |
| [One.Auth.Apple](/native/auth) | Start Apple authorization on iOS |
| [One.Haptics](/native/haptics) | Produce selection, impact, and notification feedback |
| [One.AppInfo](/native/app-info) | Read the installed version, build, and application identifier |
| [One.ImagePicker](/native/image-picker) | Pick library media or use the system camera picker |
| [One.DocumentPicker](/native/document-picker) | Select documents with cancellation and file metadata |
| [One.SecureStore](/native/secure-store) | Store strings in Keychain or encrypted Android storage, asynchronously or synchronously |
| [One.Database](/native/database) | Open and query SQLite databases through OP SQLite |
| [One.LaunchScreen](/native/launch-screen) | Hold the launch screen until the app is ready, then hide it |
| [One.Updates](/native/updates) | Check, download, stage, and reload OTA bundles; the launcher handles early-failure rollback and cache pruning |
| [One.openURL, One.openShare, One.openSettings](/native/open-url) | Hand a URL, share payload, or settings request to the system |
| [One.Speech](/native/speech) | Request permission and start or stop speech recognition |
| [global crypto](/native/crypto) | Generate secure random values and UUIDs |
| `global fetch` | Stream native HTTP responses and upload React Native file parts |

`One.Auth.Apple` is grouped under the shared authentication namespace, but Apple
authorization is an iOS operation. On Android and web, `isAvailable` is false and authorization calls reject.

Secure storage uses string keys and values:

```ts
import { One } from 'one'

export async function saveSession(token: string) {
  await One.SecureStore.setItem('session', token)
}

export async function readSession() {
  return One.SecureStore.getItem('session')
}

export async function clearSession() {
  await One.SecureStore.deleteItem('session')
}
```

A missing key reads `null`. SecureStore rejects on web; choose a separate web
session strategy. The synchronous verbs block the JavaScript thread, so reserve
them for a value needed synchronously, such as an initial session read.

## More native services

These service namespaces are exported on `One`. Their references describe
supported platforms, native configuration and unavailable operations.
`One.iOS.CameraView` and `One.iOS.WidgetUI` are iOS components.

| API | What you can do |
| --- | --- |
| [One.LocalAuthentication](/native/local-authentication) | Check biometric support and request system authentication |
| [One.ProtectedStore](/native/protected-store) | Protect Keychain items with biometric or device-passcode access policies |
| [One.Storage](/native/storage) | Fast synchronous key-value storage |
| [One.Location](/native/location) | Request permission, locate, watch position, and geocode; background updates need configuration |
| [One.MapServices](/native/map#ios-search-and-directions) | Search places, request suggestions, reverse-geocode, and obtain directions |
| [One.FileSystem](/native/file-system) | Read and write sandbox files, inspect directories, and manage files |
| [One.Audio](/native/audio) | Play and record audio, configure background playback, and handle interruptions and Now Playing |
| [One.iOS.CameraView](/native/camera) | Render a camera preview and receive scanned codes |
| [One.Share](/native/share) | Present the iOS share sheet and receive its result |
| [One.PhotoLibrary](/native/photo-library) | Save, query, export, and manage Photos assets and albums |
| [One.ImageManipulator](/native/image-manipulator) | Crop, resize, rotate, and encode local images |
| [One.Contacts](/native/contacts) | Request access, search, create, update, delete, and use the system contact picker |
| [One.Calendar](/native/calendar) | Manage events and reminders, including supported recurrence rules |
| [One.Device](/native/device) | Read device and localization snapshots |
| [One.Motion](/native/motion) | Subscribe to available motion sensors |
| [One.ScreenOrientation](/native/screen-orientation) | Read, lock, unlock, and observe the active scene orientation |
| [One.ScreenCapture](/native/screen-capture) | Observe capture state and screenshot events; capture a window or mounted view as PNG |
| [One.Purchases](/native/purchases) | Read StoreKit products, purchase, restore, and observe transactions |
| [One.BackgroundTasks](/native/background-tasks) | Register and schedule system-managed background work |
| [One.DeviceAttestation](/native/device-attestation) | Use App Attest and DeviceCheck with app-owned server validation |
| [One.AppTracking](/native/app-tracking) | Read and request tracking permission |
| [One.AppIcon](/native/app-icon) | Read and change configured alternate icons |
| [One.KeepAwake](/native/keep-awake) | Control the app's idle-timer flag |
| [One.Print](/native/print) | Present the system print sheet for a local PDF |
| [One.StoreReview](/native/store-review) | Request a system review prompt; iOS decides whether to display it |
| [One.QuickActions](/native/quick-actions) | Manage Home Screen actions and receive warm or cold selections |
| [One.Widgets, One.LiveActivities, One.iOS.WidgetUI](/native/ios-widgets) | Update a generated WidgetKit extension and ActivityKit presentations |

Widget and Live Activity APIs need a generated extension, App Group, and the
relevant signing capabilities. [Native project setup](/docs/guides-ios-native#ios-widgets-and-live-activities)
shows the configuration. Widgets render a supported serialized JSX layout,
not an arbitrary React Native subtree.

## Permissions and subscriptions

Configure native usage descriptions and capabilities before building the app,
then request runtime permission from the corresponding API. A camera picker,
camera preview, library picker, and Photos library management have different
permission contracts. The system picker can select media without granting the
app general library access.

A live network subscription returns an object with `remove()`:

```tsx
import { useEffect, useState } from 'react'
import { One } from 'one'

export function useConnectionType() {
  const [type, setType] = useState('unknown')

  useEffect(() => {
    const subscription = One.Network.addStateListener((state) => {
      setType(state.type)
    })
    return () => subscription.remove()
  }, [])

  return type
}
```

For a component that also needs the initial snapshot, use the exported
`useNetworkState` hook. Other services can return an unsubscribe function or
another handle; follow their own cleanup signature rather than assuming every
service uses `remove()`.

## HTTP

Native global `fetch` streams response bodies and accepts React Native file
parts in `FormData`. Use its standard request and response objects by calling
the global `fetch` function directly. Streaming is separate from
`One.Network`, which reports connection state.

---

### Apps Onestack.Dev Data Docs Native Components (`apps/onestack.dev/data/docs/native-components.mdx`)

---
title: Native Components
description: Choose SwiftUI, Compose, or shared native components and understand their layout boundaries
---

Use platform components when the screen should follow that platform's controls
and layout. Use `One.UI` when its shared contract fits the feature. These
components coexist with React Native views; their native layout systems own
the content inside their hosts.

## iOS components

The following families are available through `One.iOS`. Each reference describes
its props, events, child slots, and version limits.

| Family | Components and reference |
| --- | --- |
| Layout | [Host, HStack, VStack, ZStack, Spacer, LabeledContent, grids, and lazy stacks](/native/ios-stacks) |
| Groups | [Group, ControlGroup, DisclosureGroup, GroupBox, ViewThatFits, and GlassEffectContainer](/native/ios-groups) |
| Lists | [List, ScrollView, Form, Section, Overlay, ViewSlot, and SwipeActions](/native/ios-lists) |
| Actions | [Button, Toggle, Slider, Stepper, PasteButton, ShareLink, SignInWithAppleButton, EditButton, RenameButton, LocationButton, and ShortcutsLink](/native/ios-buttons-toggles) |
| Text | [TextField, SecureField, and TextEditor](/native/ios-text-fields) |
| Selection | [Picker, DatePicker, and ColorPicker](/native/ios-pickers) |
| Presentation | [Sheet, FullScreenCover, Popover, Alert, and ConfirmationDialog](/native/ios-presentations) |
| TabView | [TabView, Tab, TabSection, TabViewSlot, TabViewBottomAccessory, Pager, and Page](/native/ios-tabs-pager) |
| Navigation | [NavigationStack, Toolbar, ToolbarItem, ToolbarItemGroup, and ToolbarSpacer](/native/ios-navigation) |
| Indicators | [ProgressView and Gauge](/native/ios-progress-gauges) |
| Content | [Text, Label, Image, AsyncImage, VideoPlayer, LivePhotoView, PhotosPicker, WebView, Map, QuickLook, FileImporter, and ContentUnavailableView](/native/ios-media) |
| Charts | [Chart, drawn with Swift Charts](/native/ios-charts) |
| Store | [ProductView, StoreView, and SubscriptionStoreView](/native/ios-store) |
| Shapes | [Circle, Capsule, Rectangle, RoundedRectangle, Ellipse, UnevenRoundedRectangle, and ConcentricRectangle](/native/ios-media#shapes) |

`style` controls the host's placement in the surrounding React Native layout.
`swiftStyle` applies SwiftUI appearance and modifiers. Native containers can
host React Native content through slots. Use a finite viewport for a scrolling
or filling subtree so that the native container and Yoga have a defined size.
The [layout reference](/native/ios-stacks) explains the boundary options.

Plain controlled values work with React state:

```tsx
import { useState } from 'react'
import { One } from 'one'

export function Volume() {
  const [volume, setVolume] = useState(0.5)

  return (
    <One.iOS.Slider
      label="Volume"
      value={volume}
      onValueChange={setVolume}
      in={[0, 1]}
    />
  )
}
```

Keep this component in an iOS branch or an `.ios.tsx` file. Accept a native
edit by updating the controlled value in its callback. For controls whose
value props accept a native state handle, you can use `useNativeState` instead;
see [TextInput and native state](/native/text-input).

`One.iOS.withAnimation` is SwiftUI's `withAnimation`. The native state values
its body writes reach their bound views in one SwiftUI transaction, so they
animate together:

```tsx
const level = useNativeState(0)

<One.iOS.Slider label="Level" value={level} onValueChange={() => {}} />

One.iOS.withAnimation('spring', () => level.set(100), () => console.log('done'))

One.iOS.withAnimation({ kind: 'easeOut', duration: 2 }, () => level.set(0))
One.iOS.withAnimation(
  { kind: 'spring', response: 0.35, dampingFraction: 0.8 },
  () => level.set(100)
)
```

The animation accepts SwiftUI's named animations (`default`, `spring`,
`smooth`, `snappy`, `bouncy`, `easeInOut`, `linear`, and the rest), a record
with a factory `kind` and its parameters, or `null` to apply the writes without
animation. Durations and spring response times are in seconds. The same
animation values work in `swiftStyle.animation` and the animation callback of
`One.iOS.PhaseAnimator`. The body must write `useNativeState` handles; ordinary
React state updates do not join the native animation transaction.

The optional completion runs when the
animation is logically complete, or when it is removed if the fourth argument
is `'removed'`. A body that throws applies its writes without animation and
skips the completion. A call inside another body throws.

`One.iOS.TabView` and `NavigationStack` are explicit native composition APIs.
Use the router's [Tabs](/docs/components-Tabs) and
[Stack](/docs/components-Stack) to navigate file routes. Routed stacks also
have a [header and toolbar API](/docs/components-Stack).
The UIKit-backed [toolbar](/native/toolbar),
[split view](/native/split-view), and
[zoom transitions](/native/zoom-transitions) have separate contracts.

## Android components

`One.Android` exposes Compose and Material components:

| Family | Components |
| --- | --- |
| Layout | Column, Row, FlowRow, Box, Spacer, LazyColumn, LazyRow |
| Surfaces | Card, ElevatedCard, OutlinedCard, Surface, HorizontalDivider, VerticalDivider |
| Content | Text, Icon, Image, Badge, BadgedBox, ListItem |
| Actions | Button, IconButton, FilledIconButton, FilledTonalIconButton, OutlinedIconButton; FloatingActionButton, SmallFloatingActionButton, LargeFloatingActionButton, ExtendedFloatingActionButton |
| Selection | Switch, Checkbox, RadioButton; ToggleButton, IconToggleButton, FilledIconToggleButton, OutlinedIconToggleButton; SingleChoiceSegmentedButtonRow, MultiChoiceSegmentedButtonRow, SegmentedButton |
| Chips | AssistChip, FilterChip, InputChip, SuggestionChip |
| Input and presentation | TextField, Slider, DatePicker, TimePicker, DatePickerDialog, TimePickerDialog, AlertDialog, Dialog, Snackbar, SnackbarHost, ModalBottomSheet |
| Indicators | ProgressIndicator, LinearProgressIndicator, CircularProgressIndicator, LinearWavyProgressIndicator, CircularWavyProgressIndicator, LoadingIndicator, ContainedLoadingIndicator |

The [Compose reference](/native/android-compose) supplies the prop and slot
contracts. [Android menus](/native/android-menu),
[icons](/native/android-icons), and [colors](/native/android-color) have their
own pages.

A Compose root uses React Native `style`; its descendants use `composeStyle`.
Supply a finite width, and either a height or a surrounding layout that bounds
the root. Nested rows and columns use Compose's alignment, arrangement, and
weight rules rather than Yoga flex props.

```tsx
import { useState } from 'react'
import { One } from 'one'

export function AndroidSettings() {
  const [enabled, setEnabled] = useState(true)

  return (
    <One.Android.Column
      style={{ width: '100%', height: 180 }}
      spacing={12}
      composeStyle={{ padding: 16 }}
    >
      <One.Android.Text text="Notifications" fontSize={20} />
      <One.Android.Switch
        label="Allow notifications"
        isOn={enabled}
        onIsOnChange={setEnabled}
      />
    </One.Android.Column>
  )
}
```

## Shared components

| API | Contract |
| --- | --- |
| `One.UI.Image` | [Image loading, sizing, and load events](/native/image) |
| `One.UI.Icon` | [Per-platform elements and semantic color roles](/native/icons) |
| `One.UI.Map` | [MapKit on iOS and Google Maps on Android](/native/map) |
| `One.UI.Pager` | [Swipeable pages, scroll progress, and ref methods](/native/ui-pager) |
| `One.UI.Portal`, `PortalHost` | [Content rendered in a named host elsewhere](/native/portal) |
| `One.UI.TextInput`, `useNativeState` | [Native text input and shared observable state](/native/text-input) |
| `One.UI.EdgeFade`, `Blur`, `Mask` | [Native effects and web behavior](/native/effects) |
| `One.UI.PictureInPicture` | [Native picture-in-picture content and inline web content](/native/picture-in-picture) |
| `One.UI.SafeArea` | [Provider, View, and initial metrics](/native/safe-area) |
| `One.UI.Fonts`, `useFonts` | [Bundled and loaded fonts](/native/fonts) |
| `One.UI.ReservedRegions` | [View-scoped reserved areas, size class, hinges, and window segments](/native/adaptive-layout) |

The One router installs its root safe-area provider. Use an additional provider
only when a subtree needs its own coordinate space. Shared does not imply
identical web behavior: Map renders no web view, Blur and Mask require native,
and PictureInPicture renders inline on web. Consult the
[platform notes](/docs/native-platforms) before selecting a shared component.

---

### Apps Onestack.Dev Data Docs Native Platforms (`apps/onestack.dev/data/docs/native-platforms.mdx`)

---
title: Native Platform Notes
description: Understand OS requirements, unsupported platforms, permissions, and native build limits
---

The v2 native surface targets React Native 0.87 and the New Architecture.
One's iOS package floor is 17. That deployment target is separate from the OS
version required by an individual API.

## iOS availability

Modern SwiftUI tabs need iOS 18; an older TabView path serves earlier supported
iOS versions. Presentation sizing also needs iOS 18. Liquid Glass, glass button
styles, WebView, and tab-bar minimization require iOS 26. Some generated
modifiers require newer SDKs and runtime versions.

Unavailable behavior depends on the component. Unsupported enum values are
validated before reaching native code. WebView renders empty below iOS 26;
Glass uses its configured material fallback on older systems. Read the
[component reference](/docs/native-components) before relying on a newer
feature. A lower deployment target does not make a newer API available.

Hardware and system policy matter too. Cameras, motion sensors, successful
device attestation, store purchases, printer delivery, and background scheduling
have limits that a simulator cannot fully prove. Review prompts are requests to
iOS, not a promise that a dialog will appear. BackgroundTasks follows the OS's
scheduling policy rather than a JavaScript timer.

## Android availability

Compose components require an Android native binary and the New Architecture.
Their controls and default colors come from Material. Native menus use
Android popup surfaces; they are distinct from the iOS menu contract.
Google Maps needs the app's API key and build configuration. Push notifications
need their platform configuration as well. See the
[Map](/native/map) and [Notifications](/native/notifications) references.

Use the generated project's Android SDK settings and JDK 17. Raising or
lowering `native.app.android` SDK levels must remain compatible with React
Native and the native dependencies your app installs.

## Web behavior

Platform-only SwiftUI and Compose components throw when rendered on the wrong
platform. Select a platform branch or use platform-specific files. Shared
components and services have individual web behavior:

| Surface | Web behavior |
| --- | --- |
| Image, TextInput, Icon | Render web content; Icon needs your web element |
| EdgeFade | CSS rendering |
| Blur, Mask | Require native and throw |
| Map | Renders no map |
| PictureInPicture | Renders inline content |
| SafeArea | Uses CSS safe-area environment values |
| Clipboard, Network, Browser | Use the corresponding browser facilities |
| Haptics, Notifications | Inert feedback or permission results; see each API |
| SecureStore | Rejects; synchronous verbs throw |
| iOS services | Generally reject unsupported calls; check the service reference |

The UIKit toolbar, menu-action, and zoom integrations return `null` on
unsupported platforms. SplitView renders its routed slot without native
columns on Android and web. These exceptions do not make arbitrary
`One.iOS` components safe to render everywhere.

See the [per-feature platform table](/native/platform-support) for more detail.

## Native rebuilds and updates

Adding a native dependency, changing permissions or entitlements, generating a
widget extension, or editing Swift or Kotlin requires a native rebuild. Hot
reload updates JavaScript; it does not replace compiled native code.

OTA updates must match the runtime and native capabilities of the installed
binary. A JavaScript bundle cannot add a missing native module. Configure the
[Updates service](/native/updates) or another supported
[OTA integration](/docs/guides-ota-updates) around that boundary.

## Layout and accessibility

Use React Native `style` for a native host's outer bounds, and `swiftStyle` or
`composeStyle` for the corresponding native content. Avoid having a
content-sized parent and a filling child depend on each other's size.
Scrolling and presentation content needs a defined viewport.

Imported Kotlin composables contribute their natural content size to the React
Native layout. The surrounding Yoga container retains parent stretch, flex,
and minimum or maximum dimensions. Content height updates when composable
props or Kotlin state changes.

Keep labels, accessibility props, and native control state aligned with what
the user sees. Verify the actual platform screen, including Dynamic Type,
keyboard, safe areas, dark appearance, and the OS versions you support. A web
preview establishes the web implementation; it does not establish native
behavior.

---

### Apps Onestack.Dev Data Docs Native Setup (`apps/onestack.dev/data/docs/native-setup.mdx`)

---
title: Native Setup
description: Install One Native and generate its iOS and Android projects
---

One Native is included in the `one` package. There is no separate native UI
package to install. One v2 uses React 19.3.0 and React Native 0.88;
the current examples use 0.88.0. Native components require the New Architecture. The
[installation guide](/docs/installation) lists the full dependency set,
including screens, Worklets, and Liquid Glass.
The iOS deployment floor is 17. See the
[platform notes](/docs/native-platforms) for feature-specific requirements.

For a new app, start with:

```bash
npx one
```

For an existing One app, update `one` to v2 and align its React, React
Native, and React Navigation peers. Use your package manager to inspect the
installed package's peer dependencies. The navigation peers are 8 alpha
versions, so keep their versions aligned.

## Configure the app

Configure the application through `native.app` in `vite.config.ts`:

```ts
import { one } from 'one/vite'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    one({
      native: {
        app: {
          name: 'MyApp',
          ios: {
            bundleId: 'com.example.myapp',
            deploymentTarget: '17.0',
          },
          android: {
            applicationId: 'com.example.myapp',
          },
        },
      },
    }),
  ],
})
```

Add `react-native.config.cjs` at the app root:

```js
module.exports = require('one/react-native-config')
```

For an existing app, install the community native template as a development
dependency. One supplies the CLI and platform build tools:

```bash
npm install --save-dev @react-native-community/template@0.88.0
```

Then generate the projects from the app directory:

```bash
one prebuild
```

One generates `ios/` and `android/` from the React Native community templates.
React Native autolinking includes One's pod, Android module, Fabric components,
and native service dependencies. Run this through the app's scripts or your
package manager's local executable command if `one` is not on your shell PATH.

Use `native.app` for icons, launch screens, fonts, deep links, and permissions.
The [native project guide](/docs/guides-ios-native#one-prebuild) lists the
manifest fields. Give each permission a description of why your app needs it.
For example, `native.app.imagePicker.camera` supplies the camera usage
configuration. Updating native configuration or adding a native dependency
requires generating and rebuilding the native project.

One's native startup installs the ES2023 array methods `toSorted`, `toReversed`,
`toSpliced`, and `with` before app modules load. This includes Hermes releases
whose built-in array methods are incomplete. Apps use the standard methods
directly; dependency code and app startup need no separate polyfill setup.

Apps that keep Expo prebuild follow [Coming from Expo](/native/migrating-to-one#keeping-expo-prebuild).

## Run the app

Start One's development server in one terminal:

```bash
one dev
```

Build and launch the generated app in another:

```bash
one run:ios
```

Or use `one run:android`. iOS builds need macOS, Xcode, and CocoaPods. Android
builds need the Android SDK and JDK 17. Run native compilation on your build
machine or CI as appropriate for your project.

One bundles native apps with Rolldown, the same bundler as web.

`ONE_PUBLIC_` variables are exposed to app code; see
[Environment](/docs/environment). Rebuild the native app for native code or
manifest changes, even when JavaScript hot reload is working.

## Add a first native control

Put this in an iOS route, such as `app/settings.ios.tsx`:

```tsx
import { useState } from 'react'
import { One } from 'one'

export default function Settings() {
  const [enabled, setEnabled] = useState(true)

  return (
    <One.iOS.Form style={{ flex: 1 }}>
      <One.iOS.Section title="Notifications">
        <One.iOS.Toggle
          label="Allow notifications"
          isOn={enabled}
          onIsOnChange={setEnabled}
        />
      </One.iOS.Section>
    </One.iOS.Form>
  )
}
```

The callback updates the controlled setting in React state. For shared native
text-input state, use `useNativeState` with [TextInput](/native/text-input).
This sample changes a setting only; requesting notification permission is a separate
[Notifications API](/native/notifications) call.

---

### Apps Onestack.Dev Data Docs Native Source Events (`apps/onestack.dev/data/docs/native-source-events.mdx`)

---
title: Native events
description: Native-to-JS subscriptions from Swift and Kotlin sources
---

A method whose only parameter is a callback is a subscription, not a call.
Calling it from JavaScript returns an unsubscribe function. Native posts
each payload over the one event bridge and the listener receives the
decoded values.

```swift
@MainActor final class Clock: PeachModule {
  init() {}

  private var tick: ((Double) -> Void)?
  func onTick(_ callback: @escaping (Double) -> Void) {
    tick = callback
  }
}
```

```kotlin
object Clock : OneModule {
    private var tick: ((Double) -> Unit)? = null
    fun onTick(callback: (Double) -> Unit) {
        tick = callback
    }
}
```

```tsx
import { Clock } from './native/Clock.swift'

const unsubscribe = Clock.onTick((at) => {
  console.log('tick', at)
})
unsubscribe()
```

The callback may take zero or more parameters; each one decodes with the
same wire rules as method arguments, so structs, enums, `Date` and `Data`
payloads revive before the listener runs. Pass a real function: anything
else throws before the subscription leaves JavaScript. Calling the returned
function removes the JavaScript listener, so call it when the component
unmounts.

Native keeps its own stored closure, so the author owns the native side of
the lifecycle: emitting after every JavaScript listener unsubscribed posts
to nobody, and a closure the author never releases stays retained. Under
the Peach simulator there is no JavaScript runtime to receive an event, so
emitted events queue in the module instead: the simulator host drains them
with the `peach_poll_events` export, which returns a length-prefixed JSON
array of `{ channel, args }` pairs using the same framing as
`peach_call_poll`.

---

### Apps Onestack.Dev Data Docs Native Source Expo (`apps/onestack.dev/data/docs/native-source-expo.mdx`)

---
title: Swift and Kotlin in Expo prebuild apps
description: Import Swift and Kotlin source files in an app that uses Expo prebuild
---

Apps on Expo prebuild import Swift and Kotlin with the `nativeSource` option
of `vxrn/expo-plugin`. See
[Coming from Expo](/native/migrating-to-one#swift-and-kotlin-source).

---

### Apps Onestack.Dev Data Docs Native Source Payloads (`apps/onestack.dev/data/docs/native-source-payloads.mdx`)

---
title: Native payload types
description: Structs, enums, Date and Data across the Swift and Kotlin boundary
---

Method arguments, results, view props and callback arguments cross as JSON
by default, and four richer shapes cross with it: a `Codable` struct or
`data class` crosses as its object shape, a `String` or `Int` backed enum
crosses as its raw value, a `Date` crosses as an ISO string revived to a
`Date`, and `Data` or `ByteArray` crosses as base64 revived to a
`Uint8Array`.

```swift
struct Point: Codable {
  var x: Double
  var y: Double
  var label: String? = nil
}

enum Direction: String {
  case north
  case south
}

@MainActor final class Atlas: PeachModule {
  init() {}

  func pin(_ point: Point) -> Point { point }
  func heading(_ direction: Direction) -> Direction { direction }
  func at(_ when: Date) -> Date { when }
}
```

```kotlin
data class Point(val x: Double, val y: Double, val label: String? = null)

enum class Direction {
    NORTH,
    SOUTH
}

object Atlas : OneModule {
    fun pin(point: Point): Point = point
    fun at(when: Date): Date = when
    fun blob(data: ByteArray): ByteArray = data
}
```

```tsx
import { Atlas, type Point } from './native/Atlas.swift'

const point: Point = { x: 1, y: 2 }
await Atlas.pin(point)
await Atlas.at(new Date())
```

A struct field with a value is optional in TypeScript; Codable and the
generated Kotlin glue fill the default when the key is missing. A Kotlin
default must be visible to the generated glue. Structs nest, including
inside arrays and maps. An enum needs its cases listed without explicit raw
values or associated values. Every struct and enum lives in the file that
uses it, must be visible to the generated glue, and a Swift struct must
conform to `Codable` in its declaration. Anything else keeps failing with
the file and line that declares it.

Pass a real `Date` and a real `Uint8Array`: anything else throws before the
call leaves JavaScript. Native results revive the same way, so what arrives
is a `Date` and a `Uint8Array`, never the string on the wire.

---

### Apps Onestack.Dev Data Docs Native Source (`apps/onestack.dev/data/docs/native-source.mdx`)

---
title: Swift and Kotlin
description: Import Swift and Kotlin source files from TypeScript and call them as typed functions and components
---

An app can import its own Swift and Kotlin files. One reads each file's
declarations, writes a TypeScript declaration next to it, and generates the
native glue that `one prebuild` compiles into the app. There is no module
boilerplate, no bridge registration and no codegen step to run by hand.

```tsx
import { Device } from './native/Showcase/Device.swift'

const hash = await Device.sha256('hello')
```

Swift imports run on iOS and Kotlin imports run on Android. Native source
requires a project generated by `one prebuild`; apps on Expo prebuild follow
[Coming from Expo](/native/migrating-to-one#swift-and-kotlin-source).

## Swift functions

Put Swift files in a Swift package: any directory under the app with a
`Package.swift`. The directory name names the package, so keep it unique and
different from the app's own name. A nested JavaScript package (a directory
with its own `package.json`) counts only when its root holds the
`Package.swift`; Swift packages deeper inside it belong to that package.

```swift
// native/Showcase/Package.swift
// swift-tools-version: 6.2
import PackageDescription

let package = Package(
  name: "Showcase",
  platforms: [.iOS(.v17)],
  targets: [
    .executableTarget(name: "Showcase", path: ".", swiftSettings: [.swiftLanguageMode(.v6)]),
  ]
)
```

A class that conforms to `PeachModule` becomes a named export. Each method that
is not `private` or `fileprivate` becomes an async function:

```swift
// native/Showcase/Device.swift
import CryptoKit
import UIKit

@MainActor final class Device: PeachModule {
  init() {}

  func info() -> [String: String] {
    ["model": UIDevice.current.model, "system": UIDevice.current.systemVersion]
  }

  func sha256(_ text: String) -> String {
    SHA256.hash(data: Data(text.utf8)).map { String(format: "%02x", $0) }.joined()
  }
}
```

```ts
import { Device } from './native/Showcase/Device.swift'

await Device.info() // Promise<Record<string, string>>
await Device.sha256('hello') // Promise<string>
```

Methods may be `async` and may `throw`; a thrown error rejects the promise.
The package's sources may import Apple frameworks. SwiftPM package
dependencies are not supported; add native libraries as React Native
dependencies instead.

## Swift views

A `public struct` that conforms to `View` becomes a named React component. Its
stored properties are the props, and a closure property is a callback:

```swift
// native/Showcase/Level.swift
import SwiftUI

public struct Level: View {
  let value: Double
  let onChange: (Double) -> Void

  public var body: some View {
    Slider(value: Binding(get: { value }, set: onChange))
  }
}
```

```tsx
import { Level } from './native/Showcase/Level.swift'

<Level value={value} onChange={setValue} />
```

The view is built through its memberwise initializer, so it has no `init` of
its own. A property that already has a value, such as `@State private var
dragging = false`, stays the view's own. A property typed `T?` is an optional
prop, and a `var` may only default to `nil`. `@Binding` is not a prop; take a
value and a callback instead. Views that are not `public` stay inside the
package.

### A package as one view

The package's `@main` type is the default export of its file when it conforms
to `PeachPackage`. The element's props arrive as untyped `JSON`:

```swift
@main struct Badge: PeachPackage {
  func view(props: JSON) -> some View {
    Text(props["title"]?.stringValue ?? "")
  }
}
```

```tsx
import Badge from './native/Badge/Badge.swift'

<Badge title="new" />
```

A package has at most one `@main` type. A whole SwiftUI app
(`@main struct MyApp: App`) imported this way renders its scene's root view and
fills the space it is given.

## Kotlin functions and views

A Kotlin file can sit anywhere in the app outside `android`, `ios`,
`node_modules` and nested packages. Packages declared in the build root's
`workspaces` are included; their source IDs use paths relative to that build root. An `object` that implements `OneModule`
becomes a named export, and each of its public functions an async function.
`suspend` functions are awaited. A public `@Composable` function with a
capitalized name becomes a React component whose props are its parameters;
function-typed parameters are callbacks. Views compile against Compose UI,
Foundation, Material 3 and Navigation.

```kotlin
// native/Device.kt
import android.os.Build
import androidx.compose.material3.Slider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import dev.onejs.one.source.OneModule
import java.security.MessageDigest

object Device : OneModule {
    fun info(): Map<String, String> =
        mapOf("model" to Build.MODEL, "system" to Build.VERSION.RELEASE)

    fun sha256(text: String): String =
        MessageDigest.getInstance("SHA-256")
            .digest(text.toByteArray())
            .joinToString("") { "%02x".format(it) }
}

@Composable
fun Level(value: Double, onChange: (Double) -> Unit, modifier: Modifier = Modifier) {
    Slider(value = value.toFloat(), onValueChange = { onChange(it.toDouble()) }, modifier = modifier)
}
```

```tsx
import { Device, Level } from './native/Device.kt'

<Level value={value} onChange={setValue} />
```

A composable's `Modifier` parameter must default to `Modifier`; One supplies
it. Other parameters may only default to `null`, which makes the prop optional.
The composable sizes to its content inside the surrounding layout.

## Types

These types cross the boundary as JSON:

| Swift | Kotlin | TypeScript |
| --- | --- | --- |
| `String` | `String` | `string` |
| `Bool` | `Boolean` | `boolean` |
| `Double`, `Float`, `Int` | `Double`, `Float`, `Int`, `Long` | `number` |
| `[T]` | `List<T>` | `T[]` |
| `[String: T]` | `Map<String, T>` | `Record<string, T>` |
| `T?` | `T?` | `T \| null` |
| `Void` | `Unit` | `void` |

These types also cross as view props and callback arguments. Integer arguments
must be whole numbers in range, and `Long` results must fit in JavaScript's
safe integer range. Overloads, generics, default arguments and static methods
are rejected with the file and line.

The declaration sits beside the source as `Device.d.swift.ts` or
`Device.d.kt.ts`. The dev server and `one prebuild` rewrite it whenever they
read the file, and `one typegen` writes all of them. TypeScript reads these
declarations when `allowArbitraryExtensions` is enabled in `tsconfig.json`.

## One import on every platform

A `.swift` import only bundles for iOS and a `.kt` import only for Android. To
use one API everywhere, give the platforms their own entry files and keep the
web implementation in TypeScript:

```
features/native/device.ios.tsx      export { Device } from '../../native/Showcase/Device.swift'
                                    export { Level } from '../../native/Showcase/Level.swift'
features/native/device.android.ts   export { Device, Level } from '../../native/Device.kt'
features/native/device.tsx          export const Device = { ... }  // web
```

Screens import `features/native/device` and each platform's bundler picks its
file. The starter app created by `npx one` includes this example at the
`/native` route.

## Rebuilds

Native source compiles into the app binary. After adding or editing a Swift or
Kotlin file, run `one prebuild` and rebuild the app. Each generated function
carries a hash of its native contract: if JavaScript reloads against a binary
built from a different contract, calls reject with `E_NATIVE_SOURCE_REBUILD`
instead of reaching mismatched code. OTA updates follow the same rule; see
[native rebuilds and updates](/docs/native-platforms#native-rebuilds-and-updates).

---

### Apps Onestack.Dev Data Docs One Build (`apps/onestack.dev/data/docs/one-build.mdx`)

---
title: one build
---

Building your app for production happens through `one build`.

It takes the following arguments (see the [CLI reference](/docs/cli) for every command):

```bash
npx one build [options]

--platform web|ios|android   # what to build (default: web)
--only <pattern>             # limit the pages built to a micromatch pattern
--step <name>                # run a single internal build step
--skip-env                   # skip loading .env files during build
```

To build for iOS for example, you would run:

```bash
npx one build --platform ios
```

## Building for web

When building One for web, the output will be a mostly static directory of HTML, CSS, and JavaScript. Internally, One runs a few different Vite production builds to output client, server, and API folders. This output is always in the `dist` directory.

Ensure your `.env` file has a `ONE_SERVER_URL` set to your production URL, like:

```
ONE_SERVER_URL=https://onestack.dev
```

We provide a production server based on Hono with `one serve` that handles loaders, SSR, and API routes for you. You can choose a deploy target for that server. This can be chosen via your Vite configuration:

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      web: {
        deploy: 'vercel' // defaults to 'node'
      }
    })
  ]
}
```

For detailed deployment instructions, see the [Deployment Guide](/docs/guides-deployment).

If you are only using SPA and SSG routes and aren't using loaders, you can statically serve the results of `dist/client` using any server you'd like.

### Parallel builds with worker threads

By default, One uses worker threads to build static pages in parallel across multiple CPU cores. This can significantly speed up builds for sites with many pages.

One uses two workers by default, which keeps peak memory low. Set `build.concurrency` to use more when the machine has memory to spare:

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      build: {
        concurrency: 4
      }
    })
  ]
}
```

`ONE_BUILD_CONCURRENCY` overrides the config value for one-off builds:

```bash
ONE_BUILD_CONCURRENCY=2 npx one build
```

You can also disable worker threads entirely through config or an environment variable:

```tsx fileName=vite.config.ts
one({
  build: {
    workers: false
  }
})
```

```bash
ONE_BUILD_WORKERS=0 npx one build
```

### Security scanning

One automatically scans your client bundles for leaked secrets after building. By default it logs warnings — for production deployments, we recommend setting `securityScan` to `'error'` to fail the build if secrets are found:

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      build: {
        securityScan: 'error'
      }
    })
  ]
}
```

The scanner detects common API key patterns (Anthropic, OpenAI, Stripe, GitHub, AWS), bearer tokens, and generic secret assignments. See [Configuration: build.securityScan](/docs/configuration#buildsecurityscan) for all options.

## Building for native

Build the native JS bundle with `--platform ios` or `--platform android`. This always runs through the Rolldown path, regardless of your `native.bundler` dev setting.

To ship to devices, integrate the bundle into the existing native build process. See [Build or Run iOS](/docs/guides-ios-native) for archiving and uploading.

Or check out the [iOS Native Guide](/docs/guides-ios-native) for instructions on building your iOS app with Xcode.

---

### Apps Onestack.Dev Data Docs One Dev (`apps/onestack.dev/data/docs/one-dev.mdx`)

---
title: one dev
---

Developing an app with One is as simple as `one dev`.

It takes the following arguments (see the [CLI reference](/docs/cli) for every command):

```bash
--clean           # boolean, will clear all caches before running
--host            # string, set the hostname to bind to
--port            # string, set the port to bind to
--mode            # "development" or "production": serve the production bundle from the dev server
--debug-bundle    # string, write the bundle to a temp file and serve it from there for debugging
--debug           # string, pass debug args to Vite
--extra-config    # string, path to an extra Vite config file to merge on top of the project config
```

You can also set the port with the `ONE_PORT` or `ONE_FORCE_PORT` environment variables. See [Environment Variables](/docs/environment#server) for details.

The One development server serves both web and native apps on the same port at the same time, and should hot reload both at once. When you see the `Server running on` message with a host and port, that is the same for both native and web.

## Native Development

The `one dev` command only starts the development server — it does not build or run the native iOS/Android shell. One ships native code inside the `one` package, so native development uses your own app build:

1. Run `one prebuild` to generate the native Xcode/Android project
2. Run `one run:ios` or `one run:android` to build and launch the app

See the [Build or Run the Native iOS App](/docs/guides-ios-native) guide for detailed instructions.

---

### Apps Onestack.Dev Data Docs One Serve (`apps/onestack.dev/data/docs/one-serve.mdx`)

---
title: one serve
---

Serve your web apps for production with `one serve`, after you've successfully run `one build`.

It takes the following arguments (see the [CLI reference](/docs/cli) for every command):

```bash
--host          # string, set the hostname to bind to
--port          # string, set the port to bind to
--compress      # boolean, enable gzip compression, defaults to true
--loadEnv       # boolean, whether to load .env files before running
--outDir        # string, build output directory (default: dist)
--cluster       # enable cluster mode using all CPU cores
--cluster=N     # enable cluster mode with N workers
```

You can also set the port with the `ONE_PORT` or `ONE_FORCE_PORT` environment variables. See [Environment Variables](/docs/environment#server) for details.

One comes with a serve command that is powered by [Hono](https://hono.dev).

If you are only using SPA and SSG routes and aren't using loaders, you don't need to use `one serve` if you don't want to - you can statically serve the results of `dist/client` using any server you'd like.

You must run a `one build` before serve.

## Cluster Mode

For production deployments under heavy load, `one serve` supports cluster mode. This forks multiple Node.js worker processes that share the same port, distributing requests across CPU cores.

```bash
one serve --cluster       # use all CPU cores
one serve --cluster=4     # use 4 workers
one serve                 # single process (default)
```

Each worker handles requests independently. If a worker crashes, it automatically restarts with backoff protection to prevent crash loops.

### When to use cluster mode

- High-traffic production deployments
- Servers with many CPU cores
- Handling 200+ concurrent connections
- SSR-heavy workloads where renders block the event loop

### When single process is better

- Development (use `one dev` instead)
- Low-traffic sites where single process is simpler and has less overhead
- Less than ~100 concurrent connections, where single process may be faster due to less IPC overhead

### Performance

At 500 concurrent connections (production-style load test):

- Single process: ~2,150 req/s
- 8 workers: ~2,939 req/s (1.4x improvement)
- At 200 connections, cluster is 2.4x faster than single process

## Programmatic Usage

You can also run your production server programmatically:

```tsx
import { serve } from 'one/serve'

await serve()
```

Which takes options:

```tsx
type ServeOptions = {
  // you can pass in your own Hono server
  app?: Hono

  host?: string
  port?: number
  compress?: boolean
  outDir?: string

  /**
    * Whether to run the Vite logic to load .env files before running the server
    * @default false
    */
  loadEnv?: boolean

  /**
    * Enable cluster mode with multiple worker processes.
    * Pass true to use all CPU cores, or a number for specific worker count.
    */
  cluster?: boolean | number
}
```

---

### Apps Onestack.Dev Data Docs Render Modes (`apps/onestack.dev/data/docs/render-modes.mdx`)

---
title: Render Modes
---

A unique feature of One is the ability to seamlessly route across a variety of route modes. What this means in plain english, is that you can choose whether each individual page within your routes uses one of a few strategies, such as rendering statically at build time, or rendering on-demand at request time.

One supports the following render modes, which are explained in detail below: `api`, `spa`, `ssr`, and `ssg`.

You can set your global render mode with the One vite plugin. The default is `ssg`.

```tsx fileName=vite.config.ts
import { one } from 'one/vite'

export default {
  plugins: [
    one({
      web: {
        defaultRenderMode: 'spa'
      }
    })
  ]
}
```

To specify the render mode on a per-page basis, you add a suffix to the filename, like so:

- `route+ssg.tsx` - Matches `/route`, but will render the page as a [SSG route](#ssg-the-default).
- `route+spa.tsx` - Matches `/route`, but will render the page as a [SPA route](#spa).
- `route+ssr.tsx` - Matches `/route`, but will render the page as a [SSR route](#ssr).
- `route+api.tsx` - Matches `/route`, but will render the page as an [API route](#api).

### Layout Render Modes

Layouts can also specify render modes using the same suffix pattern:

- `_layout+ssg.tsx` - Layout is pre-rendered at build time (static)
- `_layout+ssr.tsx` - Layout is rendered on the server per-request
- `_layout+spa.tsx` - Layout is client-rendered only (default behavior)

**Why this matters for SPA pages:**

When an SPA page has a parent layout with `+ssg` or `+ssr` render mode, the layout shell is rendered on the server while the page content remains client-rendered. This gives users immediate visual content (navigation, sidebar, head tags, CSS) without waiting for JavaScript to download and execute.

```
app/
├── _layout+ssg.tsx      # Root layout rendered at build time
├── dashboard+spa.tsx    # SPA page - but layout shell is pre-rendered!
└── settings+spa.tsx     # Same - instant layout, client page content
```

This is the recommended approach for SPA-heavy apps that still want fast initial paint. The layout's HTML is included in the initial response, and page content hydrates after JavaScript loads.

### Folder-Level Render Modes

You can also apply render modes to entire folders, and all routes within that folder will inherit the render mode. This is useful for organizing routes by their rendering strategy:

```
app/
├── dashboard+ssr/         # All routes use SSR
│   ├── index.tsx         # Uses SSR
│   ├── analytics.tsx     # Uses SSR
│   └── settings+spa.tsx  # Overrides to SPA
├── blog+ssg/             # All routes use SSG
│   ├── index.tsx         # Uses SSG
│   └── [slug].tsx        # Uses SSG
└── index.tsx             # Uses vite config default
```

**Render mode hierarchy:**
1. **File suffix** (highest priority) - `page+spa.tsx`
2. **Parent folder suffix** - `dashboard+ssr/page.tsx`
3. **Vite configuration** (lowest priority) - `defaultRenderMode: 'ssg'`

**Nested folders:**
Inner folder suffixes override outer folder suffixes:

```
app/
└── dashboard+ssr/
    ├── index.tsx          # Uses SSR
    └── reports+ssg/
        └── monthly.tsx    # Uses SSG (inner folder wins)
```

**Important notes:**
- Folder suffixes are stripped from URLs: `dashboard+ssr/analytics` becomes `/dashboard/analytics`
- File suffixes always take precedence over folder suffixes
- Layouts do not inherit folder render modes (they remain as layouts)

**API routes benefit too:**
With folder suffixes, API routes no longer require the `+api.tsx` file suffix:

```
app/
└── api+api/              # Folder suffix
    ├── users.tsx         # No +api.tsx needed! → /api/users (API)
    ├── posts.tsx         # No +api.tsx needed! → /api/posts (API)
    └── auth/
        └── login.tsx     # Inherits API type → /api/auth/login (API)
```

This makes organizing API routes much cleaner, especially for versioned APIs like `v1+api/` and `v2+api/`.

## Modes

Note that render modes mostly only apply to the web. On native, everything is essentially a SPA, with all the JS for every route included in the app the user downloads from the app store. Because native apps don't have to deal with the network, this SPA mode is ideal and has very little downsides.

### SSG (the default)

When you don't add a `+` suffix to your route, it will default to an "SSG" route, unless you set the [`web.defaultRenderMode` configuration](/docs/configuration#webdefaultrendermode) otherwise. An example of a SSG route would be something like `app/blog.tsx` if you haven't changed the defaultRenderMode, or if you have `app/blog+ssg.tsx`.

SSG Stands for "[Server Side Generated](https://www.smashingmagazine.com/2020/07/differences-static-generated-sites-server-side-rendered-apps/)", it's a strategy for web where you pre-render the entire HTML and CSS of a page at build-time fully on the server (or in CI) so that you can serve the page quickly from something like a CDN, without running any JavaScript at the time of request.

We chose SSG as the default behavior as it serves as a good balance between simplicity and performance. What it is less good at is serving dynamic content to an individual user.

That said - you can still do dynamic content on top of an SSG page. It's just that you'll have to have the dynamic content load in after the browser finishes downloading and parsing the JS for that page, and it will need to gracefully replace the static content that the user sees.

This pattern is great for things like a SaaS homepage that shows mostly static content, but where you may want to load a logged-in menu for the current user. It's not good for something like a logged-in dashboard.

### SPA

If you name a route with the `+spa` suffix, like `app/dashboard+spa.tsx`, it will no longer render on the server - either at build-time or at request. Instead, at build time One will build just the client-side JavaScript necessary.

This render mode is great for highly dynamic use cases. Think a Linear, or a Figma, a metrics dashboard, or a user account panel. It is simpler to build, and doesn't require making sure every dependency works on both Node.js and in the browser.

Its downsides include a slower initial load time, and worse SEO. You can mitigate this by using a `_layout+ssg.tsx` or `_layout+ssr.tsx` root layout, which renders your layout shell on the server while keeping page content client-rendered. See [Layout Render Modes](#layout-render-modes) above.


### SSR

If you name a route with the `+ssr` suffix, like `app/issues+ssr.tsx`, you will get a server rendered page. This mode will generate the JS for both server and client at build-time, but instead of rendering out the HTML and CSS right then, it will instead wait for a request to the production server before it imports the server JS, renders the page, and then returns the HTML and CSS.

This mode is great for things that need to be dynamically rendered more often than whenever you deploy. One example is something more like GitHub Issues. By server rendering a page like this, you get faster initial loads, and better SEO than a SPA-type page. But it is the most expensive in terms of cost - each request will now run a full render of the route, and also slower than SSG in terms of initial response time, at least by default.

In the case of a GitHub Issues type page, what you'd do is cache the SSR response on a CDN, and then clear the CDN cache whenever data for that page updates. This is complex, and generally SSR is the most complex of the three render modes to support. Because it is more complex and more expensive, we generally recommend using SPA or SSG unless you are certain you can afford to pay for the extra render cost, and/or cache responses without too much trouble.

### API

Adding `+api.ts` to the end of your filename turns a route into an API route. API routes are HTTP endpoints that you call from other routes or services, not pages that users navigate to directly.

```tsx fileName=app/api/hello+api.ts
export function GET(request: Request) {
  return Response.json({ message: 'Hello!' })
}
```

API routes use Web standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects.

See [API Routes](/docs/routing-api-routes) for full documentation.

---

### Apps Onestack.Dev Data Docs Routing Api Routes (`apps/onestack.dev/data/docs/routing-api-routes.mdx`)

---
title: API Routes
---

API routes let you create HTTP endpoints in your One app. Add `+api.ts` to the end of your filename and export handler functions for each HTTP method.

## Basic Usage

```tsx fileName=app/api/hello+api.ts
export function GET(request: Request) {
  return Response.json({ message: 'Hello!' })
}
```

This creates an endpoint at `/api/hello` that responds to GET requests.

## HTTP Methods

Export named functions for each HTTP method:

```tsx fileName=app/api/users+api.ts
export function GET(request: Request) {
  return Response.json({ users: [] })
}

export function POST(request: Request) {
  // handle POST
}

export function PUT(request: Request) {
  // handle PUT
}

export function DELETE(request: Request) {
  // handle DELETE
}

export function PATCH(request: Request) {
  // handle PATCH
}

export function HEAD(request: Request) {
  // set headers for a GET without returning a body
  return new Response(null, { headers: { 'x-total-count': '42' } })
}

export function OPTIONS(request: Request) {
  // shape CORS preflight responses — see the parity note below
  return new Response(null, {
    status: 204,
    headers: {
      'access-control-allow-origin': request.headers.get('origin') || '*',
      'access-control-allow-methods': 'GET, POST, OPTIONS',
    },
  })
}
```

You can also export a `default` function as a catch-all:

```tsx
export default function handler(request: Request) {
  return Response.json({ method: request.method })
}
```

If a route has no handler for the requested method and no default export, One returns `405 Method Not Allowed` with an `Allow` header listing its exported HTTP method handlers. API routes require explicit `HEAD` and `OPTIONS` handlers, or a default handler; middleware can also answer these requests.

## Dev and prod parity

API route handlers — including `HEAD` and `OPTIONS` — are dispatched the same way in both `one dev` (Vite) and `one serve` (Hono on Node / Cloudflare Workers / Vercel). Your middleware chain and method exports are the source of truth: whatever response they produce in dev is what ships in prod.

For `OPTIONS` preflight specifically, One configures Vite's dev server with `server.cors: { preflightContinue: true, origin: true, credentials: true }` so the built-in cors middleware sets permissive headers on non-preflight requests but lets preflight `OPTIONS` fall through to your handlers. If you override `server.cors` in your own `vite.config.ts`, keep `preflightContinue: true` or your middleware's preflight response won't reach the browser in dev.

## Dynamic Routes

Use brackets for dynamic segments. Params are passed as the second argument:

```tsx fileName=app/api/users/[id]+api.ts
export function GET(request: Request, { params }: { params: { id: string } }) {
  return Response.json({ id: params.id })
}
```

Rest parameters work the same way:

```tsx fileName=app/api/files/[...path]+api.ts
export function GET(request: Request, { params }: { params: { path: string[] } }) {
  const filePath = params.path.join('/')
  return Response.json({ path: filePath })
}
```

## Request Handling

### Reading the Body

```tsx
export async function POST(request: Request) {
  // JSON
  const json = await request.json()

  // Form data
  const form = await request.formData()

  // Raw text
  const text = await request.text()

  return Response.json({ received: true })
}
```

### Query Parameters

```tsx
export function GET(request: Request) {
  const url = new URL(request.url)
  const page = url.searchParams.get('page') || '1'
  const limit = url.searchParams.get('limit') || '10'

  return Response.json({ page, limit })
}
```

### Headers

```tsx
export function GET(request: Request) {
  const auth = request.headers.get('Authorization')

  return Response.json({ authenticated: !!auth })
}
```

## Response Handling

### Custom Status Codes

```tsx
export async function POST(request: Request) {
  const data = await request.json()

  if (!data.name) {
    return Response.json({ error: 'Name required' }, { status: 400 })
  }

  return Response.json({ id: 1, ...data }, { status: 201 })
}
```

### Custom Headers

```tsx
export function GET(request: Request) {
  return new Response(JSON.stringify({ data: [] }), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, max-age=3600',
    },
  })
}
```

### Redirects

```tsx
import { redirect } from 'one'

export function GET(request: Request, { params }: { params: { id: string } }) {
  return redirect(`/api/v2/users/${params.id}`)
}
```

## Error Handling

```tsx
export async function GET(request: Request, { params }: { params: { id: string } }) {
  const user = await getUser(params.id)

  if (!user) {
    return Response.json({ error: 'User not found' }, { status: 404 })
  }

  return Response.json(user)
}
```

## Web Standards

API routes use the standard Web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs. TypeScript and Node >= 20 include these globally.

## Type Helpers

One exports an `Endpoint` type for simple cases:

```tsx
import type { Endpoint } from 'one'

export const GET: Endpoint = (request) => {
  return Response.json({ hello: 'world' })
}
```

For typed params, use `createAPIRoute`:

```tsx
import { createAPIRoute } from 'one'

export const GET = createAPIRoute<'/api/users/[id]'>((request, { params }) => {
  // params.id is typed as string
  return Response.json({ id: params.id })
})
```

## Cloudflare Worker Runtime

When deployed to Cloudflare Workers, API handlers also receive a `worker` context containing the worker's `env` bindings and `executionCtx` (with `waitUntil` and `passThroughOnException`). The `worker` key is `undefined` on Node/dev so you can write runtime-portable code.

```tsx fileName=app/api/track+api.ts
import { createAPIRoute } from 'one'

export const POST = createAPIRoute(async (request, { worker }) => {
  const body = await request.json()

  // fire-and-forget: keep the worker alive for the background task
  // without blocking the response
  worker?.executionCtx?.waitUntil(
    fetch('https://analytics.example.com/track', {
      method: 'POST',
      body: JSON.stringify(body),
    })
  )

  return Response.json({ ok: true })
})
```

The `worker` context has this shape:

```ts
type WorkerContext = {
  env?: Record<string, unknown>      // your wrangler bindings (KV, D1, R2, etc.)
  executionCtx?: {
    waitUntil: (p: Promise<unknown>) => void
    passThroughOnException: () => void
  }
}
```

Use `env` to access bindings you've configured in `wrangler.json` (databases, KV namespaces, secrets, the `ASSETS` static-asset binding), and `executionCtx.waitUntil()` for background work that should outlive the response.

---

### Apps Onestack.Dev Data Docs Routing Exports (`apps/onestack.dev/data/docs/routing-exports.mdx`)

---
title: Routing Exports
---

This page catalogs all the special exports One supports from route files.

<Notice>
  Note that `_layout` routes are special and do not support these exports unless
  specified.
</Notice>

### SuspenseFallback

A `_layout` route can export a `SuspenseFallback` component to show loading UI while one of its child routes is suspended. Nested layouts inherit the fallback, and the nearest layout export takes precedence.

```tsx fileName=app/(app)/_layout.tsx
import { ActivityIndicator, Text, View } from 'react-native'
import { Stack, type SuspenseFallbackProps } from 'one'

export function SuspenseFallback({ params }: SuspenseFallbackProps) {
  return (
    <View>
      <Text>Loading profile {params.id}...</Text>
      <ActivityIndicator />
    </View>
  )
}

export default function AppLayout() {
  return <Stack />
}
```

The component receives the suspended route module's `contextKey` as `route` and its URL parameters as `params`. Route fallbacks are enabled by default on native. On web, set `web.suspendRoutes` to `true` in the One config to enable route Suspense boundaries.

### loader

Export a `loader` function to load data for the route on the server before it renders. See [Loaders](/docs/routing-loader) for the full guide, including loader arguments, redirects, caching, and validation.

### ErrorBoundary

Export an `ErrorBoundary` component to render a fallback when the route throws during render or its loader fails:

```tsx fileName=app/dashboard.tsx
import { type ErrorBoundaryProps } from 'one'

export function ErrorBoundary({ error, retry, route }: ErrorBoundaryProps) {
  return (
    <div>
      <p>Something went wrong: {error.message}</p>
      <button onClick={() => retry()}>Try again</button>
    </div>
  )
}

export default function Dashboard() {
  return <div />
}
```

The component receives `error`, a `retry()` function that clears the error state and re-renders, and optional `route` info (`pathname`, `routeName`, `params`, `errorType`, `componentStack`; see `ErrorRouteInfo`).

### validateParams

Export a `validateParams` schema to validate route params on the client before navigation. Works with Zod, Valibot, or custom functions. See [Route Validation](/docs/routing-loader#route-validation).

### validateRoute

Export an async `validateRoute` function for checks like resource existence before navigation. See [Route Validation](/docs/routing-loader#route-validation).

### generateStaticParams

This export is needed for "ssg" type pages that use dynamic path segments (for example, `[id].tsx`). At build-time, One will expand the id segment into multiple pages, using the values you return from `generateStaticParams`.

The entire function and export `generateStaticParams` will be removed at build-time and everything it depends on will be tree-shaken out of your client-side bundle.

You'll need to return an array of objects, where the object keys match the name inside the brackets. As an example, we'll use nested dynamic segments:

```tsx fileName=app/blog/[month]/[year]/[slug]+ssg.tsx
export async function generateStaticParams() {
  const blogPosts = await getAllBlogPosts()

  return blogPosts.map((post) => {
    return {
      month: post.month,
      year: post.year,
      slug: post.slug,
    }
  })
}
```

If this function returned this:

```tsx
;[
  {
    month: 10,
    year: 2025,
    slug: 'some-slug',
  },
]
```

Then you'd generate one page at `/blog/10/2025/some-slug`.

### sitemap

Control how a route appears in the generated `sitemap.xml`. This export is only used when `web.sitemap` is enabled in your config.

```tsx fileName=app/blog/[slug].tsx
// Customize sitemap entry for this route
export const sitemap = {
  // Priority (0.0 to 1.0) - higher means more important
  priority: 0.8,

  // How often the content changes
  changefreq: 'daily',

  // Last modification date (optional)
  lastmod: '2024-12-15',
}
```

You can also exclude a route from the sitemap entirely:

```tsx fileName=app/admin/dashboard.tsx
export const sitemap = {
  exclude: true,
}
```

**Available options:**

| Option       | Type             | Description                                                                               |
| ------------ | ---------------- | ----------------------------------------------------------------------------------------- |
| `priority`   | `number`         | Value from 0.0 to 1.0 indicating relative importance                                      |
| `changefreq` | `string`         | One of: `'always'`, `'hourly'`, `'daily'`, `'weekly'`, `'monthly'`, `'yearly'`, `'never'` |
| `lastmod`    | `string \| Date` | Last modification date for this route                                                     |
| `exclude`    | `boolean`        | Set to `true` to exclude this route from the sitemap                                      |

Route-level exports override the defaults set in your [config](/docs/configuration#websitemap).

---

### Apps Onestack.Dev Data Docs Routing Layouts (`apps/onestack.dev/data/docs/routing-layouts.mdx`)

---
title: Layouts
---

Layout files can be placed in any folder inside the `./app` directory. They serve as the frame for the routes contained within that folder, and they can nest inside of each other if you place them in sub-directories.

<RouteTree
  routes={[
    {
      name: '_layout.tsx',
      description: 'Wraps all files in this directory and below',
      highlight: true,
    },
    { name: 'index.tsx', description: 'Matches "/"' },
    {
      name: 'blog',
      children: [
        {
          name: '_layout.tsx',
          description: 'A custom sub-layout for all /blog pages',
          highlight: true,
        },
        { name: 'index.tsx', description: 'Matches "/blog"' },
        {
          name: '[slug].tsx',
          description: 'Matches a single sub-path of "/blog", like "/blog/hello"',
        },
      ],
    },
  ]}
/>

Layouts must render one of the following to show the matched pages that exist in their directory:

- [Slot](/docs/components-Slot): Directly show sub-route with no frame.
- [Stack](/docs/components-Stack): Creates a [React Navigation Stack](https://reactnavigation.org/docs/stack-navigator/) of sub-routes.
- [Tabs](/docs/components-Tabs): Creates a [React Navigation BottomTabs](https://reactnavigation.org/docs/bottom-tab-navigator) with sub-routes.
- [Drawer](/docs/components-Drawer): Creates a [React Navigation Drawer](https://reactnavigation.org/docs/drawer-navigator) with sub-routes.

On web, Stack, Tabs, and Drawer render headless by default: just the matched route, no header, no tab bar, no sidebar. See the [headless web navigators guide](/docs/guides-headless-web) for how to build your own web chrome.

This looks something like this at the simplest:

```tsx fileName=app/_layout.tsx
import { Slot } from 'one'

export default function Layout() {
  return <Slot />
}
```

## Layout Loaders

Layouts can export a `loader` function just like pages. This is useful for fetching data that's shared across all routes in that layout:

```tsx fileName=app/docs/_layout.tsx
import { useLoader, Slot } from 'one'

export async function loader() {
  return {
    navItems: await fetchDocsNavigation(),
    version: await fetchCurrentVersion(),
  }
}

export default function DocsLayout() {
  const data = useLoader(loader)

  return (
    <div>
      <Sidebar items={data.navItems} version={data.version} />
      <main>
        <Slot />
      </main>
    </div>
  )
}
```

Layout loaders run in parallel with page loaders for optimal performance. All matched route loaders (root layout → nested layouts → page) execute simultaneously.

### Accessing Page Data from a Layout

Use `useMatches` to access the current page's loader data from a layout:

```tsx fileName=app/docs/_layout.tsx
import { useMatches, Slot } from 'one'

export default function DocsLayout() {
  const matches = useMatches()
  const pageMatch = matches[matches.length - 1]
  // loaderData is unknown, so narrow it before reading fields
  const { headings } = (pageMatch?.loaderData ?? {}) as { headings?: string[] }

  return (
    <div>
      <main>
        <Slot />
      </main>
      <TableOfContents headings={headings} />
    </div>
  )
}
```

See [useMatches](/docs/hooks-useMatches) for more details.

### Layout Loader Caching

On client-side navigation, layout loaders do not re-run - their data is cached from the initial page load. Only the destination page's loader runs. This is intentional for performance, as layout data (navigation items, site config) rarely changes.

**When layout loaders DO re-run:**

- Full page loads (initial visit, hard refresh)
- SSR page requests (each request runs all matched loaders on server)
- After calling `window.location.reload()`

**When layout loaders DON'T re-run:**

- Client-side navigation via `<Link>` or `useRouter().push()`

If you need fresh layout data, you can trigger a full page navigation instead of client-side navigation, or store frequently-changing data in the page loader and access it via `useMatches()`.

If you need to access the current route params in a layout, you can use [the useActiveParams hook](/docs/hooks-useActiveParams).

## Root Layout

The root layout on web controls the entire page, from `<html>` on down. You can optionally return `html`, `body`, and `head` elements in your `app/_layout.tsx` and they will "just work" - on web, it will be output, and on native it won't so as to avoid rendering errors:

```tsx fileName=app/_layout.tsx
import { Slot } from 'one'

export default function Layout() {
  return (
    <html lang="en-US">
      <head>
        <meta charSet="utf-8" />
      </head>

      <body>
        <Slot />
      </body>
    </html>
  )
}
```

Note - you should have both a `lang` property on your `html` tag, and a `<meta charSet />` tag in your `head`. Otherwise, React can have hydration re-rendering issues.

### Layout Render Modes

Layouts support render mode suffixes just like pages. This is especially useful for the root layout:

```
app/
├── _layout+ssg.tsx     # Pre-rendered at build time
├── _layout+ssr.tsx     # Rendered per-request on server
└── _layout.tsx         # Client-rendered (default)
```

**When to use layout render modes:**

- **`_layout+ssg.tsx`** - Best for static navigation, site chrome, and head tags. The layout HTML is generated once at build time and served instantly for all pages.

- **`_layout+ssr.tsx`** - Use when your layout needs dynamic data per-request (e.g., auth state in the nav, user-specific content). The layout loader runs on each request.

- **`_layout.tsx`** (no suffix) - Client-rendered. Use when the layout has no server requirements.

**SPA pages with SSG/SSR layouts:**

When an SPA page has a parent layout with `+ssg` or `+ssr` render mode, One automatically renders the layout shell on the server. This gives users immediate visual content (navigation, sidebar, head tags, CSS) while page content remains client-rendered.

```tsx fileName=app/_layout+ssg.tsx
import { Slot } from 'one'

export async function loader() {
  return {
    navItems: ['Home', 'About', 'Contact'],
  }
}

export default function RootLayout() {
  return (
    <html>
      <head>
        <meta charSet="utf-8" />
        <title>My App</title>
      </head>
      <body>
        <nav>{/* Navigation renders immediately */}</nav>
        <main>
          <Slot /> {/* SPA page content loads after JS */}
        </main>
      </body>
    </html>
  )
}
```

With this setup, even `dashboard+spa.tsx` pages show the nav instantly - only the main content area waits for JavaScript.

### useServerHeadInsertion

The root `_layout.tsx` can use a special hook that allows it to insert tags into your `<head />` tag after server rendering finishes. This is an edge-case that is mostly only useful for CSS-in-JS libraries. You can use it like so:

```tsx
import { Slot, useServerHeadInsertion } from 'one'

export default function Layout() {
  useServerHeadInsertion(() => {
    // this will run after the entire page renders
    // return a single React.ReactElement that will be spliced into your <head />
    return <style>{renderCSS()}</style>
  })

  return (
    <html>
      <Slot />
    </html>
  )
}
```

## Groups and layouts

If you'd like to logically group together some pages without creating a sub-route, you can use a group folder by naming it like so:

<RouteTree
  routes={[
    { name: '_layout.tsx', description: 'Wraps all files in this directory and below' },
    { name: 'index.tsx', description: 'Matches "/"' },
    {
      name: '(blog)',
      highlight: true,
      children: [
        { name: '_layout.tsx', description: 'A custom sub-layout for all /blog pages' },
        { name: 'blog.tsx', description: 'Matches "/blog"' },
        {
          name: '[slug].tsx',
          description: 'Matches a single sub-path of "/blog", like "/blog/hello"',
        },
      ],
    },
  ]}
/>

Groups let you add extra layouts. If done right, this gives you a lot of control over how your app feels.

## Nested layouts example

A common pattern that apps have is something like Twitter/X, where you have bottom tabs for your "top level" views, but then on some of the bottom tab sections, you want to have a Stack that remembers its state inside just that tab.

This pattern can be incredibly verbose to link together with React Navigation, and takes a bit of tinkering to figure out with One. Since it is common and a useful example, lets walk through how you'd build on using One's file system routing.

Here's our file structure:

<RouteTree
  routes={[
    { name: '_layout.tsx', description: 'Where our Tabs layout is defined' },
    { name: 'notifications.tsx', description: 'Matches "/notifications"' },
    { name: 'profile.tsx', description: 'Matches "/profile"' },
    {
      name: '(feed)',
      highlight: true,
      children: [
        { name: '_layout.tsx', description: 'Where our Stack layout is defined' },
        { name: 'index.tsx', description: 'Matches "/"' },
        { name: 'post-[id].tsx', description: 'Matches routes like "/post-123"' },
      ],
    },
  ]}
/>

The top level Layout will define our tabs:

```tsx fileName=app/_layout.tsx
import { Platform } from 'react-native'

export function RootLayout() {
  return (
    <Tabs
      screenOptions={{
        headerShown: false,
      }}
    >
      <Tabs.Screen
        name="(feed)"
        options={{
          title: 'Feed',
          tabBarIcon: Platform.select({
            ios: { type: 'sfSymbol', name: 'newspaper' },
            android: { type: 'materialSymbol', name: 'newspaper' },
          }),
        }}
      />

      <Tabs.Screen
        name="notifications"
        options={{
          title: 'Notifications',
          tabBarIcon: Platform.select({
            ios: { type: 'sfSymbol', name: 'bell' },
            android: { type: 'materialSymbol', name: 'notifications' },
          }),
        }}
      />

      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: Platform.select({
            ios: { type: 'sfSymbol', name: 'person' },
            android: { type: 'materialSymbol', name: 'person' },
          }),
        }}
      />
    </Tabs>
  )
}
```

This will set us up with three bottom tabs: Feed, Notifications, and Profile. The Notifications and Profile tabs for now will just show their content directly, but inside of the Feed tab, we want to show a stack.

We set up the stack in `(feed)/_layout.tsx`:

```tsx fileName=(feed)/_layout.tsx
import { Slot, Stack } from 'one'

export default function FeedLayout() {
  return (
    <>
      {typeof window !== 'undefined' ? (
        <Slot />
      ) : (
        <Stack>
          <Stack.Screen name="index" options={{ title: 'Feed' }} />
          <Stack.Screen name="post-[id]" options={{ title: 'Post' }} />
        </Stack>
      )}
    </>
  )
}
```

One thing we're showing here is that the layout is diverging between web and native. On web, we are showing a Slot, while on Native we show a Stack. This is because browsers feel better without stacks - the native back/forward button serves as our stack controller.

On Native we are defining the configuration for each sub-screen with the `Stack.Screen` component. The Stack component is a [React Navigation native stack navigator](https://reactnavigation.org/docs/native-stack-navigator/) and nothing more, it accepts all the props you'd expect.

You'll notice we are matching the `name` to the file names names of the sub-routes, without the `.tsx` extension.

This will get you a nice Stack-inside-Tabs pattern that is common on native apps, all with just two layouts and a few routes.

## Limitations

Note that `useParams` won't work in layouts, as they are never nested inside a route. Instead, you can use `useActiveParams`.

---

### Apps Onestack.Dev Data Docs Routing Loader (`apps/onestack.dev/data/docs/routing-loader.mdx`)

---
title: Loaders
---

Loaders are useful for one-time loading of data from the server to the client. They can be used in both page routes and layout files in your `app` directory.

Loaders run on the server based on their render mode - during build-time for SSG routes, or on each request for SPA or SSR routes. Layout loaders and page loaders run in parallel for optimal performance.

## Native Platform Behavior

Loaders work similarly on native platforms as they do on web:

- **SSG routes**: Data is fetched at compile-time and included in the bundle
- **SSR routes**: Data is fetched from the server on each load
- **SPA routes**: Data is fetched from the server on each load

This means native apps can use the same loader patterns as web apps, with data fetching behavior determined by the route's render mode rather than the platform.

Loaders and their imports are removed from client bundles, so you can access private information from within the loader. The data returned from the loader will be passed to the client, and so should be clear of private information.

## Accessing Loader Data

There are two hooks available for accessing loader data:

### useLoader

The basic hook for accessing loader data:

```tsx
import { useLoader } from 'one'

export async function loader() {
  return {
    user: 'tamagui'
  }
}

export default function HomePage() {
  const data = useLoader(loader)

  return (
    <p>
      {data.user}
    </p>
  )
}
```

### useLoaderState

For advanced use cases requiring manual refetch or loading states:

```tsx
import { useLoaderState } from 'one'

export async function loader() {
  return {
    user: 'tamagui'
  }
}

export default function HomePage() {
  const { data, refetch, state } = useLoaderState(loader)

  return (
    <>
      <p>{data.user}</p>
      <button onClick={refetch} disabled={state === 'loading'}>
        Refresh
      </button>
    </>
  )
}
```

Both hooks are automatically type safe. See [useLoader](/docs/hooks-useLoader) and [useLoaderState](/docs/hooks-useLoaderState) for detailed documentation.

### refetchLoader

Re-run the loader for a path imperatively, outside of React. This is useful in event handlers or background tasks where hooks are not available:

```tsx
import { refetchLoader } from 'one'

async function refreshUser(id: string) {
  await refetchLoader(`/users/${id}`)
}
```

The promise resolves when the refetch completes. Components reading that loader through `useLoader` or `useLoaderState` re-render with the fresh data.

### onClientLoaderResolve

Runs once on the client after hydration preloads finish and before the first render. Register it in your root `_layout.tsx` when you need to observe the initial loader payload from outside React:

```tsx
import { onClientLoaderResolve } from 'one'

onClientLoaderResolve(({ loaderData, loaderProps }) => {
  console.log('hydrated loader data for', loaderProps?.path, loaderData)
})
```

It must be registered before the client finishes setup, so call it at the top level of your root layout module. Calling it after it already ran throws.

### useMatches

To access loader data from other routes (like a layout accessing page data, or vice versa), use `useMatches`:

```tsx
import { useMatches } from 'one'

function Breadcrumbs() {
  const matches = useMatches()
  // matches = [rootLayout, nestedLayout, currentPage]
  // each has: { routeId, pathname, params, loaderData }
  // loaderData is unknown, so narrow it before reading fields

  return (
    <nav>
      {matches.map(match => {
        const { title } = (match.loaderData ?? {}) as { title?: string }
        return (
          <a key={match.routeId} href={match.pathname}>
            {title ?? match.routeId}
          </a>
        )
      })}
    </nav>
  )
}
```

See [useMatches](/docs/hooks-useMatches) for detailed documentation.

## Layout Loaders

Layouts can export a `loader` function just like pages:

```tsx fileName=app/docs/_layout.tsx
import { useLoader, Slot } from 'one'

export async function loader() {
  return {
    navItems: await fetchDocsNav()
  }
}

export default function DocsLayout() {
  const { navItems } = useLoader(loader)

  return (
    <div>
      <Sidebar items={navItems} />
      <Slot />
    </div>
  )
}
```

See [Layout Loaders](/docs/routing-layouts#layout-loaders) for more details.

## Loader arguments

Loaders receive a single argument object:

### params

 The `params` key will provide values from any dynamic route segments:

```tsx fileName=app/user/[id].tsx
export async function loader({ params }) {
  // for route /user/jamon params.id is a string "jamon"
  const user = await getUser(params.id)
  
  return {
    greet: `Hello ${user.name}`
  }
}
```

### path

The `path` key is the fully resolved pathname:

```tsx fileName=app/user/[id].tsx
export async function loader({ path }) {
  // if the route is /user/123 then path is "/user/123"
}
```

### request

Only for [`ssr` type routes](/docs/render-modes#ssr). This will pass the same [Web standard Request object](https://developer.mozilla.org/en-US/docs/Web/API/Request) as API routes.

## Accepted Return Types

Most JavaScript values, including primitives and objects, will be converted to JSON.

You may also return a `Response`:

```tsx
export async function loader({ params: { id } }) {
  const user = await db.users.findOne({ id })
  const body = JSON.stringify(user)
  return new Response(body, {
    headers: {
      'Content-Type': 'application/json',
    },
  })
}
```

## Throwing a Response

A handy pattern for loaders is throwing a response to end it early:

```tsx
export async function loader({ params: { id } }) {
  const user = await db.users.findOne({ id })

  if (!user) {
    throw Response.error()
  }

  // ... rest of function
}
```

## Redirects in Loaders

Use the [`redirect()`](/docs/helpers-redirect) helper in loaders to protect SSR routes. This is the recommended approach for server-side auth guards — it prevents unauthorized content from ever reaching the client.

```tsx fileName=app/dashboard+ssr.tsx
import { redirect, useLoader } from 'one'
import type { LoaderProps } from 'one'

export async function loader({ request }: LoaderProps) {
  const session = await getSession(request)

  if (!session) {
    throw redirect('/login')
  }

  return { user: session.user }
}

export default function Dashboard() {
  const { user } = useLoader(loader)
  return <Text>Welcome, {user.name}</Text>
}
```

Both `throw redirect()` and `return redirect()` work. `throw` is often cleaner since it stops execution immediately and makes it obvious the remaining code won't run.

### How Loader Redirects Work

During a direct page load, `redirect()` returns a standard HTTP 302 response — the browser redirects normally.

During client-side `<Link>` navigation, loaders run on the server and the client fetches the result as a JavaScript module. Without special handling, the browser's module loader would silently follow a 302, try to parse the HTML login page as JavaScript, and fail. One handles this differently:

1. The server detects the redirect response from the loader
2. Instead of returning a raw 302, it transforms the response into a JS module containing redirect metadata
3. The client receives the metadata and detects the redirect signal before rendering anything
4. The client navigates to the redirect target using a `REPLACE` action — the protected page never appears in the history

This means **no sensitive data reaches the client** when a redirect occurs. The server only sends back the redirect path, not the page content or loader data.

### Redirects at Build Time

For SSG routes, if a loader redirects during the build (e.g., no authenticated request is available), One generates a static redirect file. In production with a runtime server, the server re-evaluates the loader on each request, so authenticated users see the real page while unauthenticated users get redirected.

> Use `+ssr` routes for protected pages so loaders run on every request. SSG routes evaluate loaders once at build time, which won't have access to per-request auth state.

See the [Loader Redirects guide](/docs/guides-loader-redirects) for a complete walkthrough and the [redirect helper](/docs/helpers-redirect) for the API reference.

## Setting Response Headers

Use `setResponseHeaders` to set HTTP headers on the response from within your loader. This is useful for caching, cookies, and custom headers.

### Cache Headers (ISR)

Set cache headers to enable [Incremental Static Regeneration](/docs/guides-isr) - serve SSR pages with static-like performance:

```tsx
import { setResponseHeaders, useLoader } from 'one'

export async function loader({ params }) {
  // Cache at CDN for 1 hour, serve stale while revalidating for up to 1 day
  await setResponseHeaders((headers) => {
    headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400')
  })

  const post = await fetchPost(params.slug)
  return { post }
}
```

### Cookies

Set cookies by appending `Set-Cookie` headers:

```tsx
import { setResponseHeaders } from 'one'

export async function loader({ params }) {
  await setResponseHeaders((headers) => {
    headers.append('Set-Cookie', `visited=${params.slug}; Path=/; HttpOnly`)
  })

  return { /* ... */ }
}
```

Read cookies from the request (SSR routes only):

```tsx
export async function loader({ request }) {
  const cookies = request?.headers.get('Cookie') || ''
  const session = cookies.match(/session=([^;]+)/)?.[1]

  if (!session) {
    throw redirect('/login')
  }

  return { user: await getUser(session) }
}
```

### Custom Headers

Add any custom headers:

```tsx
await setResponseHeaders((headers) => {
  headers.set('X-Custom-Header', 'value')
  headers.set('X-Request-Id', crypto.randomUUID())
})
```

See [setResponseHeaders](/docs/helpers-setResponseHeaders) for more details.

## Loader Cache

Routes can export a `loaderCache` function to deduplicate concurrent SSR loader calls. When multiple requests hit the same route at the same time, only one loader execution runs and the result is shared across all waiting requests.

### Basic usage

Return a string key from `loaderCache` to coalesce concurrent requests with matching keys:

```tsx fileName=app/cards/[id]+ssr.tsx
import { useLoader } from 'one'
import { db } from '~/lib/db'

// concurrent requests with the same id share one loader call
export const loaderCache = (params: { id: string }) => params.id

export async function loader({ params }) {
  const card = await db.getCardWithListings(params.id)
  return { card }
}

export default function CardPage() {
  const { card } = useLoader(loader)
  return <CardDetail card={card} />
}
```

Without `ttl`, only truly concurrent requests are coalesced — the cache entry is cleaned up immediately after the loader resolves.

### TTL-based caching

Return an object with `key` and `ttl` to cache the result for a time window (in milliseconds):

```tsx
export const loaderCache = (params: { id: string }) => ({
  key: params.id,
  ttl: 100, // cache for 100ms
})
```

With `ttl`, subsequent requests within the TTL window reuse the cached result without running the loader again.

### Auth-aware cache keys

`loaderCache` receives `(params, request)` so you can include auth headers or other request data in the key:

```tsx
export const loaderCache = (params, request) => {
  const userId = request?.headers.get('x-user-id') || 'anon'
  return `${params.id}:${userId}`
}
```

### When to use loaderCache

- **Expensive queries under load** — routes with heavy database queries that get concurrent traffic (popular pages, viral content)
- **Identical results across users** — routes where the loader result doesn't depend on auth. For auth-dependent loaders, include user identity in the key
- **High concurrency** — in benchmarks with 50 concurrent connections to the same route, `loaderCache` improved throughput from ~570 req/sec to ~2,000 req/sec (3.5x)

## Hot Reload for File Dependencies

During development, you can use `watchFile` to register file dependencies in your loader. When these files change, the loader data will automatically refresh without a full page reload.

This is useful for content-driven sites where loaders read from MDX files, JSON, or other data files:

```tsx
import { watchFile } from 'one'
import { readFile } from 'fs/promises'

export async function loader({ params: { slug } }) {
  const filePath = `./content/${slug}.mdx`

  // Register this file for HMR - when it changes, loader will re-run
  watchFile(filePath)

  const content = await readFile(filePath, 'utf-8')
  return { content }
}
```

`watchFile` is a no-op in production and on the client, so it's safe to use unconditionally. The path should be the same path you pass to `fs.readFile` or similar functions.

### Using @vxrn/mdx

The `@vxrn/mdx` package has built-in HMR support. When used in a One loader, MDX files will automatically trigger hot reload when changed:

```tsx
import { getMDXBySlug } from '@vxrn/mdx'

export async function loader({ params: { slug } }) {
  // HMR is automatic - no watchFile needed
  const { frontmatter, code } = await getMDXBySlug('./content', slug)
  return { frontmatter, code }
}
```

### For Library Authors

If you're building a library that reads files and want to support One's loader HMR, you can use the global hook pattern. This allows your library to work with One's HMR without depending on the `one` package:

```tsx
const WATCH_FILE_KEY = '__oneWatchFile'

function notifyFileRead(filePath: string): void {
  const watchFile = globalThis[WATCH_FILE_KEY] as ((path: string) => void) | undefined
  if (watchFile) {
    watchFile(filePath)
  }
}

// Call this whenever you read a file
export function readContent(filePath: string) {
  notifyFileRead(filePath)
  return fs.readFileSync(filePath, 'utf-8')
}
```

When One is present, it registers the `watchFile` implementation on the global object. Your library checks for it and calls it if available - no coupling required. The `@vxrn/mdx` package exports `notifyFileRead` if you prefer to import it directly.

## Route Validation

One provides two ways to validate routes before navigation: `validateParams` for schema-based validation and `validateRoute` for async validation logic.

### validateParams

Export a schema to validate route params before navigation. Works with Zod, Valibot, or custom functions:

```tsx
import { z } from 'zod'

// Validate that id is a UUID
export const validateParams = z.object({
  id: z.string().uuid('Invalid ID format')
})

export default function UserPage({ params }) {
  // params.id is guaranteed to be a valid UUID
  return <UserDetail userId={params.id} />
}
```

If validation fails, `validateParams` throws a `ParamValidationError` (with `params` and `issues` fields), navigation is blocked and an error is shown in the Error Panel (Alt+E).

For Zod schemas, wrap with `zodParamValidator` so the validator keeps its inferred output type:

```tsx
import { zodParamValidator } from 'one'
import { z } from 'zod'

export const validateParams = zodParamValidator(z.object({
  id: z.string().uuid('Invalid ID format'),
}))
```

The related types are exported from `one`: `ParamValidator`, `InferParamInput`, `InferParamOutput`, and `ParamValidationError`.

### validateRoute

For async validation (like checking if a resource exists), export a `validateRoute` function:

```tsx
export async function validateRoute({ params, search, pathname }) {
  // Check if the product exists before allowing navigation
  const exists = await checkProductExists(params.slug)

  if (!exists) {
    return {
      valid: false,
      error: 'Product not found',
      details: { slug: params.slug }
    }
  }

  return { valid: true }
}

export default function ProductPage({ params }) {
  // We only get here if the product exists
  return <ProductDetail slug={params.slug} />
}
```

The function receives `{ params, search, pathname, href }` (see `ValidateRouteProps`) and must return `{ valid: true }` or `{ valid: false, error, details? }` (see `ValidationResult`). It can also return nothing, which counts as valid. The related handler type is `RouteValidationFn`.

### validateParams vs validateRoute

Both can validate routes, but they serve different purposes:

| Feature | validateParams | validateRoute |
|---------|----------------|---------------|
| Runs on | Client | Client |
| Use case | Schema validation | Async checks |

**Use `validateParams`** for simple schema validation (UUIDs, formats, types).

**Use `validateRoute`** for async checks on the client (API calls, existence checks).

For server-side auth and redirects, use your `loader` function and throw a redirect response.

### useValidationState

Access validation state from any component:

```tsx
import { useValidationState } from 'one'

function NavigationStatus() {
  const { status, error } = useValidationState()
  // status: 'idle' | 'validating' | 'error' | 'valid'
  // error is set only when status is 'error'

  if (status === 'validating') {
    return <Spinner />
  }

  if (status === 'error') {
    return <Alert>{error?.message}</Alert>
  }

  return null
}
```

The state also carries `lastValidatedHref`, the href that was last validated. The `error` is a `ParamValidationError` when `validateParams` fails, or a `RouteValidationError` (with a `details` field) when `validateRoute` returns `{ valid: false }`.

---

### Apps Onestack.Dev Data Docs Routing Middlewares (`apps/onestack.dev/data/docs/routing-middlewares.mdx`)

---
title: Middlewares
---

Middlewares let you intercept requests before they reach your routes. They're useful for authentication, logging, redirects, adding headers, and more.

Place a `_middleware.ts` file anywhere inside your `app` directory to create a middleware.

## File Structure

<RouteTree
  routes={[
    { name: '_layout.tsx', description: 'Wraps all files in this directory and below' },
    { name: '_middleware.ts', description: 'Middleware for all "/" routes' },
    { name: 'index.tsx', description: 'Matches "/"' },
    {
      name: 'blog',
      children: [
        { name: '_middleware.ts', description: 'Middleware for all "/blog" routes' },
        { name: 'index.tsx', description: 'Matches "/blog"' },
      ],
    },
    {
      name: 'api',
      children: [
        { name: '_middleware.ts', description: 'Middleware for all "/api" routes' },
        { name: 'users+api.ts', description: 'API route at "/api/users"' },
      ],
    },
  ]}
/>

Middlewares are **hierarchical** - they run from the root down to the most specific matching route. In the example above, a request to `/blog` would run:

1. `app/_middleware.ts` first
2. `app/blog/_middleware.ts` second
3. Then the route handler

## Basic Usage

```tsx fileName=app/_middleware.ts
import { createMiddleware } from 'one'

export default createMiddleware(async ({ request, next }) => {
  console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)

  const response = await next()

  return response
})
```

## API Reference

The `createMiddleware` function accepts a middleware handler and provides type safety:

```tsx
import { createMiddleware, type Middleware } from 'one'

const handler: Middleware = async ({ request, next, context }) => {
  // request: the standard Web API Request
  // next: calls the next middleware in the chain, then the route
  // context: a mutable object shared across all middlewares in the chain
  return await next()
}

export default createMiddleware(handler)
```

### Props

| Prop      | Type                                                                  | Description                                               |
| --------- | --------------------------------------------------------------------- | --------------------------------------------------------- |
| `request` | [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) | The incoming request object                               |
| `next`    | `() => Promise<Response \| void \| null>`                             | Calls the next middleware in the chain, then the route    |
| `context` | `MiddlewareContext`                                                   | Mutable object shared across all middlewares in the chain |

`MiddlewareContext` accepts any key. Declare your own keys on it to type them, as shown in [Sharing Data with Context](#sharing-data-with-context).

### Return Values

| Return                | Effect                                                          |
| --------------------- | --------------------------------------------------------------- |
| `Response`            | Stops the chain and returns this response immediately           |
| `await next()`        | Continues to the next middleware/route and returns its response |
| `null` or `undefined` | Continues the chain (same as calling `next()`)                  |

## Common Patterns

### Redirects

Redirect users based on conditions:

```tsx fileName=app/_middleware.ts
import { createMiddleware } from 'one'

export default createMiddleware(async ({ request, next }) => {
  const url = new URL(request.url)

  // Redirect old paths to new paths
  if (url.pathname === '/old-page') {
    return Response.redirect(new URL('/new-page', url.origin), 301)
  }

  // Redirect to HTTPS in production
  if (url.protocol === 'http:' && process.env.NODE_ENV === 'production') {
    url.protocol = 'https:'
    return Response.redirect(url, 301)
  }

  return await next()
})
```

### Adding Headers

Modify response headers for CORS, caching, security, etc:

```tsx fileName=app/api/_middleware.ts
import { createMiddleware, setResponseHeaders } from 'one'

export default createMiddleware(async ({ request, next }) => {
  // Add CORS headers using setResponseHeaders helper
  setResponseHeaders((headers) => {
    headers.set('Access-Control-Allow-Origin', '*')
    headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  })

  // Handle preflight requests
  if (request.method === 'OPTIONS') {
    return new Response(null, { status: 204 })
  }

  return await next()
})
```

<Notice>
  The middleware above runs identically in `one dev` and `one serve` — the same `OPTIONS` branch answers preflight requests in both. One configures Vite's dev server with `server.cors.preflightContinue: true` so that Vite's built-in cors middleware doesn't intercept preflight before your middleware sees it. If you override `server.cors` in your `vite.config.ts`, keep `preflightContinue: true`. See [API Routes → Dev and prod parity](/docs/routing-api-routes#dev-and-prod-parity) for details.
</Notice>

Or modify the response directly:

```tsx fileName=app/_middleware.ts
import { createMiddleware } from 'one'

export default createMiddleware(async ({ request, next }) => {
  const response = await next()

  // Add security headers to all responses
  response?.headers.set('X-Content-Type-Options', 'nosniff')
  response?.headers.set('X-Frame-Options', 'DENY')

  return response
})
```

### Logging

Log requests for debugging or analytics:

```tsx fileName=app/_middleware.ts
import { createMiddleware } from 'one'

export default createMiddleware(async ({ request, next }) => {
  const start = Date.now()
  const url = new URL(request.url)

  const response = await next()

  const duration = Date.now() - start
  console.log(`${request.method} ${url.pathname} - ${response?.status} (${duration}ms)`)

  return response
})
```

### Sharing Data with Context

Pass data between the middlewares in a request's chain. `context` starts empty for each request and accepts any key; declare the keys you use on `MiddlewareContext` to type them:

```tsx fileName=app/_middleware.ts
import { createMiddleware } from 'one'

declare module 'one' {
  interface MiddlewareContext {
    requestId?: string
    timestamp?: number
    user?: { id: string }
  }
}

export default createMiddleware(async ({ request, next, context }) => {
  // Parse and validate request data once
  context.requestId = crypto.randomUUID()
  context.timestamp = Date.now()

  // Add user info if authenticated
  const token = request.headers.get('authorization')
  if (token) {
    context.user = await getUserFromToken(token)
  }

  return await next()
})
```

Access the context in nested middlewares:

```tsx fileName=app/api/_middleware.ts
import { createMiddleware } from 'one'

export default createMiddleware(async ({ request, next, context }) => {
  // Context from parent middleware is available
  console.log(`Request ${context.requestId} from user ${context.user?.id}`)

  return await next()
})
```

### Early Response (Interception)

Return a response immediately without calling `next()` to intercept the request:

```tsx fileName=app/_middleware.ts
import { createMiddleware } from 'one'

export default createMiddleware(async ({ request, next }) => {
  const url = new URL(request.url)

  // Return cached response for specific paths
  if (url.pathname === '/api/health') {
    return Response.json({ status: 'ok', timestamp: Date.now() })
  }

  // Block certain paths
  if (url.pathname.startsWith('/admin') && !isAdmin(request)) {
    return new Response('Forbidden', { status: 403 })
  }

  return await next()
})
```

### Post-Processing Responses

Modify responses after the route has processed:

```tsx fileName=app/_middleware.ts
import { createMiddleware } from 'one'

export default createMiddleware(async ({ request, next }) => {
  const response = await next()

  // Handle 404s with a custom response
  if (!response || response.status === 404) {
    return Response.json(
      { error: 'Not found', path: new URL(request.url).pathname },
      { status: 404 }
    )
  }

  return response
})
```

## Execution Order

Middlewares execute in order from root to leaf:

```
Request to /blog/post/123
         ↓
1. app/_middleware.ts (runs first)
         ↓
2. app/blog/_middleware.ts (runs second)
         ↓
3. app/blog/post/_middleware.ts (runs third)
         ↓
4. Route handler executes
         ↓
   Responses bubble back up through each middleware
```

Each middleware can:

- **Intercept**: Return a response early without calling `next()`
- **Pass through**: Call `next()` to continue the chain
- **Post-process**: Modify the response after calling `next()`

## Platform Support

| Platform           | Support                     |
| ------------------ | --------------------------- |
| Node.js server     | Full support                |
| Vercel             | Full support                |
| Cloudflare Workers | Full support                |
| Static export      | Not applicable              |
| React Native       | Not supported (server-only) |

Middlewares are server-only and run on every request. They do not run during static site generation or on native platforms.

## Middleware vs Loader Redirects

Middleware redirects and loader redirects serve different purposes:

| | Middleware Redirects | Loader Redirects |
|---|---|---|
| **Best for** | URL rewrites, HTTPS enforcement, path-level access control | Protecting page data, auth guards for SSR pages |
| **Runs on** | Every request before the route | When loader data is fetched |
| **Client-side nav** | Not involved (middleware only runs on server requests) | Server intercepts redirect and sends metadata to client, preventing flash of protected content |
| **Data protection** | Returns early before route processes | Prevents sensitive loader data from reaching the client |

For protecting SSR routes with auth, prefer [loader redirects](/docs/routing-loader#redirects-in-loaders) — they handle both direct page loads and client-side `<Link>` navigation, ensuring no unauthorized data leaks. Use middleware redirects for request-level concerns like URL rewrites, HTTPS enforcement, and API route protection.

See the [Loader Redirects guide](/docs/guides-loader-redirects) for a complete walkthrough.

## Related

- [Loaders](/docs/routing-loader) - Load data for routes
- [Loader Redirects](/docs/guides-loader-redirects) - Protecting routes with loader redirects
- [API Routes](/docs/render-modes#api) - Create API endpoints
- [setResponseHeaders](/docs/helpers-setResponseHeaders) - Helper for setting headers

---

### Apps Onestack.Dev Data Docs Routing Navigation (`apps/onestack.dev/data/docs/routing-navigation.mdx`)

---
title: Navigation
---

When you want to move between a page, you have a few options. Examples on this page will reference this routing structure:

<RouteTree
  routes={[
    { name: '_layout.tsx', description: 'Wraps all files in this directory and below' },
    { name: 'index.tsx', description: 'Matches "/"' },
    { name: 'blog.tsx', children: [
      { name: 'index.tsx', description: 'Matches "/blog"' },
      { name: '[slug].tsx', description: 'Matches a single sub-path of "/blog", like "/blog/hello"' }
    ] },
  ]}
/>

## Link

You can use the Link component as follows:

```tsx fileName=app/index.tsx
import { Link } from 'one'

export default function HomePage() {
  return (
    <>
      <Link href="/blog">
        Go to the blog
      </Link>

      <Link href="/blog/introducing-one">
        Go to the Introducing One article
      </Link>
    </>
  )
}
```

`Link` takes an `href` typed from your file system routes (or a plain string if you turn off typed file system routing), plus `replace`, `push`, `asChild`, `className`, `onPress`, `mask`, and the web-only anchor props `target`, `rel` and `download`. See the full prop list on the [`<Link />`](/docs/components-Link) page.

## useLinkTo

This hook will generate both an `href` string and an `onPress` function, see [the useLinkTo docs](/docs/hooks-useLinkTo) for how to use it.

## useRouter

This hook returns a static object used for routing. It will never update, and is only used for imperatively controlling the router. Useful for programmatically controlling the routing, and choosing between a `push` or `replace`.

See [the useRouter documentation page](/docs/hooks-useRouter) for more.

## removeParams

`removeParams` strips named params from every route in a React Navigation state, at every nesting level. It returns a new state object and leaves the input untouched:

```tsx
import { removeParams } from 'one'

// drop one-off params before persisting or restoring navigation state
const cleanState = removeParams(state, ['previewToken'])
```

---

### Apps Onestack.Dev Data Docs Routing Typed Routes (`apps/onestack.dev/data/docs/routing-typed-routes.mdx`)

---
title: Typed Routes
---

One provides full TypeScript support for route parameters and loader props, automatically generating types from your file system routes.

## Overview

When you create route files with dynamic segments like `[slug]` or `[...rest]`, One automatically generates TypeScript types that make your route parameters fully type-safe.

### Generated Types

All route types are generated in `app/routes.d.ts`. This is the shape the generator emits (including the import at the top, which the `SingleRoutePart` reference needs):

```typescript
import type { OneRouter } from 'one'

declare module 'one' {
  export namespace OneRouter {
    export interface __routes<T extends string = string> extends Record<string, unknown> {
      StaticRoutes: '/' | '/about' | '/docs'
      DynamicRoutes: `/docs/${OneRouter.SingleRoutePart<T>}`
      DynamicRouteTemplate: '/docs/[slug]'
      IsTyped: true
      RouteTypes: {
        '/docs/[slug]': RouteInfo<{ slug: string }>
      }
    }
  }
}

/**
 * Helper type for route information
 */
type RouteInfo<Params = Record<string, never>> = {
  Params: Params
  LoaderProps: { path: string; search?: string; subdomain?: string; params: Params; request?: Request }
}
```

`SingleRoutePart` is exported from the `OneRouter` namespace in `one`, and matches a single path segment. Do not hand-edit this file, regenerate it with `npx one generate-routes` instead.

## Using Typed Routes

### With `createRoute` Helper

The recommended approach is to use the `createRoute` helper, which provides fully typed methods:

```tsx fileName=app/docs/[slug].tsx
import { createRoute } from 'one'

const route = createRoute<'/docs/[slug]'>()

export const loader = route.createLoader(async ({ params }) => {
  // params.slug is string - fully typed!
  return { doc: await fetchDoc(params.slug) }
})

export default function DocPage() {
  const params = route.useParams()
  // params.slug is string - fully typed!
  return <div>Viewing: {params.slug}</div>
}
```

### Manual Type Usage

You can also use the `RouteType` helper to get types for any route:

```tsx fileName=app/docs/[slug].tsx
import type { RouteType } from 'one'

type Route = RouteType<'/docs/[slug]'>

export const loader = ({ params }: Route['LoaderProps']) => {
  // params.slug is fully typed!
  return { doc: await fetchDoc(params.slug) }
}
```

## Auto-Generation

Enable the experimental `typedRoutesGeneration` option to automatically insert type helpers into your route files:

```tsx fileName=vite.config.ts
export default {
  plugins: [
    one({
      router: {
        experimental: {
          typedRoutesGeneration: 'runtime', // or 'type'
        },
      },
    }),
  ],
}
```

### Generation Modes

- **`false`** (default): No auto-generation, manually add types yourself
- **`'type'`**: Auto-inserts type-only helpers:

```tsx
import type { RouteType } from 'one'
type Route = RouteType<'/your/[route]'>
```

- **`'runtime'`**: Auto-inserts runtime helpers:

```tsx
import { createRoute } from 'one'
const route = createRoute<'/your/[route]'>()
```

The insertion happens automatically when route files are created or modified, with proper spacing and without modifying your existing loader code.

## CLI Command

Generate route types manually using the One CLI:

```bash
npx one generate-routes                  # Generate types only
npx one generate-routes --typed=runtime  # Generate + inject runtime helpers
npx one generate-routes --typed=type     # Generate + inject type helpers
```

**Note:** The CLI command requires `--typed` to inject helpers. The `typedRoutesGeneration` config option works automatically during development (watches files and injects on changes), while the CLI flag gives you manual control when needed.

## Complex Routes

### Multiple Parameters

```tsx fileName=app/blog/[category]/[year]/[slug].tsx
import { createRoute } from 'one'

const route = createRoute<'/blog/[category]/[year]/[slug]'>()

export const loader = route.createLoader(({ params }) => {
  // All params are fully typed!
  const { category, year, slug } = params
  // category: string, year: string, slug: string

  return { post: await fetchPost(category, year, slug) }
})
```

### Catch-All Routes

```tsx fileName=app/files/[...path].tsx
import { createRoute } from 'one'

const route = createRoute<'/files/[...path]'>()

export const loader = route.createLoader(({ params }) => {
  // params.path is string[] - fully typed!
  const filePath = params.path.join('/')
  return { file: await fetchFile(filePath) }
})
```

## Benefits

- **Type Safety**: Catch route parameter errors at compile time
- **Better DX**: Auto-complete for route params in loaders
- **Clean Intellisense**: Shows `{ slug: string }` instead of complex type aliases
- **Refactor-Friendly**: Rename routes and TypeScript catches all usages
- **Zero Runtime Cost**: Types are erased during build

## How It Works

1. One scans your `app` directory for route files
2. Extracts dynamic segments like `[slug]` and `[...rest]`
3. Generates TypeScript types in `app/routes.d.ts`
4. TypeScript provides autocomplete and type checking

The `RouteType` helper looks up the generated types from `routes.d.ts` and provides dot-notation access to `Params` and `LoaderProps`.

## Environment Variables

One also provides TypeScript types for [preset environment variables](/docs/environment#preset-environment-variables). Add this to any `.d.ts` file in your project:

```ts
/// <reference types="one/env" />
```

This gives you typed access to `import.meta.env.VITE_NATIVE`, `process.env.VITE_ENVIRONMENT`, and other One environment variables.

---

### Apps Onestack.Dev Data Docs Routing (`apps/onestack.dev/data/docs/routing.mdx`)

---
title: Routing
---

The `app` directory is where One handles universal file system routing. Every file you create in it will be turned into a route, except for files named `_layout` or `_middleware` (or files that match patterns in your [`ignoredRouteFiles`](/docs/configuration#routerignoredroutefiles) configuration, if you have that). One only supports file system routes at this time, but we have a [discussion](https://github.com/onejs/one/discussions/108) for those who want to help us figure out a configuration-based approach.

Here's an example:

<RouteTree
  routes={[
    { name: '_layout.tsx', description: 'Wraps all files in this directory and below' },
    { name: 'index.tsx', description: 'Matches "/"' },
    { name: 'blog', children: [
      { name: 'index.tsx', description: 'Matches "/blog"' },
      { name: '[slug].tsx', description: 'Matches a single sub-path of "/blog", like "/blog/hello"' },
      { name: '[...rest].tsx', description: 'Matches all sub-paths, like "/blog/hello/world"' }
    ] },
  ]}
/>

One routes support the popular feature of nested layouts, via `_layout.tsx` files, and parallel render-as-you-fetch data loading via `export loader`.

## Route Structure

All `.tsx` files in the `app` dir are used to match your routes, besides layouts. Each individual file is referred to as a "page" to disambiguate them from the term "route," which we use to refer to the actual URL state in a browser on web (or React Navigation state on native).

All pages must export a React component to be rendered. You can choose between two ways to export it: `export default` or plain `export`. If you don't `default` export, we look for the first `export` that is capitalized. The reason we support this is simple - the React Refresh library doesn't support hot reloading default exports. We're looking into modifying it to support them for route files, but until then you may want to simply use `export` to maintain a nice hot reloading experience.

### Simple Routes

A simple page that matches the `/` route and will render on native and web:

```tsx fileName=app/index.tsx
import { Text } from 'react-native'

export default function HomePage() {
  return (
    <Text>Hello world</Text>
  )
}
```

If you are targeting web-only, you may use HTML elements:

```tsx fileName=app/index.tsx
export default function HomePage() {
  return (
    <div>Hello world</div>
  )
}
```

All `index` files match against `/`, so `app/index.tsx` matches to your root `/` route. You can give your page a name as well, for example `app/about.tsx` will match the `/about` route.

### Dynamic Routes

The segments of a route, for example `/hello/world`, are referred to as "route parameters" or just "params", where "hello" and "world" are each a single parameter. Pages can match against parameters dynamically in two ways: matching a single parameter, or matching all parameters below them.

#### Single Parameter Routes

One uses brackets to match against a single dynamic parameter: `./blog/[slug].tsx` matches `/blog/post-one` and `/blog/post-two`:

<RouteTree
  routes={[
    { name: 'blog.tsx', children: [
      { name: 'index.tsx', description: `Matches "/blog"` },
      { name: '[slug].tsx', description: `Matches a single sub-path of "/blog", like "/blog/hello"` }
    ] },
  ]}
/>

One will match `./blog/[slug].tsx` to any `/blog/*` route, and pass in the parameters to the route as follows:

```tsx
import { useParams } from 'one'

export function loader({ params }) {
  // params.slug will be a string matching the URL parameter
}

export default function BlogPostPage() {
  const params = useParams()
  // params.slug will be a string matching the URL parameter
}
```

#### Rest Parameter Routes

Much like JavaScript supports rest parameters, One supports rest parameter routes which are defined using the `[...rest].tsx` syntax.

<RouteTree
  routes={[
    { name: 'catalog.tsx', children: [
      { name: 'index.tsx', description: `Matches "/catalog"` },
      { name: '[...rest].tsx', description: `Matches all sub-paths of "/catalog", like "/catalog/a/b/c"` }
    ] },
  ]}
/>

In the case where a user navigates to `/catalog/a/b/c`, the `[...rest].tsx` route would receive a params prop as follows:

```tsx
import { useParams } from 'one'

export function loader({ params }) {
  // params.rest is an array ['a', 'b', 'c']
}

export default function BlogPostPage() {
  const params = useParams()
  // params.rest is an array ['a', 'b', 'c']
}
```

### Not found routes

To have a custom 404 page, you can create a `+not-found.tsx` route in any folder. It will act as the same as a `[...rest].tsx` route in that it will catch all sub-routes for that folder.

Simply export a React component (default or named) from your not found page and it will render with a 404 status code.

<RouteTree
  routes={[
    { name: 'catalog.tsx', children: [
      { name: 'index.tsx', description: `Matches "/catalog"` },
      { name: '+not-found.tsx', description: `Matches all non-matching sub-paths of "/catalog" with a 404 status code` }
    ] },
  ]}
/>

## Render Modes

You can choose a rendering strategy on a per-page basis using a filename suffix. For more on these modes, [see the next documentation page, Render Modes](/docs/render-modes).

- `route+ssg.tsx` - Matches `/route`, but will render the page as a SSG route.
- `route+spa.tsx` - Matches `/route`, but will render the page as a SPA route.
- `route+ssr.tsx` - Matches `/route`, but will render the page as a SSR route.
- `route+api.ts` - Matches `/route`, but will render the page as an API route.

Layouts also support render mode suffixes (`_layout+ssg.tsx`, `_layout+ssr.tsx`). This is especially useful for SPA-heavy apps - when a parent layout has SSG or SSR mode, the layout shell renders on the server while page content remains client-rendered. See [Layout Render Modes](/docs/routing-layouts#layout-render-modes) for details.

## Routing per-platform

You can target a specific platform using the same specific extension convention as React Native - ie, using `.web.tsx`, `.native.tsx`, `.ios.tsx`, or `.android.tsx` as the last part of the filename.

This lets you diverge the routing based on the platform. For example:

<RouteTree
  routes={[
    { name: 'index.tsx', description: `Matches "/" on native"` },
    { name: 'index.web.tsx', description: `Matches "/" on web` },
    { name: 'blog.web.tsx', description: `Matches "/blog" on web, on native there will be no route` },
  ]}
/>

## Groups

You can define a group by naming a folder with surrounding parenthesis:


<RouteTree
  routes={[
    { name: '_layout.tsx', description: '' },
    { name: '(blog)', children: [
      { name: '_layout.tsx', description: 'This layout will nest inside the above layout' },
      { name: 'blog.tsx', description: 'Matches "/blog"' },
      { name: '[slug].tsx', description: 'Matches a single sub-path of "/blog", like "/blog/hello"' },
    ] },
  ]}
/>

Groups are useful for a couple reasons:

- They're useful to organize similar things without forcing you to nest URL segments.
- They let you nest layouts, also without nesting segments.


## Types

One generates types for your routes, so that you get type checking on a variety of things like the [Link component](/docs/components-Link).

The types are automatically generated into `app/routes.d.ts` during development. This file is committed to your repository and TypeScript automatically discovers it.

You can also manually regenerate route types at any time:

```bash
yarn one generate-routes
```

If your app directory is not named `app`, you can specify it:

```bash
yarn one generate-routes --appDir=src
```

---

### Apps Onestack.Dev Data Docs Status (`apps/onestack.dev/data/docs/status.mdx`)

---
title: Status
description: One is stable for web and native
---

This is a live document covering the high level status of the various parts of One. We don't cover all areas, just high level stable, and all developing or unstable sub-areas.

Broadly: web and native are stable.

---

#### Stages, from least to most stable

- <Status is="early" /> · <Status is="developing" /> · <Status is="mostly-stable" /> ·
  <Status is="stable" />
- Features in <Status is="developing" /> or later are available to use.

---

### OS Support

- Mac <Status is="stable" />
- Linux <Status is="stable" />
- Windows <Status is="early" /> (not officially supported; the CLI prints a warning)

---

### Web <Status is="stable" />

Web is production ready.

- Local development <Status is="stable" />
- Production builds <Status is="stable" />
- Not Found <Status is="stable" />

---

### Native <Status is="stable" />

One bundles native apps with [Rolldown](https://rolldown.rs), the same bundler as web, in development and production.

- Local development <Status is="stable" /> (HMR via React Refresh)
- Production builds <Status is="stable" /> (tree shaking)
- Assets <Status is="stable" />
- Symbolication <Status is="mostly-stable" />

---

### Routing <Status is="stable" />

SSG, SPA, and SSR are all stable.

- SSG <Status is="stable" />
- SPA <Status is="stable" />
- SSR <Status is="stable" />

---

### Deployment <Status is="stable" />

- Node (one serve) <Status is="stable" />
- Vercel <Status is="stable" />
- Cloudflare <Status is="mostly-stable" />

---

### Components

- Slot <Status is="stable" />
- Stack <Status is="stable" />
- Tabs <Status is="stable" />
- Drawer <Status is="stable" />

---

### One Native <Status is="stable" />

See the [Native docs](/native) for details.

- Native components (SwiftUI, Compose, shared) <Status is="stable" />
- Platform APIs <Status is="stable" />
- Swift and Kotlin imports <Status is="stable" />
- Over-the-air updates <Status is="stable" />
- Color API <Status is="stable" />
- Zoom Transitions <Status is="stable" />
- Toolbar <Status is="stable" />
- Menu Actions <Status is="stable" />
- SplitView <Status is="mostly-stable" />

---

--- METRICS ---
- Files Extracted: 95
- Estimated Token Budget: ~113652 tokens
- Canonical Reference: https://codewiki.google/github.com/onejs/one
