Skip to content

beautifully simple isomorphic routing for React projects

License

Notifications You must be signed in to change notification settings

quantizor/buttermilk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm version codecov downloads

buttermilk

installation

Grab the buttermilk NPM module with your favorite package manager.

npm i buttermilk

migrating from v1

  1. Upgrade all react dependencies (and buttermilk, of course):
npm i react@latest react-dom@latest react-is@latest buttermilk@latest
  1. 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 because React.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,
  },
];
  1. ??

  2. 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 component

      render: () => 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/buttermilk@2.0.0/dist/standalone.js
https://unpkg.com/buttermilk@2.0.0/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

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