GithubHelp home page GithubHelp logo

quantizor / buttermilk Goto Github PK

View Code? Open in Web Editor NEW
110.0 2.0 6.0 2.42 MB

beautifully simple isomorphic routing for React projects

Home Page: https://buttermilk.js.org/

License: MIT License

JavaScript 99.77% HTML 0.23%
react routing isomorphic

buttermilk's Introduction

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/[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

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

buttermilk's People

Contributors

dependabot[bot] avatar nerdstep avatar quantizor avatar timneutkens avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

buttermilk's Issues

Child of Link component can't be React element anymore in v2.0.0

The following code worked in [email protected] but does not work in [email protected] anymore:

import { Link } from 'buttermilk';
// ...

<Link href="/">
  <span>Home</span>
</Link>

Now no error is thrown, but it just doesn't link. Nothing happens when it actually should route to /.

When debugging I found that the <Link /> component only works if it's child is a string, e.g. <Link href="/">Home</Link>.

I saw you didn't change cjs.js in your v2.0.0. update, but something must have changed which influences this..

Prevent reloading route

Is it possible to prevent a reload/refresh when user visits the current route they're already looking at?

Take the Router Challenge

The Router Challenge aims to be to Routers what TodoMVC is to MV* frameworks. It offers the same Music Catalog application built in React using different Routers. For it to be successful I need the help of Router writers like you. Will you take the Router Challenge and implement the Music Catalog application using buttermilk, please?

[email protected] package size increase

The .tgz file size increased from 76.4 kB to 719.3 kB

Is some extra website code packed under dist/?

npm notice 1.0MB  dist/app.c328ef1a.js
npm notice 1.7MB  dist/app.c328ef1a.map
npm notice 8.1kB  dist/home.fc46b7b9.js
npm notice 303B   dist/home.fc46b7b9.map
npm notice 79B    dist/index.html

Noticed from: https://packagephobia.now.sh/[email protected]


a diff between $ npm pack [email protected] $ npm pack [email protected] output:

@@ -1,31 +1,33 @@
-npm notice package: [email protected]
+npm notice package: [email protected]
 npm notice === Tarball Contents ===
 npm notice 0      package
 npm notice 1.1kB  LICENSE
-npm notice 14.1kB README.md
+npm notice 14.7kB README.md
 npm notice 0      dist
-npm notice 1.6kB  package.json
+npm notice 1.8kB  package.json

-npm notice 0      site
+npm notice 1.0MB  dist/app.c328ef1a.js
+npm notice 1.7MB  dist/app.c328ef1a.map

-npm notice 21.1kB dist/cjs.js
-npm notice 30.6kB dist/cjs.js.map
-npm notice 9.3kB  dist/cjs.min.js
-npm notice 25.8kB dist/cjs.min.js.map
+npm notice 16.4kB dist/cjs.js
+npm notice 26.6kB dist/cjs.js.map
+npm notice 7.4kB  dist/cjs.min.js
+npm notice 22.2kB dist/cjs.min.js.map

-npm notice 20.8kB dist/es.js
-npm notice 30.6kB dist/es.js.map
-npm notice 9.0kB  dist/es.min.js
-npm notice 25.8kB dist/es.min.js.map
+npm notice 16.0kB dist/es.js
+npm notice 26.6kB dist/es.js.map
+npm notice 7.0kB  dist/es.min.js
+npm notice 22.2kB dist/es.min.js.map

+npm notice 8.1kB  dist/home.fc46b7b9.js
+npm notice 303B   dist/home.fc46b7b9.map
+npm notice 79B    dist/index.html

-npm notice 21.6kB dist/standalone.js
-npm notice 32.3kB dist/standalone.js.map
-npm notice 8.0kB  dist/standalone.min.js
-npm notice 25.7kB dist/standalone.min.js.map
+npm notice 16.8kB dist/standalone.js
+npm notice 27.9kB dist/standalone.js.map
+npm notice 5.9kB  dist/standalone.min.js
+npm notice 22.1kB dist/standalone.min.js.map

-npm notice 0      site/.next
-npm notice 0      site/.next/dist

 npm notice === Tarball Details ===
 npm notice name:          buttermilk
-npm notice version:       1.1.2
-npm notice filename:      buttermilk-1.1.2.tgz
+npm notice version:       2.0.0
+npm notice filename:      buttermilk-2.0.0.tgz
-npm notice package size:  76.4 kB
+npm notice package size:  719.3 kB
-npm notice unpacked size: 277.2 kB
+npm notice unpacked size: 3.0 MB
-npm notice shasum:        e704ada0d0384570dfe3f5c7dd3a020f534794d3
-npm notice integrity:     sha512-ZY2gM6TEVqBBg[...]Lx/4uzsrg/1Kw==
+npm notice shasum:        f55bdff0de2af1c9b2b3a1051657632fac86abf5
+npm notice integrity:     sha512-wM3O1cSPHmARZ[...]NdTFoQ/c8OQ3g==
-npm notice total files:   20
+npm notice total files:   22

Trouble in understanding regex syntax

I'm trying to validate a route that will accept any routes like http://localhost:3000/123456 where I can access 123456 as props.params.some_id

This is what I've tried:

      <Router
        routes={[
          {
            path: /^[0-9]$/,
            render: () => SomeComponent
          }
        ]}
      />

However the route is not matching.

False redirect to main page with trailing slash /

2 issues:

Given the below routes structure:

  1. If I go to https://mypage.com/imprint everything works find and it shows me the Imprint page.
    But if I want to go to https://mypage.com/imprint/ (extra slash at the end) it redirects me to home, i.e. / again, but not to /imprint where I would expect it to go to.

  2. If I got to https://mypage.com/something it correctly shows me the 404 page.
    But if I go to https://mypage.com/something/ (extra slash at the end) it redirect me to home, although something is not a valid route and it should show the 404 page again!

My routes look as follows:

{
    path: '/',
    render: () => import('./components/Home/index').then(mdl => mdl.default)
},
{
    path: '/imprint',
    render: () => Imprint
},
{
    path: '*',
    render: () => NotFound
}

I think it has to do with URL Normalization to deal with trailing slashes.

Internet Explorer requires Event polyfill

Internet Explorer breaks due to usage of the new Event constructor.

It works fine after adding a polyfill, but it was somewhat difficult to track this issue down since the error appears as originating in react-dom, and at least with a Create React App bootstrapped app, you can't easily test IE in development.

So testing the production build you get this useful error:

SCRIPT445: Object doesn't support this action
react-dom.production.min.js (169,13)

Initially I was trying to figure out why Babel/core-js wasn't polyfilling this but it seems they're not focused on DOM related APIs.

Anyway, I'm opening this issue because it might help others avoid similar grief trying to debug the problem. Perhaps a quick mention in the README would be warranted?

Link component to set "active" on parent

I am using Bootstrap and I am trying to figure out a way to set active on the parent component on a Link. I am curious to know if there is a way to do this. This is what I have thus far:

import React from 'react';
import { Link } from 'buttermilk';

const RouteLink = ({route, text}) => (
  <li className="nav-item">
    <Link as="a" className="nav-link" href={route}>
      {text}
    </Link>
  </li>
);

export default () => (
  <nav className="sticky-top navbar navbar-expand-lg navbar-light bg-light">
    <a className="navbar-brand" href="/">Navbar</a>
    <div className="navbar-collapse" id="navbarNav">
      <ul className="navbar-nav">
        <RouteLink route='/' text='Home' />
        <RouteLink route='/about' text='About' />
        <RouteLink route='/404' text='FourOhFour' />
      </ul>
    </div>
  </nav>
);

Cleanly allow path to ignore query params?

With these routes:

const routes = [
    { path: '/page', render: Page },
    { path: '*', render: NotFound },
]

Calls to mypage/page?fbid=some_unique_id will render to the 404 page as query params are not ignored on the path. The workaround is to adjust the route object to become:

{ path: '/page*', render: Page },

I am wondering, is there is a cleaner approach?

<div location="[object Object]" params="[object Object]" route="[object Object]"> being rendered into DOM

import React from 'react'
import ReactDOM from 'react-dom'
import { Router } from 'buttermilk'
import { loadableReady } from '@loadable/component'

import App from './App'
import routes from './routes'

function Index() {
    return (
        <App>
            <Router routes={routes} />
        </App>
    )
}

ReactDOM.hydrate(<Index />, document.getElementById('root'))

Here is my entry point, and it's strange that this is being rendered into the DOM:

<div location="[object Object]" params="[object Object]" route="[object Object]">..</div>

Screenshot 2019-04-16 at 13 51 50

My routes look like:

import Page from './pages/Page'
import NotFound from './pages/NotFound'

const routes = [
    { path: '/page', render: Page },
    { path: '*', render: NotFound },
]

export default routes

So nothing out of the ordinary.

Error using dynamic imports with [email protected]

This might be an issue further upstream, but updating to [email protected] breaks dynamic imports.

Works fine using 16.11.0:

https://codesandbox.io/s/buttermilk-react-is-11-h2pke

But update to 16.12.0:

https://codesandbox.io/s/buttermilk-react-is-12-zjn9d

And we get the following error:

Error
A React component suspended while rendering, but no fallback UI was specified.

Add a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.
    in Unknown (created by Router)
    in div (created by Router)
    in Router (at src/index.js:11)
    in div (at src/index.js:8)
    in App (at src/index.js:20)

(todo) generated README from template

  • use react-docgen or something like it to scan the components file and extract some useful info
  • convert the README into a template that interpolates the result of docgen to fill in the various sections instead of manually copy & pasting code blocks
  • make sure the HMR experience when running the site works as expected with this strategy

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.