Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/reatom/reatom/llms.txt

Use this file to discover all available pages before exploring further.

Routing

Reatom’s routing system provides a powerful, type-safe solution for building single-page applications with nested routes, data loading, and automatic parameter validation.

Core Concepts

Reatom routing is built on these principles:
  • Declarative Routes - Define routes with path patterns and schemas
  • Type Safety - Full TypeScript inference for route parameters
  • Data Loading - Automatic data fetching with loader functions
  • Nested Routes - Hierarchical route composition
  • Framework Agnostic - Works with React, Vue, Lit, or vanilla JS

Quick Start

Basic Routes

import { reatomRoute } from '@reatom/core/routing'

// Create routes
const homeRoute = reatomRoute('')
const aboutRoute = reatomRoute('about')
const userRoute = reatomRoute('users/:userId')

// Navigate
homeRoute.go()
aboutRoute.go()
userRoute.go({ userId: '123' })

// Check if route is active
if (userRoute()) {
  const params = userRoute() // { userId: '123' }
  console.log(params.userId)
}

Route Patterns

Path Parameters

Define dynamic segments with :paramName:
// Required parameter
const userRoute = reatomRoute('users/:userId')
userRoute.go({ userId: '123' }) // /users/123

// Optional parameter
const postRoute = reatomRoute('posts/:postId?')
postRoute.go()                    // /posts
postRoute.go({ postId: 'abc' })   // /posts/abc

// Multiple parameters
const commentRoute = reatomRoute('posts/:postId/comments/:commentId')
commentRoute.go({ 
  postId: '1', 
  commentId: '42' 
}) // /posts/1/comments/42

Search Parameters

Define query parameters with a schema:
import { z } from 'zod'

const searchRoute = reatomRoute({
  path: 'search',
  search: z.object({
    q: z.string().optional(),
    page: z.string().transform(Number).default('1'),
    sort: z.enum(['asc', 'desc']).optional(),
  }),
})

searchRoute.go({ 
  q: 'reatom', 
  page: 2,
  sort: 'desc' 
})
// URL: /search?q=reatom&page=2&sort=desc

const params = searchRoute()
params?.q      // string | undefined
params?.page   // number
params?.sort   // 'asc' | 'desc' | undefined

Search-Only Routes

Create routes that only affect query parameters:
const dialogRoute = reatomRoute({
  search: z.object({
    dialog: z.enum(['login', 'signup']).optional(),
  }),
})

// Preserves current pathname, only updates search params
dialogRoute.go({ dialog: 'login' })
// URL: /current-path?dialog=login

Parameter Validation

Schema Validation

Use Standard Schema (Zod, Valibot, etc.) for validation:
import { z } from 'zod'

const userRoute = reatomRoute({
  path: 'users/:userId',
  params: z.object({
    userId: z.string().regex(/^\d+$/).transform(Number),
  }),
})

// Valid: converts string to number
userRoute.go({ userId: '123' })
const params = userRoute() // { userId: 123 } - number type!

// Invalid: won't match
userRoute.go({ userId: 'abc' }) // Route won't match
userRoute() // → null

Custom Validators

Use functions for custom validation:
const userRoute = reatomRoute({
  path: 'users/:userId',
  params: (params: { userId: string }) => {
    const id = parseInt(params.userId)
    return isNaN(id) ? null : { userId: id }
  },
})

Nested Routes

Creating Child Routes

Build hierarchical route structures:
// Parent route
const usersRoute = reatomRoute('users')

// Child routes
const userListRoute = usersRoute.reatomRoute('')
const userDetailRoute = usersRoute.reatomRoute(':userId')
const userEditRoute = userDetailRoute.reatomRoute('edit')

// Full paths:
// userListRoute   → /users
// userDetailRoute → /users/:userId
// userEditRoute   → /users/:userId/edit

Accessing Parent Parameters

Child routes inherit parent parameters:
const usersRoute = reatomRoute('users/:userId')
const postsRoute = usersRoute.reatomRoute('posts/:postId')

postsRoute.go({ userId: '1', postId: '42' })
// URL: /users/1/posts/42

const params = postsRoute()
params?.userId  // '1'
params?.postId  // '42'

Route State

Match vs Exact

const usersRoute = reatomRoute('users')
const userRoute = usersRoute.reatomRoute(':userId')

// At URL: /users/123

usersRoute.match()  // → true (partial match)
userRoute.match()   // → true (exact match)

Route Parameters

const userRoute = reatomRoute('users/:userId')

// When matched
userRoute() // → { userId: '123' } | null

// When not matched
userRoute() // → null

Data Loading

Loader Functions

Automatically fetch data when routes become active:
import { wrap } from '@reatom/core'

const userRoute = reatomRoute({
  path: 'users/:userId',
  params: z.object({
    userId: z.string().transform(Number),
  }),
  async loader({ userId }) {
    const res = await wrap(fetch(`/api/users/${userId}`))
    return await wrap(res.json())
  },
})

// Access loaded data
const user = userRoute.loader.data()

// Loading state
const isLoading = !userRoute.loader.ready()

// Error handling
const error = userRoute.loader.error()

Loader State

Loaders are extended with withAsyncData:
// Loading states
userRoute.loader.pending()  // Number of pending loads
userRoute.loader.ready()    // Is load complete?

// Data and errors
userRoute.loader.data()     // Loaded data
userRoute.loader.error()    // Load error if any

// Detailed status
userRoute.loader.status()   // Full status tracking

// Retry
await wrap(userRoute.loader.retry())

Cascading Loaders

Parent loaders complete before child loaders:
const userRoute = reatomRoute({
  path: 'users/:userId',
  async loader({ userId }) {
    // Load user first
    const user = await wrap(fetchUser(userId))
    return user
  },
})

const postsRoute = userRoute.reatomRoute({
  path: 'posts',
  async loader({ userId }) {
    // userRoute.loader completes first
    // Then this loader runs
    const posts = await wrap(fetchUserPosts(userId))
    return posts
  },
})

Automatic Abort

Loaders are automatically aborted when:
  • Navigating to a different route
  • Route parameters change
  • Parent route becomes inactive
const searchRoute = reatomRoute({
  path: 'search',
  search: z.object({ q: z.string() }),
  async loader({ q }) {
    // Aborted if q changes before completion
    const res = await wrap(fetch(`/api/search?q=${q}`))
    return await wrap(res.json())
  },
})

searchRoute.go({ q: 'react' })  // Starts loader
searchRoute.go({ q: 'reatom' }) // Aborts previous, starts new

Component Rendering

Render Functions

Define components directly in routes:
import { html } from 'lit'

const layoutRoute = reatomRoute({
  render(self) {
    return html`
      <div class="layout">
        <header>My App</header>
        <main>${self.outlet().map(child => child)}</main>
      </div>
    `
  },
})

const homeRoute = layoutRoute.reatomRoute({
  path: '',
  render() {
    return html`<h1>Home Page</h1>`
  },
})

const aboutRoute = layoutRoute.reatomRoute({
  path: 'about',
  render() {
    return html`<h1>About Page</h1>`
  },
})

Outlet

Render active child routes:
const layoutRoute = reatomRoute({
  render(self) {
    const children = self.outlet() // Array<RouteChild>
    return html`
      <div>
        <nav>Navigation</nav>
        <main>${children.map(child => child)}</main>
      </div>
    `
  },
})

Exact Rendering

Only render on exact match:
const userRoute = reatomRoute({
  path: 'users/:userId',
  exactRender: true, // Only renders on exact /users/:userId
  render(self) {
    const { userId } = self()
    return html`<div>User ${userId}</div>`
  },
})

Programmatic Navigation

1
Using route.go()
2
const userRoute = reatomRoute('users/:userId')

// Navigate to route
userRoute.go({ userId: '123' })

// Replace history entry
userRoute.go({ userId: '456' }, true)
3
Using urlAtom
4
import { urlAtom } from '@reatom/core/web'

// Navigate to URL
urlAtom.go('/users/123')

// Replace history
urlAtom.set(new URL('/about', location.href), true)
5
Build URLs
6
const userRoute = reatomRoute('users/:userId')

// Get URL string
const url = userRoute.path({ userId: '123' })
// → "/users/123"

// Use in links
const link = html`<a href=${userRoute.path({ userId: '123' })}>View User</a>`

Advanced Patterns

404 Handling

import { is404 } from '@reatom/core/routing'
import { reatomComponent } from '@reatom/npm-react'

function App() {
  const [notFound] = useAtom(is404)
  
  if (notFound) {
    return <NotFoundPage />
  }
  
  return (
    <div>
      {layoutRoute.render()}
    </div>
  )
}

Protected Routes

const isAuthenticated = atom(false, 'isAuthenticated')

const protectedRoute = reatomRoute({
  path: 'dashboard',
  render(self) {
    if (!isAuthenticated()) {
      loginRoute.go()
      return null
    }
    return html`<Dashboard />`
  },
})

Route Guards

import { effect } from '@reatom/core'

const adminRoute = reatomRoute('admin/:section')

effect(() => {
  const params = adminRoute()
  
  if (params && !isAdmin()) {
    // Redirect if not admin
    homeRoute.go()
  }
})

Loading Indicators

import { isSomeLoaderPending } from '@reatom/core/routing'
import { reatomComponent } from '@reatom/npm-react'

function App() {
  const [isLoading] = useAtom(isSomeLoaderPending)
  
  return (
    <div>
      {isLoading && <LoadingBar />}
      <Router />
    </div>
  )
}
Use search-only routes for modals:
const modalRoute = reatomRoute({
  search: z.object({
    modal: z.enum(['login', 'signup', 'settings']).optional(),
  }),
})

function App() {
  const [params] = useAtom(modalRoute)
  
  return (
    <div>
      <MainContent />
      {params?.modal === 'login' && <LoginModal />}
      {params?.modal === 'signup' && <SignupModal />}
      {params?.modal === 'settings' && <SettingsModal />}
    </div>
  )
}

// Open modal without changing the page
modalRoute.go({ modal: 'login' })
function Breadcrumbs() {
  const routes = [
    { route: homeRoute, label: 'Home' },
    { route: usersRoute, label: 'Users' },
    { route: userRoute, label: 'User Details' },
  ]
  
  return (
    <nav>
      {routes.map(({ route, label }) => (
        route.match() && (
          <span key={label}>
            {route.exact() ? (
              <strong>{label}</strong>
            ) : (
              <a href={route.path(route())}>{label}</a>
            )}
          </span>
        )
      ))}
    </nav>
  )
}

Best Practices

1. Use Nested Routes

Organize routes hierarchically:
// ✓ Good - clear hierarchy
const usersRoute = reatomRoute('users')
const userRoute = usersRoute.reatomRoute(':userId')
const userEditRoute = userRoute.reatomRoute('edit')

// ✗ Bad - flat structure
const usersRoute = reatomRoute('users')
const userRoute = reatomRoute('users/:userId')
const userEditRoute = reatomRoute('users/:userId/edit')

2. Validate Parameters

Always validate and transform route parameters:
// ✓ Good - validated and typed
const userRoute = reatomRoute({
  path: 'users/:userId',
  params: z.object({
    userId: z.string().regex(/^\d+$/).transform(Number),
  }),
})

// ✗ Bad - no validation
const userRoute = reatomRoute('users/:userId')

3. Use Loaders for Data

Prefer loaders over manual fetching:
// ✓ Good - automatic loading and abort
const userRoute = reatomRoute({
  path: 'users/:userId',
  async loader({ userId }) {
    return await wrap(fetchUser(userId))
  },
})

// ✗ Bad - manual effect
const userRoute = reatomRoute('users/:userId')
effect(() => {
  const params = userRoute()
  if (params) fetchUser(params.userId)
})

4. Handle Errors

Always handle loader errors:
function UserPage() {
  const [user] = useAtom(userRoute.loader.data)
  const [error] = useAtom(userRoute.loader.error)
  const [ready] = useAtom(userRoute.loader.ready)
  
  if (!ready) return <Loading />
  if (error) return <Error error={error} />
  if (!user) return <NotFound />
  
  return <UserDetails user={user} />
}
Always use wrap() around promises in loaders to enable automatic abort handling.