Skip to content

Routing

thednp edited this page May 2, 2025 · 16 revisions

The vite-plugin-vanjs plugin provides file system routing, configurable route load and/or preload, code splitting and lazy loading, all via the exported @vanjs/router module. This functionality is still under development, but it manages to work with both Server Side Rendering (SSR - when using an appropriate starter template), as well as Client Side Rendering (CSR/SPA - your classic VanJS app), as we'll see in the examples below.

Router

The Router() is an outlet that returns a <main id="main" /> element which usually renders page contents, but also acts as a Suspense and ErrorBoundary for your application. It can work with props like any other van.tags instance, will handle your file system routing and should work with JSX as well.

Assuming you are using the basic setup described in the quick start guide, here's a basic example, let's create an app.ts:

// src/app.ts
import van from "vanjs-core";
import { Router } from "@vanjs/router";

function App() {
  // the Router is the main application outlet
  return Router();
}

// render the app
van.add(document.getElementById("app"), App());

This won't do anything yet, your app will only show that no route was found. We need to create a specific file structure and define routes.

Notes:

  • the data-h and data-root are reserved attributes for Router and are internally used for hydration and other functionality;
  • you don't need to use a separate outlet component for file system routing.

Route

The Route() allows you to register new routes, checks if you have duplicates and is also used internally by the file system routing functionality.

Here's an updated src/app.ts with routes definitions:

// src/app.ts
import van from "vanjs-core";
import { Router, Route } from "@vanjs/router";

// define routes
Route({ path: '/', component: () => van.tags.div('Hello VanJS') });
Route({ path: '*', component: () => van.tags.div('404 - Page Not Found') });

function App() {
  return Router();
}

// render the app
van.add(document.getElementById("app"), App());

Notes:

  • if you have a complex structure, you can define your routes in a src/routes.ts file and import "./routes" in your src/app.ts;
  • in this example we've showcased some static components, but there are other ways to register routes for your page components, as well as a certain level of opinionated implementations, so continue on this guide to learn more.

Page Component

Just like any other VanJS component, you can define page components in the src/pages folder, so let's create it and add your first page. Here's how a page component usually looks like, pay attention to the comments:

// src/pages/home.ts
import van from "vanjs-core";

export const route = {
  preload: async () => {
    // in most cases you may want to enforce user access control
    console.log('Home preload triggered');
  },
  load: async () => {
    // Load data if needed
    // you might want to cache this data
    console.log('Home load triggered');
  }
}

export const Page = () => {
  const { div, h1, p } = van.tags;

  return [
    div({ class: "flex h-screen" },
      div({ class: "p-4 m-auto text-center" },
        h1({ class: "text-5xl font-bold my-8" }, "Hello World!"),
        p({ class: "my-8" }, "This is a simple VanJS component page."),
      ),
    ),
  ];
};

Notes

  • important: the router unwraps page content to help simplify the document structure; that is why we wrapped all elements in an array ([]) closure so the router outlet won't unwrap; if we didn't do that, the <div class="flex h-screen"> would be removed from the markup and only its children will be rendered;
  • the page component must export route and Page when you need to set preloaders, otherwise you can export the component as default; this is to make sure the router renders what it expects, but also vite won't warn you about unusual exports during development, build or any other runtime.

Layout Component

Each layout file should export a Layout component, for instance as in the examples above:

// src/routes/admin.ts
// applies to all routes in the src/routes/admin/ folder
import van from "vanjs-core";

export const Layout = ({ children }) => {
  const { div, header, footer, nav } = van.tags;
  return div(
    header(
      nav(
        // navigation items
      )
    ),
    div({ class: "content" },
      children
    )
    footer("Copyright © YourBrand")
  );
};

When using file system routing, your routes are created automatically at build or vite dev server start time, but you can also create them manually to use the layout with your page components:

import { Route, lazy } from "@vanjs/router";
import { Layout } from "./components/layout";
import { Page } from "./pages/home";

Route({
  path: "/",
  component: () => Layout({ children: Page() }),
});

Lazy Loading

The lazy() allows you to register lazy components and enables code splitting for a great boost to your app load performance. If your main JS bundle goes over the 20Kb mark, page load metrics usually go down, and this is the solution to go for.

In the src/pages folder we create a new file src/about.ts that we can register as a lazy component:

// src/pages/about.ts
import van from "vanjs-core";

export const route = {
  preload: async () => {
    console.log('About preload triggered');
  },
  load: async () => {
    console.log('About load triggered');
  }
}

export function Page() {
  const { div, h1, p } = van.tags;
  return div(
    h1('About page'),
    p('This is the about page'),
  )
}

Now let's update the src/app.ts to register this as a lazy component.

// src/app.ts
import van from "vanjs-core";
import { Router, Route, lazy } from "@vanjs/router";

// * A lazy route for the / (index) page
Route({ path: '/', component: lazy(() => import('./pages/home')) });

// * A lazy route for the /about page
Route({ path: '/about', component: lazy(() => import('./pages/about')) });

// * A lazy route for the /not-found page with custom layout
Route({
  path: "*",
  component: lazy(() => {
    return async () => {
      const IndexModule = await import("./pages/not-found");
      const LayoutModule = await import("./components/layout");
      const { Page, route } = IndexModule;
      const { Layout } = LayoutModule;
      return Promise.resolve({
        route,
        Page: () => Layout({ children: Page() })
      })
    }
  })
});

function App() {
  return Router();
}

// render the app
van.add(document.getElementById("app"), App());

routerState

The routerState is an object with a van.state instance for each of its main properties:

  • pathname: a state of the current path,
  • searchParams: a state which consists of an URLSearchParams instance,
  • params: a state with parameters of the current page.

This state is internally used by the router outlet, the route preloaders and other utilities like navigate and soon to come "redirect".

Access parameters in your page components and its preloaders:

// src/routes/blog/[id].ts
import van from "vanjs-core";
import { routerState } from "@vanjs/router";

export const route = {
  preload: async (params) => {
    console.log("Preload params:", params.id)
  },
  load: async (params) => {
    console.log("Load params:", params.id)
  }
};

export const Page = () => {
  const { div } = van.tags;
  const params = routerState.params.val;
  return div(`Blog post ${params.id}`);
};

Anchor

The A() is a VanJS component which extends the van.tags.a and enables client side routing. In other words, when using the van.tags.a instance, your app will work like a classic multi page app (MPA). As a quick note, we call it "A" to be different from "Link" from @vanjs/meta.

What it actually does

  • When clicked, it will modify the routerState to trigger rendering another page content into the router outlet.
  • When hovered, if it points to a lazy component, it will preload that component; this is to store the component in the browser cache to have it ready to render when used clicks that particular A component.
  • It uses an ARIA compliant attribute aria-current="page" to enable easy active styling and to highlight the fact that the user is currently on that particular page.
  • preventDefault() is invoked by default.

Example

// src/pages/about.ts
import van from "vanjs-core";
import { A } from "@vanjs/router";

export function Page() {
  const { div, h1, p } = van.tags;
  return div(
    h1('About page'),
    p('This is the about page'),
    A({ href: "/" }, "Home"),
  )
}

navigate

The navigate() allows you to change the router state programmatically. This is mostly used by client side scripting and has no effect during the server side rendering.

Example

// src/pages/about.ts
import van from "vanjs-core";
import { navigate } from "@vanjs/router";

export function Page() {
  const { div, h1, p, button } = van.tags;
  return div(
    h1('About page'),
    p('This is the about page'),
    button({ onclick: navigate("/", { replace: true }) }, "Home"),
  )
}

When the "replace" option is true, it will clear the browser navigation history and is false by default.

unwrap

The unwrap() utility is internally used to help in two main cases:

  1. Match the hydration root to easily invoke a root.replaceChildren(...newChildren) (root is a <main /> element returned by the Router()).
  2. Match the van.add or van.hydrate expected arguments.

Your page components may return various types of content structures, some return an Array, others return a single HTMLElement. The unwrap utility makes sure to always provide van.add with the exact same type of arguments every time.

Example component returning HTMLElement

import van from "vanjs-core";

// Returns a HTMLDivElement
const Component1 = () => {
  const { div, p } = van.tags;
  return div(
    p("Hello"),
    p("World")
  );
};

// Removes the parent div in the markup structure
const component1Result = unwrap(Component1());
// => { children: [p, p] }

const root = document.getElementById("main");

// we add 2 paragraphs to the root
van.add(root, ...component1Result.children);

Example component returning Array<HTMLElement>

import van from "vanjs-core";

// Returns an Array<HTMLDivElement>
const Component2 = () => {
  const { div, p } = van.tags;
  return [
    div(
      p("Hello"),
      p("World")
    )
  ];
};

// Keeps the parent div in the markup structure
const component2Result = unwrap(Component2());
// => { children: [div] }

const root = document.getElementById("main");

// we add the entire structure to the root
van.add(root, ...component2Result.children);

FileSystem Routing

Our latest addition to the pack is file-system routing. It can do everything explained above just by using the proper file structure. Here's how it works: on development server start, it will scan a designated folder for page components and layouts and register your routes automatically; on any change in the structure of that folder, it will be re-scanned and your routes updated via hot module replacement.

Directory Structure

By default, the plugin looks for routes in the src/routes folder. You can configure this in your vite.config.ts:

// vite.config.ts
import { defineConfig } from 'vite';
import vanjs from 'vite-plugin-vanjs';

export default defineConfig({
  plugins: [
    vanjs({
      routesDir: "src/routes",                    // (default)
      extensions: ['.tsx', '.jsx', '.ts', '.js'], // (default)
    })
  ],
});
  • routesDir: the "src/pages" and "src/routes" are preferable since most starter templates and other frameworks alike use them, but more importantly our prerenderer is configured with these;
  • extensions: the option is intended to optimize heavily complex apps, for instance when you only use .js files in your routesDir, there is no point in checking other extensions.

Route Patterns

  1. Basic Routes
src/routes/
├── index.ts         # / (home page)
├── about.ts         # /about
└── contact.ts       # /contact
  1. Nested Routes
src/routes/
├── blog/
│   ├── index.ts     # /blog
│   ├── [id].ts      # /blog/:id (dynamic route)
│   └── draft.ts     # /blog/draft
└── index.ts         # /
  1. Layout Routes
src/routes/
├── admin/
│   ├── index.ts     # /admin
│   ├── users.ts     # /admin/users
│   └── [...all].ts  # /admin/* (catch all in /admin)
├── admin.ts         # layout for /admin/*
└── index.ts         # /
  1. Grouped Routes
src/routes/
│── (root)/          # grouped routes with shared layout
│   ├── index.ts     # /
│   ├── about.ts     # /about
│   └── [...all].ts  # /* (catch all)
└── (root).ts        # layout for grouped routes
  1. Dynamic Routes
src/routes/
├── blog/
│   ├── [id].ts           # /blog/:id
│   └── [...all].ts       # /blog/* (catch-all)
└── users/
    └── [userId]/
        └── [postId].ts   # /users/:userId/:postId
Clone this wiki locally