Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/quantizor/buttermilk
beautifully simple isomorphic routing for React projects
https://github.com/quantizor/buttermilk
isomorphic react routing
Last synced: 3 days ago
JSON representation
beautifully simple isomorphic routing for React projects
- Host: GitHub
- URL: https://github.com/quantizor/buttermilk
- Owner: quantizor
- License: mit
- Created: 2018-01-12T06:35:05.000Z (almost 7 years ago)
- Default Branch: main
- Last Pushed: 2023-11-28T17:10:27.000Z (about 1 year ago)
- Last Synced: 2024-12-01T09:37:47.777Z (21 days ago)
- Topics: isomorphic, react, routing
- Language: JavaScript
- Homepage: https://buttermilk.js.org/
- Size: 2.36 MB
- Stars: 110
- Watchers: 2
- Forks: 6
- Open Issues: 25
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
[![npm version](https://badge.fury.io/js/buttermilk.svg)](https://badge.fury.io/js/buttermilk) [![codecov](https://codecov.io/gh/probablyup/buttermilk/branch/master/graph/badge.svg)](https://codecov.io/gh/probablyup/buttermilk) [![downloads](https://img.shields.io/npm/dm/buttermilk.svg)](https://npm-stat.com/charts.html?package=buttermilk)
# buttermilk
- [buttermilk](#buttermilk)
- [installation](#installation)
- [usage](#usage)
- [basic example](#basic-example)
- [writing route configurations](#writing-route-configurations)
- [components](#components)
- [``](#router)
- [``](#routingstate)
- [``](#link)
- [utilities](#utilities)
- [`match(routes, url)`](#matchroutes-url)
- [`route()`](#route)
- [holistic example](#holistic-example)
- [without a bundler](#without-a-bundler)
- [more examples](#more-examples)
- [goals](#goals)
- [browser compatibility](#browser-compatibility)
- [internet explorer](#internet-explorer)## 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
```2. 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](https://reactjs.org/blog/2018/10/23/react-v-16-6.html#reactlazy-code-splitting-with-suspense).)
✅
```js
routes: [
{
path: '/',
render: () => React.lazy(() => import('./Home')),
},
{
path: '*',
render: () => NotFound,
},
];
```⛔️
```js
routes: [
{
path: '/',
render: () => import('./Home').then(mdl => mdl.default),
},
{
path: '*',
render: () => NotFound,
},
];
```3. ??
4. Profit!
## usage
Setting up `buttermilk` involves placing a `` component on your page and feeding it an array of route definitions. If you learn better by reverse engineering, check out the [holistic example](#holistic-example).
### basic example
```jsx
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 (
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:
```js
{
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
```js
render: () => HelloWorldPage,
```- Some JSX
```jsx
render: () =>Hi!,
```- A string
```js
render: () => 'Howdy!',
```- A `React.lazy`-wrapped dynamically-imported component
```js
render: () => React.lazy(() => import('./HelloWorld')),
```If it's a component class, Buttermilk will inject the [routing context](#routingstate) as props.
- A route that redirects to another path:
```js
{
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
#### ``
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 `` 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:
```js
/**
* 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,
```#### ``
A render prop higher-order component (HOC) for arbitrarily consuming routing state.
```jsx
{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 */;
}}```
#### ``
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 ``, `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](https://www.styled-components.com/) 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.
```jsx
Somewhere over the rainbow…
```
Available props:
```js
/**
* 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:
```js
import { Link } from 'buttermilk';
import styled from 'styled-components';const Anchor = styled.a`
color: red;
`;export default function StyledButtermilkLink(props) {
return ;
}
```### 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.
```js
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 `` setup for the server since we're doing all the work upfront to find the correct route:
```js
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(
);/**
* route.title below is an example arbitrary prop
* you could add to the route configuration if desired
*/
res.send(`
${route.title}
${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.
```js
// signature: route(url: String, addNewHistoryEntry: Boolean = true)route('/some/other/url');
```### misc
#### `RoutingContext`
Used with the [`useContext` react hook](https://reactjs.org/docs/hooks-reference.html#usecontext) to get access to `routingState` in your functional components. Just an alternative to the `RoutingState` render prop component.
```js
import { RoutingContext } from 'buttermilk';
import React, { useContext } from 'react';function MyComponent(props) {
const routing = useContext(RoutingContext);return
The current path is: {routing.location.pathname};
}
```### holistic example
See it live:
```jsx
/* 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 => (
My sweet website
Home
Kitter Blep!
Corg Blep!
{props.children}
);const NotFound = () => (
Oh noes, a 404 page!
{routing => (
No page was found with the path:
{routing.location.pathname}
)}
Let's go back home.
);const routes = [
{
path: '/',
render: () => React.lazy(() => import('./Home')),
},
{
path: '/blep/:animal',
render: routing => (
),
},
{
path: '*',
render: () => NotFound,
},
];const root = document.body.appendChild(document.createElement('div'));
ReactDOM.render(, 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:
- using Buttermilk, React, etc from CDN:## 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](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) constructor.
Note that [Babel](https://babeljs.io/) does not transpile/polyfill this for you, so bootstrapped setups such as those based on [Create React App](https://create-react-app.dev/) will still need to manually include a polyfill.
Suggested: [events-polyfill](https://github.com/lifaon74/events-polyfill)