meiosis-routing
v3.0.0
Published
Meiosis Helpers - Routing
Downloads
9
Maintainers
Readme
meiosis-routing
Meiosis is a pattern, not a library. Nevertheless, in response to popular demand and for your convenience, here are some reusable snippets of code that help setup and use Meiosis. This module provides support for routing in two modules:
state
: provides functions that help manage routing staterouter-helper
: provides support for configuring routes and creating a router. Out-of-the-box support is provided for these router libraries:
You can also plug in another router library of your choice.
Benefits!
Meiosis Routing gives you programmable routes which you manage in application state, with the following benefits:
- Simple route configuration
- No hardcoded paths in links
- Parent and child routes, and reusable child routes
- Relative navigation: navigate to a parent, sibling, or child route
- Navigate to the same route but with different parameters
- Redirect to a route after an action
- Authenticate / authorize before going to a route
- Load data (synchronously or asynchronously) when arriving at a route
- Clean up state when leaving a route
- Trigger arriving and leaving a route based on route and query parameters
- Prevent leaving a route to e.g. warn user of unsaved data
Because routing is managed in application state, you don't need a complex router with support for all of the above. The actual router is just a thin layer that matches URLs to routes. You can use one of the routers mentioned above, or plug in your own. feather-route-matcher is a nice example of how you only need a lightweight router library.
Installation
Using npm
:
npm i meiosis-routing
Using a script
tag:
<script src="https://unpkg.com/meiosis-routing"></script>
Using the script
tag exposes a MeiosisRouting
global, under which the helper functions are
provided:
state.*
routerHelper.*
Using meiosis-routing
PLEASE NOTE that this is a summary of how to use
meiosis-routing
. Refer to this tutorial for a more detailed explanation. Also refer to the tutorial to usemeiosis-routing
with Mithril.
Create Route Segments
Meiosis Routing is based on the idea of route segments, which are plain objects of the form
{ id, params }
. Then, a route is an array of route segments:
[{ id: "User", params: { name: "duck" }, { id: "Profile" } }]
Using an array of route segments opens up some nice possibilities:
- Navigating to same, parent, sibling, or child route
- Creating reusable child routes
- Managing pages and transitions independently of route paths
For convenience, meiosis-routing/state
provides the createRouteSegments
to which you provide an
array of strings that correspond to the route segments of your application:
import { createRouteSegments } from "meiosis-routing/state";
const Route = createRouteSegments([
"Home",
"Login",
"User",
"Profile",
"Preferences"
]);
Route.Home()
// returns { id: "Home", params: {} }
[Route.User({ name: "duck" }), Route.Profile()]
// returns [{ id: "User", params: { name: "duck" } }, { id: "Profile", params: {} }]
Now that we can create route segments and routes (arrays of route segments), let's use routing to manage them.
Use Routing
We'll store the current route in the application state, under route
:
{ route: [Route.Home()] }
Next, from our top-level component, we'll create an instance of Routing
, passing in the current
route. The routing
instance we get has these properties and functions:
localSegment: RouteSegment
childSegment: RouteSegment
next(): routing
parentRoute(): Route
childRoute(route): Route
siblingRoute(route): Route
sameRoute(params): Route
We can now render the top-level component according to the localSegment
id. We can use a simple
string
→Component
map to look up the corresponding component:
import { Routing } from "meiosis-routing/state";
import { Home, Login, User } from "./our-components";
const componentMap = { Home, Login, User };
const Root = ({ state }) => {
const routing = Routing(state.route);
const Component = componentMap[routing.localSegment.id];
return (
<div>
{/* ... */}
<Component /* other props... */ routing={routing} />
{/* ... */}
</div>
);
};
Then in the Component
, we can use routing.localSegment.params
to retrieve any params. Again we
can use a component map, now using routing.childSegment.id
. We can do this for as many levels as
we want, taking care to pass routing.next()
down to the child component so that we "advance" the
routing instance:
import { Profile, Preferences } from "./our-components";
const componentMap = { Profile, Preferences };
const User = ({ state, routing }) => {
const params = routing.localSegment.params;
const Component = componentMap[routing.childSegment.id];
return (
<div>
{/* ... */}
<Component /* other props... */ routing={routing.next()} />
{/* ... */}
</div>
);
};
To navigate to a route, we can use a simple action that updates the state's route
property:
const navigateTo = route => ({ nextRoute: () => Array.isArray(route) ? route : [route] });
const Actions = update => ({ navigateTo: route => update(navigateTo(route)) });
const update = ...;
const actions = Actions(update);
// ...
<a href="#" onClick={() => actions.navigateTo(
[Route.User({ id: userId }), Route.Profile()]
)}>
User Profile
</a>
We can also navigate to a parent route, child route, sibling route, and same route:
// Say we are in [Route.User({ name }), Route.Profile()].
// This navigates to [Route.User({ name })]
<a href="#" onClick={() => actions.navigateTo(
routing.parentRoute()
)}>
User
</a>
// Say we are in [Route.User({ name })].
// This navigates to [Route.User({ name }), Route.Profile()]
<a href="#" onClick={() => actions.navigateTo(
routing.childRoute(Route.Profile())
)}>
Profile
</a>
// Say we are in [Route.User({ name }), Route.Profile()].
// This navigates to [Route.User({ name }), Route.Preferences({ name })]
<a href="#" onClick={() => actions.navigateTo(
routing.siblingRoute(Route.Preferences({ name }))
)}>
Preferences
</a>
// Say we are in [Route.User({ name: "name1" })].
// This navigates to [Route.User({ name: "name2" })]
<a href="#" onClick={() => actions.navigateTo(
routing.sameRoute({ name: "name2" }))
)}>
Preferences
</a>
Note that you can also pass an array of route segments to childRoute
and siblingRoute
.
We now have an application that uses routing and works just fine without a router and without paths. To add paths, we can plug in a simple router library.
Create and use a Router
Adding a router gives us the ability to generate paths and put them in the href
attribute of our
links. The path will show in the browser's location bar, users can use the back and forward
buttons, bookmark links, and so on.
What's nice is that we can continue using programmatic routes as we've done so far. Route paths are generated from routes, so we never have to hardcode paths or mess with them in our application's routing logic.
Route Configuration
First, we create a route configuration. This is a plain object with id
→config
mappings,
where id
is the id of the route segment, and config
can either be:
- a string: the route path
- an array:
[ path, (optional) array of parameters from the parent, nested route config ]
For example:
const routeConfig = {
Home: "/",
User: ["/user/:name", {
Profile: "/profile",
Preferences: ["/preferences", ["name"]]
}]
}
This gives us the following path → route mappings:
/
→[Route.Home()]
/user/:name
→[Route.User({ name })]
/user/:name/profile
→[Route.User({ name }), Route.Profile()]
/user/:name/preferences
→[Route.User({ name }), Route.Profile({ name })]
Create the Router
Next, we create a router. The router libraries mentioned at the top of the page are supported
out-of-the-box. Let's use feather-route-matcher
:
import createRouteMatcher from "feather-route-matcher";
import { createFeatherRouter } from "meiosis-routing/router-helper";
const routeConfig = { ... };
const router = createFeatherRouter({
createRouteMatcher,
routeConfig,
defaultRoute: [Route.Home()]
});
Use the Router
This gives us a router
with:
router.initialRoute
: the initial route as parsed from the browser's location bar. We can use this in our application's initial state,{ route: router.initialRoute }
router.start()
: a function to call at application startup. We pass anavigateTo
callback for route changes:router.start({ navigateTo: actions.navigateTo })
router.toPath(route)
: converts a route into a path. For example,router.toPath([Route.Home()])
or a relative route such asrouter.toPath(routing.parentRoute())
.router.locationBarSync()
: a function to call to keep the location bar in sync. Every time the state changes, we callrouter.locationBarSync(state.route)
.
Now that we have router.toPath
, we no longer need to have href="#"
and onClick={...}
in our
links. Instead, we can use router.toPath()
in href
:
// Say we are in [Route.User({ name }), Route.Profile()].
// This navigates to [Route.User({ name })]
<a href={router.toPath(routing.parentRoute())}>
User
</a>
// Say we are in [Route.User({ name })].
// This navigates to [Route.User({ name }), Route.Profile()]
<a href={router.toPath(routing.childRoute(Route.Profile()))}>
Profile
</a>
// Say we are in [Route.User({ name }), Route.Profile()].
// This navigates to [Route.User({ name }), Route.Preferences({ name })]
// Notice that we don't have to specify ({ name }) in Route.Preferences(),
// since it is a parameter that is inherited from the parent route segment.
<a href={router.toPath(routing.siblingRoute(Route.Preferences()))}>
Preferences
</a>
// Say we are in [Route.User({ name: "name1" })].
// This navigates to [Route.User({ name: "name2" })]
<a href={router.toPath(routing.sameRoute({ name: "name2" }))}>
Profile
</a>
(Optional) Use Query Strings
We can use query strings by plugging in a query string library such as:
Note that query strings work out-of-the-box with Mithril. Refer to the routing tutorial for information on using
meiosis-routing
with Mithril.
To use a query string library, we just need to specify it as queryString
when creating the router:
import createRouteMatcher from "feather-route-matcher";
import qs from "qs";
const router = createFeatherRouter({
createRouteMatcher,
queryString: qs,
routeConfig,
defaultRoute: [Route.Home()]
});
Then, we specify query string parameters in our route configuration using ?
and/or &
:
const routeConfig = {
Home: "/",
User: ["/user/:name?param1", {
Profile: "/profile?param2¶m3",
Preferences: ["/preferences", ["name"]]
}]
};
The parameters will be available in our route segments just like path parameters.
Use Transitions
It's often desirable to load data when arriving at a route, clear data when leaving a route, guard a route to restrict access, and so on. We can do them with route transitions.
meiosis-routing
provides a routeTransition
function that takes the current and next route state
and returns a route transition object, { leave: {...}, arrive: {...} }
. You can use this function
in a service function to update the
state with the route transition.
As a service function, it looks like this:
const service = state => ({
routeTransition: () => routeTransition(state.route, state.nextRoute),
route: state.nextRoute
});
With this, state.routeTransition
will contain leave
and arrive
properties with the routes that
we left and arrived to, keyed by route id. We can then use this to perform any actions we want when
leaving from or arriving to a route:
// in service function, reducer, etc.
function loadDataForUser(state) {
if (state.routeTransition.arrive.User) {
const name = state.routeTransition.arrive.User.params.name;
// load data for user according to the value of 'name'...
}
}
function cleanup(state) {
if (state.routeTransition.leave.User) {
// leaving User route segment, cleanup...
}
}
For More Details
As mentioned above, you will find a more in-depth tutorial in the Meiosis Routing documentation.
More details are also available in the API documentation.
Changelog
Version 3.0.0: Works with
feather-route-matcher
version4.x
. Breaking changes: removednavigateTo
andActions
since they were limited to working with Mergerino. These should instead be implemented in application code.Version 2.1.0: Works with
feather-route-matcher
version3.x
.
Credits
Many thanks to Stephan Thon for experimenting with early versions, testing and reporting bugs, and providing feedback and suggestions. Your help is very valuable and much appreciated!
meiosis-routing is developed by foxdonut (@foxdonut00) and is released under the MIT license.