In context of a single page application (SPA) routing is a feature that allows you to show different content depending on the URL, but without reloading the page. To the user it looks like navigating different pages.
Routing can use either modern HTML5 History API to change URL completely or use hash URLs, storing route in the URL part after hash mark (#).
To facilitate routing Frint comes with with two packages: frint-router
and frint-router-react
.
frint-router
provides you with router services responsible for storing URL, notifying of URL
changes and matching it against different path. The services differ in how and where they store URL,
but provide identical API so you can freely replace them with each other. This package isn't bound
to any view library.
frint-router-react
is a React-specific package which complements frint-router
and makes routing
really easy if you chose React as your view library by providing some handy components.
You will need both frint-router
and frint-router-react
for this tutorial as well as frint
,
react
and frint-react
.
$ npm install --save frint-router frint-router-react frint react frint-react
Let's start by creating components. We will have two components for content pages: HomePage
and
AboutPage
. We are also going to have RootComponent
which will serve as
container for navigation links and content pages.
We will need to import React
for creating components as well as Link
and Route
components
from frint-router-react
.
import React from 'react';
import { Link, Route } from 'frint-router-react';
const HomePage = () => <div>Home page</div>;
const AboutPage = () => <div>About page</div>;
const RootComponent = () => {
return (
<div>
<div className="links">
<Link to="/" activeClassName="is-active" exact>[Home]</Link>
<Link to="/about" activeClassName="is-active">[About]</Link>
</div>
<div className="content">
<Route path="/" component={HomePage} exact />
<Route path="/about" component={AboutPage} />
</div>
</div>
);
};
Let's go over what happens here.
RootComponent
renders section with navigation links using Link
component. Link
component
creates an anchor or a button leading to a certain URL. It also applies CSS class is-active
(passed as activeClassName
prop) to the link element automatically when current URL matches the
one passed to the Link
.Route
component to switch between content pages. Whenever path
passed to
Route
component matches current URL it will render component
from the props
. Just as with
Link
you can choose whether it should be an exact match. You can also pass a Frint app as an app
prop instead of component
.There are three router services available:
BrowserRouterService
: uses modern HTML5 History APIHashRouterService
: for legacy browsersMemoryRouterService
: useful for testsFor the purpose of this tutorial we will use BrowserRouterService
:
import BrowserRouterService from 'frint-router/BrowserRouterService';
Now let's create an app that makes use of the RootComponent
and router
we've just created. We
pass them as providers to the app. Names of the providers are defined here by convention.
frint-react
requires that app's main component would be a provider named component
.frint-router-react
relies on router
provider name for Link
and Route
to get and set
current URL. If you want to use Link
or Route
in your child apps you'll need to cascade the
router
provider to them by adding cascade: true
import { createApp } from 'frint';
import BrowserRouterService from 'frint-router/BrowserRouterService';
const RouterApp = createApp({
name: 'RouterApp',
providers: [
{
name: 'component',
useValue: RootComponent
},
{
name: 'router',
useFactory: function() {
return new BrowserRouterService();
},
cascade: true,
},
],
});
We have everything ready to start the app. We will need frint-react
to render our app into HTML
element (with id root
in this case).
import { render } from 'frint-react';
window.app = new RouterApp();
render(
window.app,
document.getElementById('root')
);
Now if you open it in a browser you should see a page with [Home] and [About] links and 'Home page' content. If you click the links content will change accordingly.
Another useful feature of frint-router-react
is default route which allows you to render a
component or an app when no other route matched. To enable this behaviour add a Route
without a
path
to your group of Route
components and them wrap them into Switch
.
import { Route, Link, Switch } from 'frint-router-react';
const NotFoundPage = () => <div>Not found</div>;
const RootComponent = () => {
return (
<div>
<div className="content">
<Switch>
<Route path="/" component={HomePage} exact />
<Route path="/about" component={AboutPage} />
<Route component={NotFoundPage} />
</Switch>
</div>
</div>
);
};
Now whenever you navigate to URL that doesn't match /
or /about
you'll see NotFoundPage
rendered.
Note: in Switch
only the first matching Route
gets rendered.
It is also possible for child components to define their own subroutes. So for example if you want
AboutPage
to have subpages you can render Link
and Route
to it.
const AboutUs = () => <article>Some content about us...</article>;
const AboutThem = () => <article>Some content about them...</article>;
const AboutPage = ({ match }) => {
return (
<div>
<h2>About page</h2>
<ul>
<li><Link to={`${match.url}`}>About</Link></li>
<li><Link to={`${match.url}/us`}>About us</Link></li>
<li><Link to={`${match.url}/them`}>About them</Link></li>
</ul>
<Switch>
<Route path={`${match.url}/us`} component={AboutUs} />
<Route path={`${match.url}/them`} component={AboutThem} />
</Switch>
</div>
);
};
Because AboutPage
is rendered using Route
component it receives a match
object as a prop. This
object contains url
which let's you define routes relative to the current.
It is important that the parent Route
doesn't have exact
prop so that it would match all the URLs
starting with the path
.
In your path you can also define params which will be passed to your component once the path is
matched. Imagine we have a photo gallery where we show a list of photo names and by clicking on the
name you can see the name and the photo itself. Below you can find how it can be implemented by
using route params. PhotosPage
acts as a container with routing for two nested components: Photos
and Photo
.
const Photos = ({ match }) => {
return (
<div>
<h1>Photos</h1>
<ul>
<li><Link to={`${match.url}/1`}>Photo #1</Link></li>
<li><Link to={`${match.url}/2`}>Photo #2</Link></li>
<li><Link to={`${match.url}/3`}>Photo #3</Link></li>
</ul>
</div>
);
};
const Photo = ({ match }) => {
return (
<div>
<h1>Photo #{match.params.photoId}</h1>
<div>
<img src={`/static/img/photos/${match.params.photoId}`} />
</div>
</div>
);
}
const PhotosPage = ({ match }) => {
return (
<Switch>
<Route path={`${match.url}/:photoId`} component={Photo} />
<Route component={Photos} />
</Switch>
);
};
To learn more about usage of Frint routing you can take a look at another example from our monorepo.
Install frint-cli
package, then init the router
example and run it.
$ npm install -g frint-cli
$ mkdir my-frint-app
$ cd my-frint-app
$ frint init --example router
$ npm install
$ npm start
It will launch the example in your browser for you, after taking care of bundling.