Skip to content

Route Parameters

A routed app usually needs pages for individual records too: one post, one product, one user, one event.

In our example, the next step is a public page for one event. That means the route needs a dynamic slug parameter:

import { param } from '@nano_kit/router'
export const routes = {
home: '/',
event: '/events/:slug'
} as const
export const $slug = param($location, 'slug')

param reads the dynamic part of the current route. For /events/react-ssr-workshop, $slug() returns 'react-ssr-workshop'.

The router can now include that page:

import { page, router, useNavigationListenLinks, usePageSignal } from '@nano_kit/react-router'
import { $location, navigation } from './stores/router'
import Event from './ui/pages/Event'
import Home from './ui/pages/Home'
const $page = router($location, [
page('home', Home),
page('event', Event)
])
export function App() {
const Page = usePageSignal($page)
useNavigationListenLinks(navigation)
return Page ? <Page /> : null
}

The event page uses the new /events/:slug route. When the browser URL matches that pattern, $page becomes Event.

The event page can load one event by slug.

import { queryKey } from '@nano_kit/query'
import { type BoardEvent, fetchEvent } from '#src/services/events'
import { query } from './query'
import { $slug } from './router'
export const EventKey = queryKey<[slug: string | undefined], BoardEvent | null>('event')
export const [
$event,
$eventError,
$eventLoading
] = query(
EventKey,
[$slug],
async slug => (
slug
? await fetchEvent(slug)
: null
)
)

The cache key idea is the same as before, but this time we use a regular query instead of infinite. The page needs one record, not a list of pages. The important parameter is [$slug]: when the route parameter changes, the query loads data for the new page.

The result signals follow the same pattern as the list query: $event, $eventError, and $eventLoading.

The detail page also needs an RSVP button. When the user clicks it, the app will call POST /api/events/:id/rsvp and the server will return the event with an updated attendee count.

The query client needs mutations for this action:

import { client, infinites, mutations } from '@nano_kit/query'
export const {
query,
infinite,
mutation,
revalidate
} = client(
infinites(),
mutations()
)

mutations() adds mutation to the client. A query runs from reactive parameters. A mutation runs only when you call it.

revalidate marks cached data as stale and tells active queries to fetch again. We will use it after the mutation succeeds.

The button action is a mutation:

import { onSuccess } from '@nano_kit/query'
import { type BoardEvent, rsvpEvent } from '#src/services/events'
import { mutation, revalidate } from './query'
import { $slug } from './router'
export const [
rsvp,
$rsvpEvent,
$rsvpError,
$rsvpLoading
] = mutation<[id: string], BoardEvent>((id, ctx) => {
onSuccess(ctx, () => {
revalidate(EventKey($slug()))
})
return rsvpEvent(id)
})

When the request succeeds, revalidate(EventKey($slug())) refreshes the event query. The page keeps reading the event from $event.

The React page reads the query signals and uses the mutation signals only for the button state:

import { useSignal } from '@nano_kit/react'
import { $event, $eventError, $eventLoading, $rsvpError, $rsvpLoading, rsvp } from '#src/stores/events'
export default function Event() {
const event = useSignal($event)
const error = useSignal($eventError)
const loading = useSignal($eventLoading)
const rsvpError = useSignal($rsvpError)
const rsvpLoading = useSignal($rsvpLoading)
if (loading) {
return <p>Loading...</p>
}
if (error) {
return <p>{error}</p>
}
if (!event) {
return <p>Event not found.</p>
}
return (
<article>
<a href='/'>Back to events</a>
<h1>{event.title}</h1>
<p>{event.description}</p>
<p>{event.location}</p>
<p>{event.attendees} going</p>
{rsvpError && <p>{rsvpError}</p>}
<button disabled={rsvpLoading} onClick={() => rsvp(event.id)}>
{rsvpLoading ? 'Saving...' : "I'm going"}
</button>
</article>
)
}

The page has three states before the final UI: loading, error, and not found. Once event is available, it renders the public content and the button.