-
-
Notifications
You must be signed in to change notification settings - Fork 0
Routing
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.
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
anddata-root
are reserved attributes forRouter
and are internally used for hydration and other functionality; - you don't need to use a separate outlet component for file system routing.
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 andimport "./routes"
in yoursrc/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.
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.
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() }),
});
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());
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}`);
};
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"),
)
}
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.
The unwrap()
utility is internally used to help in two main cases:
-
Match the hydration root to easily invoke a
root.replaceChildren(...newChildren)
(root is a<main />
element returned by theRouter()
). -
Match the
van.add
orvan.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);
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.
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 yourroutesDir
, there is no point in checking other extensions.
- Basic Routes
src/routes/
├── index.ts # / (home page)
├── about.ts # /about
└── contact.ts # /contact
- Nested Routes
src/routes/
├── blog/
│ ├── index.ts # /blog
│ ├── [id].ts # /blog/:id (dynamic route)
│ └── draft.ts # /blog/draft
└── index.ts # /
- 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 # /
- 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
- Dynamic Routes
src/routes/
├── blog/
│ ├── [id].ts # /blog/:id
│ └── [...all].ts # /blog/* (catch-all)
└── users/
└── [userId]/
└── [postId].ts # /users/:userId/:postId