Skip to content

subframe7536/solid-file-router

Repository files navigation

solid-file-router

Type safe file router for solid.js

Generate type safe route definition and virtual module that return @solidjs/router's RouteDefinition and <FileRouter />

ESM Only

Features

  • 📁 File-based routing - Automatically generates routes from your src/pages/** directory structure
  • 🔒 Type-safe - Full TypeScript support with generated type definitions for routes and path parameters
  • Vite integration - Works seamlessly with Vite as a plugin
  • 🎯 Flexible layouts - Support for _layout.tsx files to define nested layouts
  • 🛡️ Error boundaries - Built-in error handling with custom error components
  • 📦 Loading states - Optional loading components while data is being fetched

Getting Started

Installation

npm install solid-file-router
# or
yarn add solid-file-router
# or
bun add solid-file-router

Setup

  1. Add the Vite plugin to your vite.config.ts:
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
import { fileRouter } from 'solid-file-router/plugin'

export default defineConfig({
  plugins: [solid(), fileRouter()],
})
  1. Create your pages directory at src/pages/

  2. Create the app root (src/pages/_app.tsx): This serves as the root layout for your application.

import { createRoute } from 'solid-file-router'

export default createRoute({
  component: (props) => {
    return <div id="app-root">{props.children}</div>
  },
})
  1. Create your entry point (e.g., src/index.tsx):
import { render } from 'solid-js/web'
import { FileRouter } from 'virtual:routes'

render(() => <FileRouter base="/optional/base" />, document.getElementById('app')!)

Project Structure

Understanding the file structure is key to using the router effectively.

src/
  pages/
    _app.tsx              # App root (Required)
    index.tsx             # Matches: /
    about.tsx             # Matches: /about
    404.tsx               # Catch-all for unmatched routes

    # Nested Routes & Layouts
    blog/
      _layout.tsx         # Wraps all routes inside /blog/
      index.tsx           # Matches: /blog
      [id].tsx            # Matches: /blog/:id

    # Dynamic & Optional Params
    -[lang]/
      index.tsx           # Matches: /:lang?

    # Pathless Layouts (Logical grouping without URL change)
    (auth)/
      login.tsx           # Matches: /login
      register.tsx        # Matches: /register

    # Nested URLs without nested layouts
    path.to.some.url.tsx  # Matches: /path/to/some/url

  index.tsx               # Entry point
  routes.d.ts             # Auto-generated type definitions

API Reference & Examples

createRoute(config)

The core function to define route behavior. Must be the default export in every page file.

Parameters:

  • component (Required): Component to render.
  • preload (Optional): Async function to fetch data before rendering (@solidjs/router mechanism).
  • loadingComponent (Optional): Component shown while preload is pending.
  • errorComponent (Optional): Error Boundary component shown if rendering or preloading fails.
  • info (Optional): Arbitrary metadata.
  • matchFilters (Optional): Custom logic to validate route matching.

Component Inheritance:

When loadingComponent and errorComponent are defined in _app.tsx or _layout.tsx files, they automatically become defaults for all descendant routes. This follows a three-tier fallback chain:

  1. Route-specific - Component defined in the route's own createRoute()
  2. Nearest layout - Component from the closest _layout.tsx ancestor
  3. App default - Component from _app.tsx
  4. None - If not defined anywhere

This inheritance system reduces boilerplate while maintaining flexibility for route-specific overrides.

Example 1: Basic Page with Dynamic Params

File: src/pages/blog/[id].tsx

import { createRoute } from 'solid-file-router'
import { useParams } from '@solidjs/router'

export default createRoute({
  // Validate matches or extract custom data
  matchFilters: {
    id: (v) => /^\d+$/.test(v) // Only match if ID is numeric
  },
  component: (props) => {
    // Typesafe params if using the generated hooks/types
    const params = useParams<{ id: string }>()
    return <div>Viewing Post ID: {params.id}</div>
  },
})

Example 2: Data Loading, Loading States & Error Handling

File: src/pages/dashboard.tsx

import { createRoute } from 'solid-file-router'

export default createRoute({
  // Fetch data before the component renders
  preload: async ({ params, location }) => {
    const res = await fetch(`/api/stats`)
    if (!res.ok) throw new Error("Failed to load stats")
    return res.json()
  },

  // Show this while preload is awaiting
  loadingComponent: () => <div class="spinner">Loading Dashboard...</div>,

  // Show this if preload throws or component errors
  errorComponent: (props) => (
    <div class="error-alert">
      <p>Error: {props.error.message}</p>
      <button onClick={props.reset}>Retry</button>
    </div>
  ),

  // Main component receives data from preload via props.data
  component: (props) => (
    <main>
      <h1>Dashboard</h1>
      <pre>{JSON.stringify(props.data, null, 2)}</pre>
    </main>
  ),
})

Example 3: Nested Layouts

File: src/pages/settings/_layout.tsx

import { createRoute } from 'solid-file-router'
import { A } from '@solidjs/router'

export default createRoute({
  component: (props) => (
    <div class="settings-layout">
      <nav>
        <A href="/settings/profile">Profile</A>
        <A href="/settings/account">Account</A>
      </nav>
      <div class="content">
        {/* Renders the nested child route */}
        {props.children}
      </div>
    </div>
  ),
})

Component Inheritance

One of the most powerful features is automatic inheritance of loading and error components from layouts to routes. This eliminates repetitive configuration while maintaining full control when needed.

How It Works

When you define loadingComponent or errorComponent in _app.tsx or _layout.tsx, all descendant routes automatically inherit these components unless they provide their own.

Inheritance Priority (Fallback Chain):

  1. Route's own component (highest priority)
  2. Nearest _layout.tsx ancestor
  3. _app.tsx application default
  4. None (lowest priority)

Example: Application-Wide Defaults

File: src/pages/_app.tsx

import { createRoute } from 'solid-file-router'

export default createRoute({
  component: (props) => (
    <div id="app">
      <header>My App</header>
      <main>{props.children}</main>
    </div>
  ),
  
  // These become defaults for ALL routes
  loadingComponent: () => (
    <div class="loading-spinner">
      <div class="spinner" />
      <p>Loading...</p>
    </div>
  ),
  
  errorComponent: (props) => (
    <div class="error-page">
      <h1>Something went wrong</h1>
      <p>{props.error.message}</p>
      <button onClick={props.reset}>Try Again</button>
    </div>
  ),
})

Now every route in your app automatically gets these loading and error components without any additional configuration!

Example: Section-Specific Overrides

File: src/pages/dashboard/_layout.tsx

import { createRoute } from 'solid-file-router'

export default createRoute({
  component: (props) => (
    <div class="dashboard">
      <aside>Dashboard Nav</aside>
      <div class="dashboard-content">{props.children}</div>
    </div>
  ),
  
  // Override loading for all dashboard routes
  loadingComponent: () => (
    <div class="dashboard-loading">
      <div class="skeleton-layout" />
    </div>
  ),
  
  // errorComponent not specified - inherits from _app.tsx
})

Result:

  • All routes under /dashboard/* use the dashboard-specific loading component
  • All routes under /dashboard/* still use the app-wide error component from _app.tsx

Example: Route-Specific Override

File: src/pages/dashboard/analytics.tsx

import { createRoute } from 'solid-file-router'

export default createRoute({
  preload: async () => {
    const data = await fetch('/api/analytics').then(r => r.json())
    return data
  },
  
  // This route needs a special loading state
  loadingComponent: () => (
    <div class="analytics-loading">
      <div class="chart-skeleton" />
      <div class="stats-skeleton" />
    </div>
  ),
  
  // errorComponent not specified - inherits from _app.tsx
  
  component: (props) => (
    <div class="analytics">
      <h1>Analytics</h1>
      <pre>{JSON.stringify(props.data, null, 2)}</pre>
    </div>
  ),
})

Result:

  • This specific route uses its own custom loading component
  • Still inherits the error component from _app.tsx

Example: Complete Inheritance Chain

Here's a complete example showing how the three-tier fallback works:

src/pages/
  _app.tsx                    # Defines: loadingComponent, errorComponent
  dashboard/
    _layout.tsx               # Defines: loadingComponent (overrides app)
    index.tsx                 # Inherits: dashboard loading, app error
    users.tsx                 # Inherits: dashboard loading, app error
    analytics.tsx             # Defines: loadingComponent (overrides dashboard)
                              # Inherits: app error
  settings/
    _layout.tsx               # Defines: errorComponent (overrides app)
    profile.tsx               # Inherits: app loading, settings error
    account.tsx               # Inherits: app loading, settings error

Inheritance Resolution:

Route Loading Component Error Component
/dashboard dashboard/_layout _app
/dashboard/users dashboard/_layout _app
/dashboard/analytics analytics (own) _app
/settings/profile _app settings/_layout
/settings/account _app settings/_layout

Benefits

Less Boilerplate - Define defaults once, use everywhere
Consistent UX - All routes in a section share the same loading/error experience
Full Control - Override at any level when you need custom behavior
Type Safe - Full TypeScript support with proper type inference
Zero Runtime Cost - Inheritance resolved at build time


generatePath(path, params)

A utility to construct URLs with type validation. It ensures you don't pass incorrect parameters to your routes.

Parameters:

  • path: The route pattern (e.g., /blog/:id).
  • params: Object containing:
    • Path parameters: Prefixed with $ (e.g., $id, $lang).
    • Query parameters: Standard keys (e.g., search, page).

Example: Type-Safe Navigation

import { generatePath } from 'solid-file-router'
import { useNavigate } from '@solidjs/router'

export function NavigationButton() {
  const navigate = useNavigate()

  const goToPost = (postId: string) => {
    // ✅ Type Safe: TS will error if $id is missing
    const url = generatePath('/blog/:id', {
      $id: postId,      // Path param
      ref: 'newsletter' // Query param -> /blog/123?ref=newsletter
    })

    navigate(url)
  }

  return <button onClick={() => goToPost('123')}>Read Post</button>
}

virtual:routes

The virtual module that exposes the generated routing configuration.

Exports:

  • FileRouter: High-level component to render the app (Easy to use).
  • fileRoutes: The raw RouteDefinition array for @solidjs/router.
  • Root: The component exported from _app.tsx.

Example1: Custom Base URL

import { render } from 'solid-js/web'
import { Router } from '@solidjs/router'
import { FileRouter } from 'virtual:routes'

render(() => <FileRouter base="/app" />, document.getElementById('app')!)

Example2: Custom Router Integration

If you need more control than <FileRouter> provides (e.g., preload or use <HashRouter />), you can use the raw exports:

import { render } from 'solid-js/web'
import { Router } from '@solidjs/router'
import { fileRoutes, Root } from 'virtual:routes'

render(() => (
  <Router
    root={<Root />} // Transformed `src/pages/_app.tsx`
    preload={true}
    {/* Other props */}
  >
    {fileRoutes}
  </Router>
), document.getElementById('app')!)

Type Definition

In tsconfig.json

{
  "compilerOptions": {
    "types": [
      "solid-file-router/client"
    ]
  }
}

Configuration

Options passed to the fileRouter() plugin in vite.config.ts.

interface FileRouterPluginOption {
  /**
   * The output file path where the page types will be saved.
   * @default 'src/routes.d.ts'
   */
  output?: string
  /**
   * The base directory of `src/pages`.
   *
   * e.g. If your `_app.tsx` is located at `packages/app/module/src/pages/_app.tsx`,
   * You need to setup to `packages/app/module/`
   * @default ''
   */
  baseDir?: string
  /**
   * A list of glob patterns to be ignored during processing.
   *
   * Default is {@link DEFAULT_IGNORES}: all files in `components/`, `node_modules/` and `dist/`
   */
  ignore?: string[]
  /**
   * Whether to reload the page when route files change.
   * @default true
   */
  reloadOnChange?: boolean
  /**
   * Route's dts config to control Route's info type
   * @example
   * ```ts
   * {
   *   title: 'string',
   *   description: 'string',
   *   auth: {
   *     required: 'boolean',
   *     code: 'string',
   *   },
   *   tags: 'string[]',
   * }
   * ```
   */
  infoDts?: InfoTypeDefinition
  /**
   * Component inheritance configuration.
   * 
   * Controls how loading and error components are inherited from layouts.
   * 
   * @default { enabled: true, inheritLoading: true, inheritError: true }
   */
  inheritance?: {
    /**
     * Whether to enable component inheritance globally.
     * When false, routes will not inherit loading/error components from layouts.
     * @default true
     */
    enabled?: boolean
    /**
     * Whether to inherit loadingComponent from layouts.
     * Only applies when `enabled` is true.
     * @default true
     */
    inheritLoading?: boolean
    /**
     * Whether to inherit errorComponent from layouts.
     * Only applies when `enabled` is true.
     * @default true
     */
    inheritError?: boolean
  }
}

Configuring Component Inheritance

By default, all routes inherit loading and error components from their layouts. You can control this behavior at both the plugin level (build-time) and route level (runtime).

Build-Time Configuration (Plugin Level)

Control inheritance globally for all routes:

// vite.config.ts
import { fileRouter } from 'solid-file-router/plugin'

export default defineConfig({
  plugins: [
    fileRouter({
      // Disable all inheritance globally
      inheritance: {
        enabled: false
      }
    })
  ]
})

Or selectively disable specific component types:

// vite.config.ts
export default defineConfig({
  plugins: [
    fileRouter({
      inheritance: {
        enabled: true,
        inheritLoading: false,  // Routes won't inherit loading components
        inheritError: true      // Routes will still inherit error components
      }
    })
  ]
})

Runtime Configuration (Route Level)

Control inheritance for individual routes using the inherit property:

// Disable all inheritance for this route
export default createRoute({
  component: () => <SpecialPage />,
  inherit: false  // No loading/error components from layouts
})
// Selectively disable inheritance
export default createRoute({
  component: () => <CustomPage />,
  loadingComponent: () => <CustomLoader />,
  inherit: {
    loading: false,  // Don't inherit loading component
    error: true      // Still inherit error component (default)
  }
})

Configuration Priority

The inheritance resolution follows this priority order:

  1. Route-level inherit configuration (highest priority)

    • inherit: false disables all inheritance
    • inherit: { loading: false } disables loading inheritance
    • inherit: { error: false } disables error inheritance
  2. Build-time plugin configuration

    • inheritance.enabled: false disables globally
    • inheritance.inheritLoading: false disables loading inheritance globally
    • inheritance.inheritError: false disables error inheritance globally
  3. Default behavior (lowest priority)

    • Inheritance enabled for both loading and error components

Use Cases

Performance-Critical Routes:

// Skip wrapper components for maximum performance
export default createRoute({
  component: () => <HighPerformancePage />,
  inherit: false
})

Custom Error Handling:

// Use custom error handling instead of inherited error boundary
export default createRoute({
  component: () => <CustomErrorHandlingPage />,
  errorComponent: (props) => <CustomErrorUI error={props.error} />,
  inherit: { error: false }
})

Gradual Migration:

// During migration, disable inheritance for legacy routes
export default createRoute({
  component: () => <LegacyPage />,
  inherit: false  // Legacy page handles its own loading/error states
})

Credit

Highly inspired by generouted. Created to provide better customization for SolidJS specific features like lazy loading route components while keeping route metadata eager.

License

MIT

About

Type-safe file router helper for solid.js

Resources

Stars

Watchers

Forks

Packages

No packages published