Skip to content

⚪️ beautifully simple isomorphic routing for React projects

License

Notifications You must be signed in to change notification settings

timneutkens/buttermilk

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

78 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm version build status codecov downloads

buttermilk

installation

Grab the buttermilk package with your favorite package manager.

npm i buttermilk
yarn add buttermilk

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.

A basic, starter setup:

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,
          }
        ]}
      />
    );
  }
}

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*
fallback *
splat /foo/**/bar.html
functional yourValidationFunction(url)
regex ^(?=bar)/foo
query string ?foo=bar

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 should have this shape:

{
  path: String | RegExp | Function,
  redirect: String?,
  render: Function?,  // note that this is required if "redirect" is not passed
}

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 promise resolving to one of the above (great for async-loading pages)

    render: () => import('./HelloWorld').then(mdl => mdl.default),

If it's a component class, Buttermilk will inject the routing context as props.

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 promise resolving to JSX or a React component class.
     */
    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.
 */
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 value route, the routing context will be modified without reloading the page. Otherwise, it will act like a normal anchor link.

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,

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 definition 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');

holistic example

See it live: https://codesandbox.io/s/20q311nn6n

/* 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: () => import("./Home").then(mdl => mdl.default),
  },
  {
    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@1.1.0/dist/umd.js
https://unpkg.com/buttermilk@1.1.0/dist/umd.min.js

The exports will be accessible at window.Buttermilk. Note that this requires react >= 16.3 (window.React) and prop-types (window.PropTypes) to also be accessible in the window scope.

more examples

goals

  • centrally-managed routing
  • fast
  • first-class async support
  • HMR-friendly
  • obvious API
  • small
  • SSR

About

⚪️ beautifully simple isomorphic routing for React projects

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 99.9%
  • HTML 0.1%