Vue 3 Integration
This guide covers everything you need to integrate @esmx/router with Vue 3. By the end, you'll have a fully working Vue 3 app with routing and server-side rendering (SSR).
Installation
Install the core router and the Vue integration package:
npm install @esmx/router @esmx/router-vue
@esmx/router is the framework-agnostic router core. @esmx/router-vue provides Vue-specific bindings — a plugin, composables, and components.
Key Concepts
The Vue 3 integration relies on three pieces:
- RouterPlugin — Registers
RouterView and RouterLink as global components
- useProvideRouter() — Provides the router instance to the component tree via
provide/inject
- useRouter() / useRoute() — Composition API composables for accessing the router and current route
Step-by-Step Setup
1. Define Your Routes
Create a single source of truth for route definitions:
src/routes.ts
import type { RouteConfig } from '@esmx/router';
export const routes: RouteConfig[] = [
{
path: '/',
component: () => import('./layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('./pages/Home.vue') },
{ path: 'about', component: () => import('./pages/About.vue') },
{
path: 'users/:id',
component: () => import('./pages/UserProfile.vue'),
meta: { requiresAuth: true }
}
]
}
];
2. Create the App Factory
A shared factory that both the client and server entries use:
src/create-app.ts
import { h, createApp, createSSRApp } from 'vue';
import { Router } from '@esmx/router';
import { RouterPlugin, useProvideRouter } from '@esmx/router-vue';
import App from './App.vue';
export function createVueApp(router: Router, ssr = false) {
const create = ssr ? createSSRApp : createApp;
const app = create({
setup() {
useProvideRouter(router);
return () => h(App);
}
});
app.use(RouterPlugin);
return { app, router };
}
Key points:
useProvideRouter(router) must be called inside setup() of the root component. It makes the router available to all descendants via useRouter() and useRoute().
RouterPlugin registers RouterView and RouterLink as global components, and sets up $router and $route on globalProperties.
- Use
createSSRApp for server-side rendering and createApp for client-side only.
3. Client Entry
The client entry creates the router in history mode and mounts the app:
src/entry.client.ts
import { Router, RouterMode } from '@esmx/router';
import { createVueApp } from './create-app';
import { routes } from './routes';
const router = new Router({
appId: 'app',
mode: RouterMode.history,
routes
});
const { app } = createVueApp(router);
app.mount('#app');
RouterMode.history uses the browser's History API (pushState, popstate) for clean URLs.
4. Server Entry (SSR)
The server entry creates the router in memory mode and renders HTML:
src/entry.server.ts
import type { RenderContext } from '@esmx/core';
import { renderToString } from '@vue/server-renderer';
import { Router, RouterMode } from '@esmx/router';
import { createVueApp } from './create-app';
import { routes } from './routes';
export default async (rc: RenderContext) => {
const router = new Router({
mode: RouterMode.memory,
base: new URL(rc.params.url, 'http://localhost'),
routes
});
await router.replace(rc.params.url);
const { app } = createVueApp(router, true);
const html = await renderToString(app, {
importMetaSet: rc.importMetaSet
});
rc.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
${rc.preload()}
${rc.css()}
</head>
<body>
<div id="app">${html}</div>
${rc.importmap()}
${rc.moduleEntry()}
${rc.modulePreload()}
</body>
</html>`;
};
RouterMode.memory keeps the routing state in memory — no browser API calls, perfect for Node.js environments.
5. Node Entry
The Node entry configures the dev server and build tooling:
src/entry.node.ts
import http from 'node:http';
import type { EsmxOptions } from '@esmx/core';
export default {
async devApp(esmx) {
return import('@esmx/rspack-vue').then((m) =>
m.createRspackVue3App(esmx)
);
},
async server(esmx) {
const server = http.createServer((req, res) => {
esmx.middleware(req, res, async () => {
const rc = await esmx.render({
params: { url: req.url }
});
res.end(rc.html);
});
});
server.listen(3000, () => {
console.log('Server started: http://localhost:3000');
});
}
} satisfies EsmxOptions;
createRspackVue3App from @esmx/rspack-vue sets up Rspack with Vue 3 SFC support, HMR, and SSR bundling.
Using the Router in Components
RouterView and RouterLink
RouterView renders the matched component for the current route. RouterLink creates navigation links with active state management:
src/App.vue
<template>
<div>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/users/42">User 42</RouterLink>
</nav>
<RouterView />
</div>
</template>
RouterLink automatically applies the router-link-active class when the current route matches the link's to path. Customize it with the activeClass prop:
<RouterLink to="/about" active-class="nav-active">About</RouterLink>
useRouter and useRoute
Access the router and current route inside <script setup>:
src/pages/UserProfile.vue
<template>
<div>
<h1>User {{ route.params.id }}</h1>
<p>Current path: {{ route.path }}</p>
<p>Query: {{ JSON.stringify(route.query) }}</p>
<p>Meta: {{ JSON.stringify(route.meta) }}</p>
<button @click="goHome">Go Home</button>
<button @click="goToNextUser">Next User</button>
</div>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from '@esmx/router-vue';
import { watch } from 'vue';
const router = useRouter();
const route = useRoute();
function goHome() {
router.push('/');
}
function goToNextUser() {
const currentId = Number(route.params.id);
router.push(`/users/${currentId + 1}`);
}
// Watch for route changes
watch(() => route.path, (newPath, oldPath) => {
console.log(`Route changed from ${oldPath} to ${newPath}`);
});
</script>
Navigation Methods
The router provides several navigation methods:
<script setup lang="ts">
import { useRouter } from '@esmx/router-vue';
const router = useRouter();
// Push a new entry onto the history stack
router.push('/about');
// Replace the current entry (no new history entry)
router.replace('/about');
// Go back
router.back();
// Go forward
router.forward();
// Go to a specific history offset
router.go(-2);
</script>
The route object provides full information about the current route:
<script setup lang="ts">
import { useRoute } from '@esmx/router-vue';
const route = useRoute();
// Current path
route.path // '/users/42'
// Route parameters
route.params // { id: '42' }
// Query string parameters
route.query // { tab: 'profile' } for /users/42?tab=profile
// Route meta data
route.meta // { requiresAuth: true }
// Full URL object
route.url // URL instance
// Matched route configs (parent → child)
route.matched // RouteConfig[]
</script>
Project File Structure
A typical Vue 3 + SSR project with @esmx/router:
src/
├── entry.node.ts # Node.js server setup, dev/build config
├── entry.server.ts # SSR rendering logic
├── entry.client.ts # Client-side mounting and app activation
├── create-app.ts # Shared app factory (used by both server & client)
├── routes.ts # Route definitions
├── App.vue # Root component (RouterView + navigation)
├── layouts/
│ └── MainLayout.vue # Layout with nested RouterView
└── pages/
├── Home.vue
├── About.vue
└── UserProfile.vue
What's Next?