Creating a Router
The Router#
The router is the controller of the single-page application. A router is created using a history
object and a routes
array.
import { curi } from '@curi/router';
const history = Browser();
const routes = [...];
const router = curi(history, routes);
History#
The history
object manages navigation between locations within an application.
There are three types of history
to choose from; which one you use depends on where your application is running.
The
browser
history is used for applications running in a browser.If you use the
browser
history, your application should be hosted on a server that can handle dynamic requests. This either means a server with real time route matching (like an Express server) or through configuration where a fallback page is served when the request doesn't map to a real file on the server.You can use
@curi/static
to generate static HTML pages for abrowser
history powered application.import Browser from "@hickory/browser"; const browserHistory = Browser();
The
hash
history is a fallback history for applications running in a browser.A
hash
history should only be used if you cannot configure the server to respond to requests that don't match files on the server. Most static file hosts are configurable, so you probably don't need to use this.import Hash from "@hickory/hash"; const hashHistory = Hash();
The
in-memory
history is used for applications not running in a browser. For example, thein-memory
history is used on the server, in a React Native app, and during testing.import InMemory from "@hickory/in-memory"; const inMemoryHistory = InMemory();
If you are not familiar with how single-page applications interact with a server, this article should help: Single-Page Applications and the Server.
Locations#
The history
object will map URLs into location objects. Only the pathname
, query
(search), and hash
segments are used; locations ignore the domain and protocol segments of a URL.
When matching loactions to routes, only the pathname
is used.
// https://www.example.com/page?key=value#trending
location = {
pathname: "/page",
query: "key=value"
hash: "trending"
}
The query
value of a location is a string by default, but the history object can be configured to automatically parse it into an object.
You can choose whichever query parsing/stringifying package you prefer. Some of the most popular are qs
, query-string
, and querystring
.
import { parse, stringify } from "qs";
import Browser from "@hickory/browser";
const history = Browser({
query: { parse, stringify }
});
// https://www.example.com/page?key=value#trending
location = {
pathname: "/page",
query: { key: "value" }
hash: "trending"
}
For more details on the history objects and their APIs, please check out the Hickory documentation.
Routes#
The routes
array defines the valid locations within an application. When the router receives a new location, it matches it against the routes to generate a response.
Each route is an object that has a unique name
and a path
string, which defines what locations the route matches.
path
strings do not start with a slash.Routes can be nested. A child route's path
will build on the paths from any ancestor routes.
You will almost always want to include a "catch all" route to match any "invalid" locations and render a 404 page. The path "(.*)"
matches every location. During development, Curi will log a warning in the console to let you know if you forgot to include a catch all route.
const routes = [
{
name: "Home",
path: ""
},
{
name: "Album",
path: "photos/:albumID",
children: [
{
name: "Photo",
// matches /photos/6789/12345 with params
// { albumID: "6789", photoID: "12345" }
path: ":photoID"
}
]
},
{
name: "Not Found",
path: "(.*)"
}
];
Route names#
Why do routes have names? Curi lets you interact with routes using their names.
For example, Curi provides a pathname
route interaction to generate the pathname
of a location to navigate to. Instead of manually writing pathname
strings, you tell Curi the name of the route that you want to navigate to (and also any required params) and Curi will create the pathname
for you.
const pathname = router.route.pathname(
"Photo",
{
albumID: "abcd",
photoId: "98765"
}
);
// pathname = "/photos/abcd/98765"
Response Handlers#
When the router has created a response, it emits it to any response handlers. There are three types of response handlers: observers, one time functions, and side effects.
Side effects are passed to the router when you are creating it. You can read more about them in the side effects guide.
One time functions are passed to the router using router.once()
. These are response handlers that will only be called one time. If a response already exists, then the response handler will be called immediately (unless configured not to). Otherwise, the one time response handler will be called after the next response is emitted.
Observers are passed to the router using router.observe()
. Unlike one time functions, these will be called for every response emitted by the router (until you tell the router to stop calling it). You most likely will not need to call this yourself. The different framework implementations (@curi/react-dom
, @curi/react-native
, @curi/vue
, and @curi/svelte
) setup observers for you.
const router = curi(history, routes);
const stop = router.observe(({ response }) => {
console.log('new response!', response);
});
// ...
stop();
// no longer observing
If you have any asynchronous routes (routes with resolve
functions), router.once()
should be used to delay the initial render until after the initial response is ready.
// wait for initial response to be ready
router.once(() => {
// safe to render async routes now
});
Rendering#
Rendering is left to whatever rendering library you are using. The way that Curi interfaces with each of them varies, but they all use observers to be notified when there is a new response.
@curi/react-dom
uses a <Router>
with a render-invoked children
function that will be called whenever there is a new response.
In React applications, response.body
should be a React component, so rendering the application means creating an element from response.body
.
The React Basics Tutorial gets into more detail about how this works.
// React
const Router = curiProvider(router);
ReactDOM.render((
<Router>
{({ response }) => {
const { body:Body } = response;
return <Body />;
}}
</Router>
), document.getElementById('root'));
@curi/vue
sets up reactive objects that update when there is a new response. <component :is>
can be used to render the body
component.
The Vue Basics Tutorial details how to use Vue and Curi.
// Vue
Vue.use(CuriPlugin, { router });
new Vue({
el: '#app',
template: '<app />',
components: { app }
});
@curi/svelte
uses the Svelte store and <svelte:component>
to render.
// Svelte
const store = curiStore(router);
new app({ target, store });