Routing in Refract
Welcome to the router guide! If you're building a single-page application with multiple views, you'll need a way to manage navigation. While you could handle this with simple state, a dedicated router provides a more robust solution. Best of all, we can build one that feels completely natural in the Refract ecosystem.
Why a Custom Router?
You might be wondering: "Why build my own router when there are existing solutions?" Great question! While you could wrap an existing router, building your own allows for:
- Perfect integration with Refract's reactivity system
- Simplified API tailored to your specific needs
- No external dependencies beyond Refract itself
- Complete understanding of how routing works in your app
Think of it this way:A custom router is like building a custom piece of furniture instead of buying something pre-made. It fits your space perfectly and does exactly what you need.
The Reactive Routing Concept
At its core, a router needs to do two things:
- Track the current URL and parse it into useful information
- Update the UI when the URL changes
In Refract, we can model both of these as reactive state. The current route becomes a refraction that components can observe and react to. If you need a reminder about how refractions work, check out the Core Concepts: Refractions guide.
Planning Our Router API
Before we start coding, let's think about what we want our router to do:
// How we want to use it
const router = createRouter({
routes: [
{ path: '/', component: HomePage },
{ path: '/about', component: AboutPage },
{ path: '/users/:id', component: UserProfile }
]
})
// In our main app
app.use(router)
// In components - reactive to route changes!
const currentRoute = useRoute()
Building the Router Step by Step
Let's build our router piece by piece. We'll create this as a plugin, so if you haven't read about Creating Plugins yet, you might want to glance at that first.
Step 1: The Basic Plugin Structure
We'll start with the plugin structure we learned about in the plugins guide:
function createRouter(routes) {
const router = {
// Our router methods and properties will go here
}
return {
install(app) {
// Make router available to the whole app
app.provide('router', router)
app.config.globalProperties.$router = router
}
}
}
Step 2: Tracking the Current Route
The heart of our router is reactive state that tracks the current URL:
function createRouter(routes) {
// Create reactive state for current route
const currentRoute = refract({
path: window.location.pathname,
params: {},
query: {}
})
// Function to parse the current URL
function parseRoute(path) {
// Match against our defined routes
for (const route of routes) {
const matches = path.match(pathToRegex(route.path))
if (matches) {
return {
path,
params: matches.groups || {},
component: route.component
}
}
}
// Return a not found route if no match
return {
path,
params: {},
component: NotFoundComponent
}
}
// Update currentRoute when URL changes
function updateRoute() {
currentRoute.value = parseRoute(window.location.pathname)
}
// Listen to browser navigation events
window.addEventListener('popstate', updateRoute)
const router = {
currentRoute,
// More methods to come...
}
// Initialize with current URL
updateRoute()
return {
install(app) {
app.provide('router', router)
app.config.globalProperties.$router = router
// Also provide the current route directly
app.provide('currentRoute', currentRoute)
}
}
}
The pathToRegex function is a simplified version of what you'd implement in a real router. In production, you'd want more robust path matching.
Step 3: Adding Navigation Methods
Now let's add methods to programmatically navigate:
const router = {
currentRoute,
push(path) {
window.history.pushState({}, '', path)
this.updateRoute()
},
replace(path) {
window.history.replaceState({}, '', path)
this.updateRoute()
},
go(n) {
window.history.go(n)
},
back() {
window.history.back()
},
forward() {
window.history.forward()
},
updateRoute // Make available internally
}
Step 4: Creating a Route Component
Let's create a component that renders the current route:
// router-view.js
export default {
setup() {
const currentRoute = inject('currentRoute')
return () => {
const Component = currentRoute.value.component || DefaultComponent
return h(Component)
}
}
}
If you're not familiar with the h function or component rendering, check out the Component Creation guide.
Step 5: Creating a Composition Function
Let's make a convenient composition function for components:
// useRoute.js
export function useRoute() {
const route = inject('currentRoute')
const router = inject('router')
return {
route,
router,
push: router.push,
replace: router.replace
}
}
Now in any component, we can do:
import { useRoute } from './useRoute'
export default {
setup() {
const { route, push } = useRoute()
// route is reactive - component will update when it changes!
const userId = computed(() => route.value.params.id)
const navigateToAbout = () => push('/about')
return { userId, navigateToAbout }
}
}
Advanced Router Features
Once you have the basic router working, you might want to add:
Route Guards
Protect routes with authentication checks:
function createRouter(routes) {
// ... existing code ...
const guards = []
function addGuard(guard) {
guards.push(guard)
}
async function navigate(path) {
const to = parseRoute(path)
// Run all guards
for (const guard of guards) {
const result = await guard(to, currentRoute.value)
if (result === false || typeof result === 'string') {
// Guard blocked navigation or redirected
return result === false ? false : navigate(result)
}
}
// All guards passed - proceed with navigation
window.history.pushState({}, '', path)
updateRoute()
return true
}
// Update router methods to use navigate
const router = {
// ... other methods ...
push: navigate,
addGuard
}
}
Scroll Behavior
Manage scroll position during navigation:
function createRouter(routes) {
// ... existing code ...
let scrollPositions = new Map()
window.addEventListener('popstate', () => {
updateRoute()
restoreScrollPosition()
})
function saveScrollPosition() {
scrollPositions.set(currentRoute.value.path, {
x: window.scrollX,
y: window.scrollY
})
}
function restoreScrollPosition() {
const position = scrollPositions.get(currentRoute.value.path)
if (position) {
window.scrollTo(position.x, position.y)
} else {
window.scrollTo(0, 0)
}
}
// Add event listener to save position before navigation
window.addEventListener('beforeunload', saveScrollPosition)
}
Testing Your Router
Like any important piece of infrastructure, you should test your router:
// router.test.js
import { createApp } from 'refract'
import { createRouter } from './router'
test('navigates between routes', async () => {
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
const router = createRouter([
{ path: '/', component: Home },
{ path: '/about', component: About }
])
const app = createApp()
app.use(router)
// Test initial route
expect(router.currentRoute.value.path).toBe('/')
// Test navigation
router.push('/about')
expect(router.currentRoute.value.path).toBe('/about')
expect(router.currentRoute.value.component).toBe(About)
})
Integration with Your App
Here's how to use your finished router:
// main.js
import { createApp } from 'refract'
import { createRouter } from './router'
import App from './App.vue'
import Home from './components/Home.vue'
import About from './components/About.vue'
const router = createRouter([
{ path: '/', component: Home },
{ path: '/about', component: About }
])
const app = createApp(App)
app.use(router)
app.mount('#app')
// App.vue
import RouterView from './router-view.vue'
export default {
setup() {
return () => h('div', [
h('nav', [/* navigation links */]),
h(RouterView)
])
}
}
Performance Considerations
As your app grows, consider these optimizations:
-
Lazy loading: Load components only when needed for a route -
Route preloading: Prefetch components for likely next routes -
Scroll restoration: Efficiently manage scroll positions -
Route matching optimization: Use efficient algorithms for matching routes
Congratulations! You've built a fully functional router for Refract. Remember, the best part of building your own router is that you can customize it exactly for your application's needs. Keep up the routing!