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.
Meiosis Routing gives you programmable routes which you manage in application state, with the following benefits:
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.
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.*
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.
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:
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.
Routing
We'll store the current route in the application state, under route.current
:
{ route: { current: [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: Route
childSegment: Route
next(): routing
parentRoute(): route
childRoute(route): route
siblingRoute(route): 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.current);
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.current
property:
// You can also import { navigateTo, Actions } from "meiosis-routing/state"
const navigateTo = route => ({
route: { current: route }
});
const Actions = update => ({ navigateTo: route => 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, and sibling 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>
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.
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.
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:
[ 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 })]
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()]
});
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: { current: router.initialRoute } }
router.start()
: a function to call at application startup. We pass a navigateTo
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 as router.toPath(routing.parentRoute())
.router.locationBarSync()
: a function to call to keep the location bar in sync. Every time the
state changes, we call router.locationBarSync(state.route.current)
.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>
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.
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 route state and
returns the updated route state. You can use this function in an
accept function, in a Redux
reducer, and so on.
As an accept function, it looks like this:
// You can also import { accept } from "meiosis-routing/state"
const routeAccept = state => ({ route: routeTransition(state.route) });
With this, state.route
will contain leave
and arrive
properties with the routes that we left
and arrived to. We can then use the convenience functions that meiosis-routing
provides,
findRouteSegment
and whenPresent
, to perform any actions we want when leaving from or arriving
to a route:
import { findRouteSegment, whenPresent } from "meiosis-routing/state";
// in accept function, reducer, service, etc.
function cleanup(state) {
if (findRouteSegment(state.route.leave, Route.User())) {
// leaving User route segment, cleanup...
}
}
function loadDataForUser(state) {
// whenPresent is a convenience function to get the matching route segment
// so that we can have its params
whenPresent(
findRouteSegment(state.route.arrive, Route.User()),
arrive => {
const name = arrive.params.name;
// load data for user according to the value of 'name'...
}
);
}
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.
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.
Generated using TypeDoc