Sailboat: A modern Router for React

jeswin
jeswin
Aug 20, 2017 · 6 min read

Summary of features:

  • Streaming UI Components (via generators)
  • Async route handling
  • Easy integration with Redux, MobX and the like
  • Server-side Rendering
  • Powerful nested routes
  • Avoid re-implementing features which already exist in HTML/JS
  • Minimal learning curve

GitHub Repo: https://github.com/nsoap-official/sailboat

NSOAP — A Routing Convention for JavaScript

If you know JavaScript, you already know the NSOAP convention.

Let’s look at a basic example for NSOAP in action for server-side routing.

//Consider the simple web service below
const myRoutes = {
greet(name) => `Hello ${name}`,
math: {
sum(a, b) => a + b,
zero: 0,
asyncSum(a, b) => Promise.resolve(a + b)
},
}
/*
NSOAP routes for this service will look like this:
/greet(jes) returns "Hello jes"
/math.sum(10,20) returns 30
/math.zero returns 0
/math.asyncSum(10,20) also returns 30
*/

Since it’s just JavaScript, you can do interesting things like chained routes.

const myRoutes = {
async getCustomer(id) {
const customer = await getCustomerFromDatabase(id);
return {
index: customer,
async details() {
const customerDetails = await customer.getDetails(id);
return customerDetails;
}
}
}
};
/*
NSOAP URLs:
/getCustomer(100) returns a customer object
/getCustomer(100).details returns customer details
*/

Alright, so that was NSOAP in a nutshell. To read more, see the documentation for NSOAP Express Router. Let’s get started with the official NSOAP Router for React — Sailboat.

Installation

npm install sailboat

Getting Started

Let’s start with a home page, which sits at the url “/”.

import React from "react";
import { Router, navigateTo } from "sailboat";
const Home = (
<div>
<h1>Welcome to Sailboat</h1>
<p>You are on the home page.</p>
</div>
);
const myApp = {
index: <HomePage />
}
//Load the home page when rendered.
navigateTo("/");
ReactDOM.render(Router(myApp), mountNode);

Ok, that was simple. Let’s now build a page which displays the sum of two numbers passed via a url. According to the NSOAP convention, your url is going to look like “/sum(10,20)”. Or if you want to use parameters, you could use “/sum(x,y)?x=10&y=20”.

We’ll also introduce here a component called Link, which navigates to the url when clicked. It renders an Anchor tag with its click handler invoking the navigateTo function seen previously, and sets the url in the browser’s address bar.

import { Router, navigateTo } from "sailboat";const Link => props => (
<a href="#" onClick={() => navigateTo(props.href)}>
{props.children}
</a>
);
const HomePage = props => (
<div>
<p>
<Link href="/sum(10,20)">
Sum of 10 and 20
</Link>
</p>
</div>
);
const Sum = props => (
<div>Sum is `${props.a + props.b}`</div>
)
const myApp = {
index: <HomePage />,
sum: (a,b) => <Sum a={a} b={b} />
}
ReactDOM.render(Router(myApp), mountNode);

That wasn’t so hard either. Let’s see how we can build more complex UIs. We’re going to introduce an alternate syntax for declaring Routes. It makes our routing more expressive.

//This...
const myApp = {
index: <HomePage />,
sum: (a,b) => <Sum a={a} b={b} />
}
//... is the same as
const myApp = {
index: () => [HomePage],
sum: (a, b) => [Sum, { a, b }]
};
ReactDOM.render(Router(myApp), mountNode);

What’s this good for? Child Routes. Read on.

Child Routes

  1. /team(teamId) — returns the TeamPage component
  2. /team(teamId).player(jerseyNumber) — returns PlayerComponent inside TeamPage
  3. /team(teamId).player(jerseyNumber).game(gameId) — returns GameComponent inside PlayerComponent inside TeamPage

Parent routes like /team(10) should be callable on their own, as well as along with child-components. eg: /team(10).player(2).game(23)

Sailboat has a short-hand syntax for this:

const myApp = {
team: teamId => [
TeamPage, //Component
{ teamId }, //Props
{ //Child routes
player: jerseyNumber => [
PlayerComponent, //Component
{ jerseyNumber }, //Props
{ //Child routes
game: gameId => [GameComponent, { gameId }]
}
]
}
]
};
ReactDOM.render(Router(myApp), mountNode);

This is how you’d define routes with Sailboat. Note that there was no need to define “index” routes to match just the parent.

Async Handlers and Streaming

const myApp = {
async team(teamId) {
const team = await getTeamFromDatabase();
return [
TeamPage,
{ team },
{
async player(jerseyNumber) {
const player = await team.getPlayer(jerseyNumber);
return [
PlayerComponent,
{ player },
{
game: gameId => [GameComponent, { gameId }]
}
];
}
}
];
}
};
ReactDOM.render(Router(myApp), mountNode);

A small problem with the code above is that the page will refresh only after all the async calls are complete. Ideally you should be showing a Spinner (“loading…” indicators) while waiting for the data to arrive. Right?

Sailboat lets you do that using generators. Here’s the rewritten player() function. (Some of the code is removed for brevity).

{
//....
async *player(jerseyNumber) {
//show a spinner
yield <Spinner />;
const player = await team.getPlayer(jerseyNumber);
//show the real thing
return [
PlayerComponent,
{ player },
{
game: gameId => [GameComponent, { gameId }]
}
];
}
};

It renders a Spinner while fetching the data. Once the data is available, it renders the actual PlayerComponent.

By the way, you can keep streaming HTML without ever returning. The following route streams seconds.

{
//....
async *seconds() {
let counter = 0;
while(true) {
yield <div>${counter} seconds have passed.</div>
await sleep(1);
counter++;
}
}
}

Works automatically with Redux and the like.

In the following example, a route change causes an action to be fired. The action could cause a change in state and thus re-render the UI.

//Callable as /getTeam(245)
const myApp = {
getTeam(teamId) {
actions.loadTeam(teamId);
}
};

Multiple instances of the Router

import { Router } from "sailboat";const routes1 = {
customers: { index: <CustomersPage /> }
}
const routes1 = {
orders: { index: <OrdersPage /> }
}
const Customers = Router(routes1);
const Order = Router(routes2);
const App =
<div>
<Customers />
<Orders />
</div>;
ReactDOM.render(<App />, mountNode);

It should bepossible to embed Sailboat instances in an app driven by another router such as React Router, or even inside an Angular or Backbone app.

Server-side Rendering

import { Router, navigateTo } from "sailboat";const myRoutes = { 
//....omitted for brevity
};
router.get("*", (req, res) => {
navigateTo(req.url).then(() => {
const content = ReactDOMServer.renderToString(Router(myRoutes));
res.render("index", { title: "Sail", data: false, content });
});
});

If you were rendering to the DOM, you could have called navigateTo after the ReactDOM.render() is called.

Sailboat versus React Router

What about nesting routes inside components like React Router v4?

Don’t like the dot notation?

//omitted for brevity
ReactDOM.render(Router(myApp, { useSlash: true }), mountNode);

Browser History

Example Apps

jeswin

Written by

jeswin

I have an idea.