This article covers creating a Riot app coupled with Riot-Route, Riot's official client-side routing solution.
Before starting, make sure you have a base application running, or read my previous article Setup Riot + BeerCSS + Vite.
Client-side routing links the browser URL with the content on the page. When a user navigates the Riot application, the URL changes without requesting a new front-end from a server. This is called a SPA, for Single Page Applications: Riot handles all data updates and navigation without reloading the page, which makes the app rich and reactive!
Let's create the simplest routing example; then, we will delve into advanced production usage.
Basic Route
We aim to create the following app: a left drawer displaying links to different pages, and when a click happens on a link, the right section prints the corresponding page. The style is powered with the Material Design CSS BeerCSS:
Write the following code in ./index.riot
. The HTML comes from the BeerCSS documentation, and I added RiotJS syntax for the logic:
<index-riot> <router> <nav class="drawer left right-round border"> <header> <nav> <img class="circle" src="./examples/data/img-card.png"/> <h6>Jon Snow</h6> </nav> </header> <!-- These links will trigger automatically HTML5 history events --> <a href="/"> <i>inbox</i> <span class="max">Inbox</span> <b>24</b> </a> <a href="/favorite"> <i>favorite</i> <span class="max">Starred</span> <b>3</b> </a> <a href="/sent"> <i>send</i> <span class="max">Sent</span> <b>11</b> </a> <div class="medium-divider"></div> <a href="/subscription"> <i>rocket</i> <span>Subscription</span> </a> <a href="/settings"> <i>settings</i> <span>Settings</span> </a> </nav> <!-- Your application routes will be rendered here --> <span style="display:block;margin-left:20px;margin-top:20px"> <route path="/"> <h2>Inbox</h2> </route> <route path="/favorite"> <h2>Starred</h2> </route> <route path="/sent"> <h2>Sent</h2> </route> <route path="/subscription"> <h2>Subscription</h2> </route> <route path="/settings"> <h2>Settings</h2> </route> </span> </router> <script> import { Router, Route } from '@riotjs/route' export default { components: { Router, Route } } </script> </index-riot>
Source Code: https://github.com/steevepay/riot-beercss/blob/main/examples/riot-route/index.basic.riot
This example uses two Components provided by riot-route:
- Router: The
<router>
wraps the Riot application and automatically detects all the clicks on links that should trigger navigation change. - Route: The
<route path="/some/route/:params">
renders the page content if thepath
attribute corresponds to the current URL path. Thepath
can accept regex, or parameters, and you can access the current route with the route object:
<route path="/:some/:route/:param"> {JSON.stringify(route.params)} </route> <route path="/search(.*)"> <!-- Assuming the URL is "/search?q=awesome" --> {route.searchParams.get('q')} </route>
Source Code from the Riot-Route documentation
To access the current route in the Javascript section, it is possible to import the route object from '@riotjs/route':
import { Router, Route, route } from '@riotjs/route'
Advanced Route
Let's delve into advanced routing with the following requirements for a front-end:
- Show a 404 page if a URL path does not exist.
- Access query parameters into each page component.
- For each route, display a Riot Component as a page.
- Create a routing configuration file defining all routes, paths, and components.
In the first step, we will create 6 components, one for each page and another for the 404 Not Found
page. Components are located in the pages directory:
pages/p-favorite.riot pages/p-inbox.riot pages/p-sent.riot pages/p-settings.riot pages/p-subscription.riot pages/p-not-found.riot
Each component has only one title <h2>
tag, for instance, the pages/p-sent.riot
component looks like this:
<p-sent> <h2>Sent</h2> </p-sent>
Or the pages/p-not-found.riot
looks like:
<p-not-found> <h2> 404 Page Not Found </h2> </p-not-found>
Then, create a global routing configuration file in the routes.js. The file returns a list of pages, and each Page has a name
, a path
with a long regex, and a corresponding component:
export default [ { name : 'Inbox', href : '/', path : '/(/?[?#].*)?(#.*)?', component: 'p-inbox', icon : 'inbox' }, { name : 'Starred', href : '/favorite', path : '/favorite(/?[?#].*)?(#.*)?', component: 'p-favorite', icon : 'favorite' }, { name : 'Sent', href : '/sent', path : '/sent(/?[?#].*)?(#.*)?', component: 'p-sent', icon : 'send', separator: true }, { name : 'Subscription', href : '/subscription', path : '/subscription(/?[?#].*)?(#.*)?', component: 'p-subscription', icon : 'rocket', }, { name : 'Settings', href : '/settings', path : '/settings(/?[?#].*)?(#.*)?', component: 'p-settings', icon : 'settings' } ]
Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/riot-route/routes.js
The <route>
Riot component will use the path
attribute. Each regex is composed of 3 parts:
-
/settings
: Path of the page (Required static string) -
(/?[?#].*)
: Query parameters (Optional group) -
(#.*)?
: Fragment, a section within a page (Optional group)
Now, import the routes.js, and all components into the index.riot file: Define components into the components:{}
Riot Object, and load the routes into the state:{}
Object:
<index-riot> <router> <nav class="drawer left right-round border"> <header> <nav> <img class="circle" src="./examples/data/img-card.png"/> <h6>Jon Snow</h6> </nav> </header> <!-- Navigation bar created dynamically --> <template each={ page in state.pages }> <a href={ page.href }> <i>{ page.icon }</i> <span class="max">{ page.name }</span> </a> <div if={ page.separator === true } class="medium-divider"></div> </template> </nav> <!-- Your application components/routes will be rendered here --> <span style="display:block;margin-left:20px;margin-top:20px"> <route each={ page in state.pages } path={ page.path }> <span is={ page.component } route={ route }></span> </route> <p-not-found if={ state.showNotFound } /> </span> </router> <script> import { Router, Route, route, toRegexp, match } from '@riotjs/route'; import pages from './routes.js' import pInbox from "./pages/p-inbox.riot"; import pFavorite from "./pages/p-favorite.riot"; import pSent from "./pages/p-sent.riot" import pSettings from "./pages/p-settings.riot" import pSubscription from "./pages/p-subscription.riot" import pNotFound from "./pages/p-not-found.riot" export default { components: { Router, Route, pInbox, pFavorite, pSent, pSettings, pSubscription, pNotFound }, state: { pages, showNotFound: false }, onMounted (props, state) { // ROUTING: create a stream on all routes this.anyRouteStream = route('(.*)') // ROUTING: check any route change to understand if the not found site should be displayed this.anyRouteStream.on.value((path) => { this.update({ showNotFound: !this.state.pages.some(p => match(path.pathname, toRegexp(p?.path))) }) }) }, onUnmounted() { this.anyRouteStream.end() } } </script> </index-riot>
Source code: https://github.com/steevepay/riot-beercss/blob/main/examples/riot-route/index.advanced.riot
This code differs a lot compared to the Basic example; here are the major changes:
- To print a component for each page, a loop is created on
state.pages
. Within each<route></route>
, the span HTML elements are used as Riot components by adding the is attribute:
<route each={ page in state.pages } path={ page.path }> <span is={ page.component } route={ route }></span> </route>
- Navigation drawer links are also generated thanks to the route configuration, which is accessible with
state.pages
:
<template each={ page in state.pages }> <a href={ page.href }> <i>{ page.icon }</i> <span class="max">{ page.name }</span> </a> <div if={ page.separator === true } class="medium-divider"></div> </template>
- The route object is used on the Javascript part to check if the current URL exists: A stream of routes is created to listen for route changes on the
onMounted () {}
Riot lifecycle. When a route changes, a function checks if the URL matches an existing route:
onMounted (props, state) { this.anyRouteStream = route('(.*)') this.anyRouteStream.on.value((path) => { this.update({ showNotFound: !this.state.pages.some(p => match(path.pathname, toRegexp(p?.path))) }) }) }, onUnmounted() { // When the component is unmounted, the stream is stopped. this.anyRouteStream.end() }
- For each component, the current route is passed as Props:
<span is={ r.component } route={ route }></span>
Within each component, the route is accessible with props.route
, for instance the c-inbox.riot file:
<p-inbox> <h2> Inbox </h2> <span>Filter: { props.route.searchParams.get('filter') }</span><br> <span>Order By: { props.route.searchParams.get('order') }</span> </p-inbox>
Now you can use query parameters to request an API when the page is loaded in the onMounted(){}
Riot lifecycle.
Conclusion
Riot Route lets you create the navigation easily, with a syntax that is always close to HTML standards.
One limitation of a Single Page Application is that it depends on a Backend/API to load the data. The browser/user has to wait until the content is rendered. To counter this issue, you can render HTML on the server side with Riot-SSR: the first request receives the page filled with data, so the user won't have to wait.
Have a great day! Cheers 🍻
Top comments (1)
This article concludes the "Mastering RiotJS" series. I hope it will help newcomers build sleek production front-ends with Riot!
Feel free to comment if you have questions about Riot Routes. Cheers 🍻