Rutter is a framework-agnostic, lightweight router. Built with URLPattern & History API. Internal reactivity is powered by Signal.
This library doesn't ship polyfill for
URLPattern
. You may consider installing urlpattern-polyfill.
import { CreateHistory } from 'rutter'
const router = new CreateHistory({
routes: {
index: {
pathname: ''
},
about: {
pathname: '/about'
},
blog: {
pathname: '/blog'
},
blogDetail: {
pathname: '/blog/:id'
}
}
})
router.on('index') // boolean
router.onOneOf(['index', 'about']) // boolean
useState
/context
// router.(tsx|jsx)
import {
FC,
PropsWithChildren,
createContext,
useContext,
useEffect,
useState
} from 'react'
import { CreateHistory } from 'rutter'
export const {
redirect,
on,
summaryState,
routeState,
watchSummaryState,
watchRouteState
} = new CreateHistory({
routes: {
index: {
pathname: ''
},
about: {
pathname: '/about'
},
blog: {
pathname: '/blog'
},
blogDetail: {
pathname: '/blog/:id'
}
}
})
/**
* Although using with `context` is recommended for performance reason, you can directly use this hook if you don't want to store all the states in `context` tree.
*/
export const useRouterValues = () => {
const [routeStateValue, setRouteStateState] = useState(routeState)
const [summaryStateValue, setSummaryStateState] = useState(summaryState)
useEffect(() => watchRouteState(setRouteStateState), [])
useEffect(() => watchSummaryState(setSummaryStateState), [])
return {
routeState: routeStateValue,
summaryState: summaryStateValue
}
}
const context = createContext({
routeState,
summaryState
})
const useRouterContext = () => useContext(context)
export const RouterProvider: FC<PropsWithChildren> = ({ children }) => {
const value = useRouterValues()
return <context.Provider value={value}>{children}</context.Provider>
}
export const useRoute = () => {
const { routeState } = useRouterContext()
return routeState
}
// app.(tsx|jsx)
import { FC } from 'react'
import { on, redirect, useRoute, RouterProvider } from './router'
const Routing: FC = () => {
const { is404, ...restStates } = useRoute()
return (
<>
<nav>
<button onClick={() => redirect('index')}>Index</button>
<button onClick={() => redirect('blog')}>Blog</button>
<a href="/invalid-url">
<button>404</button>
</a>
</nav>
<fieldset>
<legend>Body:</legend>
<div>
{is404 ? (
<h1>404 Page</h1>
) : (
<>
{on('index') && <h1>Index Page</h1>}
{on('about') && <h1>About Page</h1>}
{on('blog') && (
<>
<h1>Blog Page</h1>
<button
onClick={() =>
redirect('blogDetail', {
params: {
id: 123
}
})
}
>
Blog Detail
</button>
</>
)}
{on('blogDetail') && <h1>Blog Detail Page</h1>}
</>
)}
</div>
</fieldset>
<fieldset>
<legend>Current route detail:</legend>
<code>
<pre>{JSON.stringify(restStates, null, 2)}</pre>
</code>
</fieldset>
</>
)
}
const App: FC = () => (
<RouterProvider>
<Routing />
</RouterProvider>
)
shallowRef
/computed
// router.(ts|js)
import { computed, shallowRef } from 'vue'
import { CreateHistory } from 'rutter'
import { mapValues } from 'lodash-es'
const router = new CreateHistory({
routes: {
index: {
pathname: ''
},
about: {
pathname: '/about'
},
blog: {
pathname: '/blog'
},
blogDetail: {
pathname: '/blog/:id'
}
}
})
const {
//
summaryState,
routeState,
watchSummaryState,
watchRouteState,
on
} = router
export const { redirect } = router
export const routerState = shallowRef(summaryState)
export const route = shallowRef(routeState)
export const is404 = computed(() => route.value.is404)
export const matches = computed(() => {
const { details } = routerState.value
type RouteNames = keyof typeof details
return mapValues(details, (_, name) => on(name as RouteNames))
})
watchSummaryState(state => {
routerState.value = state
})
watchRouteState(state => {
route.value = state
})
<script setup lang="ts">
// app.vue
import { redirect, route, matches, is404 } from './router'
</script>
<template>
<nav>
<button @click="() => redirect('index')">Index</button>
<button @click="() => redirect('blog')">Blog</button>
<a href="/invalid-url">
<button>404</button>
</a>
</nav>
<fieldset>
<legend>Body:</legend>
<div>
<h1 v-if="is404">404 Page</h1>
<template v-else>
<h1 v-if="matches.index">Index Page</h1>
<h1 v-if="matches.about">About Page</h1>
<template v-if="matches.blog">
<h1>Blog Page</h1>
<button
@click="() => redirect('blogDetail', { params: { id: 123 } })"
>
Blog Detail
</button>
</template>
<h1 v-if="matches.blogDetail">Blog Detail Page</h1>
</template>
</div>
</fieldset>
<fieldset>
<legend>Current route detail:</legend>
<code>
<pre>{{ route }}</pre>
</code>
</fieldset>
</template>
readable
/derived
// router.(ts|js)
import { readable, derived } from 'svelte/store'
import { CreateHistory } from 'rutter'
import { mapValues } from 'lodash-es'
const router = new CreateHistory({
routes: {
index: {
pathname: ''
},
about: {
pathname: '/about'
},
blog: {
pathname: '/blog'
},
blogDetail: {
pathname: '/blog/:id'
}
}
})
const { summaryState, routeState, watchSummaryState, watchRouteState } = router
export const { redirect, on, onOneOf } = router
export const route = readable(routeState, watchRouteState)
export const routerState = readable(summaryState, watchSummaryState)
export const matches = derived(routerState, ({ details }) =>
mapValues(details, (_, name) => on(name as keyof typeof details))
)
<script lang="ts">
// app.svelte
import { redirect, route, matches } from './router'
$: ({ is404, ...restState } = $route)
$: data = JSON.stringify(restState, null, 2)
</script>
<nav>
<button on:click={() => redirect('index')}>Index</button>
<button on:click={() => redirect('blog')}>Blog</button>
<a href="/invalid-url">
<button>404</button>
</a>
</nav>
<fieldset>
<legend>Body:</legend>
<div>
{#if is404}
<h1>404 Page</h1>
{:else}
{#if $matches.index}
<h1>Index Page</h1>
{/if}
{#if $matches.about}
<h1>About Page</h1>
{/if}
{#if $matches.blog}
<h1>Blog Page</h1>
<button
on:click={() => redirect('blogDetail', { params: { id: 123 } })}
>
Blog Detail
</button>
{/if}
{#if $matches.blogDetail}
<h1>Blog Detail Page</h1>
{/if}
{/if}
</div>
</fieldset>
<fieldset>
<legend>Current route detail:</legend>
<code>
<pre>{data}</pre>
</code>
</fieldset>
Type API: https://paka.dev/npm/rutter/api
pnpm i
pnpm dev