buttermilk
v2.0.1
Published
Beautifully simple isomorphic routing for React projects.
Downloads
110
Readme
buttermilk
installation
Grab the buttermilk
NPM module with your favorite package manager.
npm i buttermilk
migrating from v1
- Upgrade all react dependencies (and buttermilk, of course):
npm i react@latest react-dom@latest react-is@latest buttermilk@latest
- If you are dynamically-importing components for any routes, wrap the import in
React.lazy()
(note that this only works in the browser right now becauseReact.Suspense
doesn't work server-side yet.)
✅
routes: [
{
path: '/',
render: () => React.lazy(() => import('./Home')),
},
{
path: '*',
render: () => NotFound,
},
];
⛔️
routes: [
{
path: '/',
render: () => import('./Home').then(mdl => mdl.default),
},
{
path: '*',
render: () => NotFound,
},
];
??
Profit!
usage
Setting up buttermilk
involves placing a <Router>
component on your page and feeding it an array of route definitions. If you learn better by reverse engineering, check out the holistic example.
basic example
import { Router } from 'buttermilk';
import React from 'react';
// whatever your folder structure looks like, etc.
import FooPage from '../foo';
import NotFoundPage from '../404';
class App extends React.Component {
render() {
return (
<Router
routes={[
{
path: '/foo',
render: () => FooPage,
},
{
path: '*',
render: () => NotFoundPage,
},
]}
/>
);
}
}
With the above setup, a URL like "https://yoursite.com/foo"
would trigger the FooPage
component to be rendered. All other paths would trigger the NotFoundPage
component.
writing route configurations
Buttermilk has a highly flexible matching system, offering the following flavors of routing:
| flavor | syntax |
| ------------------ | ----------------------------- |
| static | "/foo"
|
| dynamic fragments | "/foo/:id"
|
| optional fragments | "/foo(/bar)"
|
| wildcard | "/foo*"
|
| splat | "/foo/**/bar.html"
|
| query string | "?foo=bar"
|
| fallback | "*"
|
| function callback | yourValidationFunction(url)
|
| regex | /^(?=bar)\/foo/
|
The only hard rule is there must be a fallback route at the end of the routing chain: path: "*"
. Otherwise, you are free to compose routes as it makes sense for your app.
A route configuration can take two forms:
A route that renders something:
{ path: String | RegExp | Function, render: Function, } // example { path: "/", render: () => "Hello world!", }
Return whatever you'd like from the
render
function. A few ideas:A React component class
render: () => HelloWorldPage,
Some JSX
render: () => <div>Hi!</div>,
A string
render: () => 'Howdy!',
A
React.lazy
-wrapped dynamically-imported componentrender: () => React.lazy(() => import('./HelloWorld')),
If it's a component class, Buttermilk will inject the routing context as props.
A route that redirects to another path:
{ path: String | RegExp | Function, redirect: String, } // example { path: "/bar", redirect: "/", }
You may also pass any other properties you'd like inside the route configuration object and they will be available to the RoutingState
higher-order component, routing callbacks, etc.
components
<Router>
The gist of Buttermilk's router is that it acts like a controlled component when used server-side (driven by props.url
) and an uncontrolled one client-side (driven by the value of window.location.href
and intercepted navigation events.)
In the browser, use either a <Link>
component or the route()
utility method to change routes. The router will also automatically pick up popstate
events caused by user-driven browser navigation (forward, back buttons, etc.)
Available props:
/**
* Provide a spinner or something to look at while the promise
* is in flight if using async routes.
*/
loadingComponent: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
/**
* An optional app runtime component. Think of it like
* the "shell" of your app, so perhaps the outer container,
* nav bar, etc. You'll probably want to put any "Provider"
* type components here that are intended to wrap your
* whole application.
*/
outerComponent: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
routes: PropTypes.arrayOf(
PropTypes.shape({
/**
* A RegExp, string, or function accepting the URL as
* an argument and returning a boolean if valid.
*/
path: PropTypes.oneOfType([
PropTypes.instanceOf(RegExp),
PropTypes.string,
PropTypes.func,
]).isRequired,
/**
* A string URL path to a different route. If this is given,
* then "render" is not required.
*/
redirect: PropTypes.string,
/**
* A function that returns one of the following:
*
* 1. JSX.
* 2. A React component class.
* 3. A `React.lazy`-wrapped dynamic component import.
*/
render: PropTypes.func,
}),
).isRequired,
/**
* A hook for reacting to an impending route transition.
* Accepts a promise and will pause the route transition
* until the promise is resolved. Return false or reject
* a given promise to abort the routing update.
*
* Provides currentRouting and nextRouting as arguments.
*/
routeWillChange: PropTypes.func,
/**
* A hook for reacting to a completed route transition. It
* might be used for synchronizing some global state if
* desired.
*
* Provides currentRouting and previousRouting as arguments.
*/
routeDidChange: PropTypes.func,
/**
* A hook for synchronizing initial routing state.
*
* Providers initialRouting as an argument.
*/
routerDidInitialize: PropTypes.func,
/**
* The initial URL to be used for processing, falls back to
* window.location.href for non-SSR. Required for
* environments without browser navigation eventing, like Node.
*/
url: PropTypes.string,
<RoutingState>
A render prop higher-order component (HOC) for arbitrarily consuming routing state.
<RoutingState>
{routingProps => {
// routingProps.location
// (the parsed current URL in window.location.* form)
// routingProps.params
// (any extracted dynamic params from the URL)
// routingProps.route
// (the current route)
return /* some JSX */;
}}
</RoutingState>
<Link>
A polymorphic anchor link component. On click/tap/enter if the destination matches a valid route, the routing context will be modified and the URL updated without reloading the page. Otherwise, it will act like a normal anchor link.
A polymorphic component is one that can change shape as part of its public API. In the case of
<Link>
,props.as
allows the developer to pass in their own base link component if desired.This might make sense if you use a library like styled-components and want to make a shared, styled anchor link component.
If something other than an anchor tag is specified via props.as
, a [role="link"]
attribute will be added for basic assistive technology support.
Adds [data-active]
if the given href matches the active route.
<Link as="button" href="/somewhere" target="_blank">
Somewhere over the rainbow…
</Link>
Available props:
/**
* An HTML tag name or valid ReactComponent class to
* be rendered. Must be compatible with React.createElement.
*
* Defaults to an anchor "a" tag.
*/
as: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
/**
* A valid relative or absolute URL string.
*/
href: PropTypes.string.isRequired,
/**
* Any valid value of the anchor tag "target" attribute.
*
* See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target
*
* Defaults to "_self".
*/
target: PropTypes.string,
An example using a styled-components element base:
import { Link } from 'buttermilk';
import styled from 'styled-components';
const Anchor = styled.a`
color: red;
`;
export default function StyledButtermilkLink(props) {
return <Link {...props} as={Anchor} />;
}
utilities
match(routes, url)
This is an advanced API meant primarily for highly-custom server side rendering use cases. Provide your array of route defintions and the fully-resolved URL to receive the matched route, route context, and any suggested redirect.
import { match } from 'buttermilk';
const url = 'https://fizz.com/buzz';
const routes = [
{
path: '/foo',
render: () => FooPage,
},
{
path: '/bar',
render: () => BarPage,
},
{
path: '*',
render: () => NotFoundPage,
},
];
const { location, params, redirect, route } = match(routes, url);
When using this API, you'll probably want to have a more streamlined <Router>
setup for the server since we're doing all the work upfront to find the correct route:
import { match, Router } from 'buttermilk';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import routes from '../routes';
/**
* An example express middleware.
*/
export default function renderingMiddleware(req, res, next) {
const url = req.protocol + '//' + req.get('host') + req.originalUrl;
const { location, params, redirect, route } = match(routes, url);
if (redirect) return res.redirect(redirect);
const html = ReactDOMServer.renderToString(
<Router
url={url}
routes={[
{
...route,
path: '*',
},
]}
/>
);
/**
* route.title below is an example arbitrary prop
* you could add to the route configuration if desired
*/
res.send(`
<!doctype html>
<html>
<head><title>${route.title}</title></head>
<body>${html}</body>
</html>
`);
}
route()
Use this API to programmatically change the route browser-side. It uses pushState
or replaceState
under the hood, depending on if you pass the second argument. Defaults to creating a new browser history entry.
// signature: route(url: String, addNewHistoryEntry: Boolean = true)
route('/some/other/url');
misc
RoutingContext
Used with the useContext
react hook to get access to routingState
in your functional components. Just an alternative to the RoutingState
render prop component.
import { RoutingContext } from 'buttermilk';
import React, { useContext } from 'react';
function MyComponent(props) {
const routing = useContext(RoutingContext);
return <div {...props}>The current path is: {routing.location.pathname}</div>;
}
holistic example
See it live: https://codesandbox.io/s/2xrr26y2lp
/* Home.js */
export default () => 'Home';
/* index.js */
import React from 'react';
import ReactDOM from 'react-dom';
import { Router, RoutingState, Link } from 'buttermilk';
const App = props => (
<div>
<header>
<h1>My sweet website</h1>
</header>
<nav>
<Link href="/">Home</Link>
<Link href="/blep/kitter">Kitter Blep!</Link>
<Link href="/blep/corg">Corg Blep!</Link>
</nav>
<main>{props.children}</main>
</div>
);
const NotFound = () => (
<div>
<h2>Oh noes, a 404 page!</h2>
<RoutingState>
{routing => (
<p>
No page was found with the path:
<code>{routing.location.pathname}</code>
</p>
)}
</RoutingState>
<p>
<Link href="/">Let's go back home.</Link>
</p>
</div>
);
const routes = [
{
path: '/',
render: () => React.lazy(() => import('./Home')),
},
{
path: '/blep/:animal',
render: routing => (
<img
alt="Bleppin'"
src={
routing.params.animal === 'corg'
? 'http://static.damnlol.com/media/bc42fc943ada24176298871de477e0c6.jpg'
: 'https://i.imgur.com/OvbGwwI.jpg'
}
/>
),
},
{
path: '*',
render: () => NotFound,
},
];
const root = document.body.appendChild(document.createElement('div'));
ReactDOM.render(<Router routes={routes} outerComponent={App} />, root);
without a bundler
You can also use consume Buttermilk from a CDN like unpkg:
https://unpkg.com/[email protected]/dist/standalone.js
https://unpkg.com/[email protected]/dist/standalone.min.js
The exports will be accessible at window.Buttermilk
. Note that this requires react >= 16.8
(window.React
),react-is >= 16.8
(window.ReactIs
), and prop-types
(window.PropTypes
) to also be accessible in the window
scope.
Both the minified and development versions ship with source maps for ease of debugging.
more examples
- holistic example + animated route transitions: https://codesandbox.io/s/vvr16kyqy7
- using Buttermilk, React, etc from CDN: https://codesandbox.io/s/n3lq32ppxp
goals
- centrally-managed routing
- fast
- first-class async support
- HMR-friendly
- obvious API
- small
- SSR
browser compatibility
internet explorer
Internet Explorer requires a polyfill to support the Event constructor.
Note that Babel does not transpile/polyfill this for you, so bootstrapped setups such as those based on Create React App will still need to manually include a polyfill.
Suggested: events-polyfill