{"id":16592289,"url":"https://github.com/quantizor/buttermilk","last_synced_at":"2026-03-02T13:12:13.838Z","repository":{"id":31048118,"uuid":"117202905","full_name":"quantizor/buttermilk","owner":"quantizor","description":"beautifully simple isomorphic routing for React projects","archived":false,"fork":false,"pushed_at":"2023-11-28T17:10:27.000Z","size":2476,"stargazers_count":110,"open_issues_count":25,"forks_count":6,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-09-09T02:26:48.178Z","etag":null,"topics":["isomorphic","react","routing"],"latest_commit_sha":null,"homepage":"https://buttermilk.js.org/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quantizor.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"github":null,"patreon":"quantizor","open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2018-01-12T06:35:05.000Z","updated_at":"2024-02-22T16:04:07.000Z","dependencies_parsed_at":"2023-12-19T05:29:31.322Z","dependency_job_id":"0ef218e7-4c96-4348-82da-b1d8b1d6fd81","html_url":"https://github.com/quantizor/buttermilk","commit_stats":null,"previous_names":["quantizor/buttermilk","probablyup/buttermilk"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/quantizor/buttermilk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quantizor%2Fbuttermilk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quantizor%2Fbuttermilk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quantizor%2Fbuttermilk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quantizor%2Fbuttermilk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quantizor","download_url":"https://codeload.github.com/quantizor/buttermilk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quantizor%2Fbuttermilk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30003732,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-02T12:19:43.414Z","status":"ssl_error","status_checked_at":"2026-03-02T12:19:02.215Z","response_time":60,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["isomorphic","react","routing"],"created_at":"2024-10-11T23:20:14.274Z","updated_at":"2026-03-02T13:12:13.818Z","avatar_url":"https://github.com/quantizor.png","language":"JavaScript","funding_links":["https://patreon.com/quantizor"],"categories":[],"sub_categories":[],"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)\n\n# buttermilk\n\n\u003c!-- TOC --\u003e\n\n- [buttermilk](#buttermilk)\n  - [installation](#installation)\n  - [usage](#usage)\n    - [basic example](#basic-example)\n    - [writing route configurations](#writing-route-configurations)\n    - [components](#components)\n      - [`\u003cRouter\u003e`](#router)\n      - [`\u003cRoutingState\u003e`](#routingstate)\n      - [`\u003cLink\u003e`](#link)\n    - [utilities](#utilities)\n      - [`match(routes, url)`](#matchroutes-url)\n      - [`route()`](#route)\n    - [holistic example](#holistic-example)\n    - [without a bundler](#without-a-bundler)\n  - [more examples](#more-examples)\n  - [goals](#goals)\n  - [browser compatibility](#browser-compatibility)\n    - [internet explorer](#internet-explorer)\n\n\u003c!-- /TOC --\u003e\n\n## installation\n\nGrab the `buttermilk` NPM module with your favorite package manager.\n\n```\nnpm i buttermilk\n```\n\n## migrating from v1\n\n1. Upgrade all react dependencies (and buttermilk, of course):\n\n```\nnpm i react@latest react-dom@latest react-is@latest buttermilk@latest\n```\n\n2. 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).)\n\n✅\n\n```js\nroutes: [\n  {\n    path: '/',\n    render: () =\u003e React.lazy(() =\u003e import('./Home')),\n  },\n  {\n    path: '*',\n    render: () =\u003e NotFound,\n  },\n];\n```\n\n⛔️\n\n```js\nroutes: [\n  {\n    path: '/',\n    render: () =\u003e import('./Home').then(mdl =\u003e mdl.default),\n  },\n  {\n    path: '*',\n    render: () =\u003e NotFound,\n  },\n];\n```\n\n3. ??\n\n4. Profit!\n\n## usage\n\nSetting up `buttermilk` involves placing a `\u003cRouter\u003e` 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).\n\n### basic example\n\n```jsx\nimport { Router } from 'buttermilk';\nimport React from 'react';\n\n// whatever your folder structure looks like, etc.\nimport FooPage from '../foo';\nimport NotFoundPage from '../404';\n\nclass App extends React.Component {\n  render() {\n    return (\n      \u003cRouter\n        routes={[\n          {\n            path: '/foo',\n            render: () =\u003e FooPage,\n          },\n          {\n            path: '*',\n            render: () =\u003e NotFoundPage,\n          },\n        ]}\n      /\u003e\n    );\n  }\n}\n```\n\nWith 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.\n\n### writing route configurations\n\nButtermilk has a highly flexible matching system, offering the following flavors of routing:\n\n| flavor             | syntax                        |\n| ------------------ | ----------------------------- |\n| static             | `\"/foo\"`                      |\n| dynamic fragments  | `\"/foo/:id\"`                  |\n| optional fragments | `\"/foo(/bar)\"`                |\n| wildcard           | `\"/foo*\"`                     |\n| splat              | `\"/foo/**/bar.html\"`          |\n| query string       | `\"?foo=bar\"`                  |\n| fallback           | `\"*\"`                         |\n| function callback  | `yourValidationFunction(url)` |\n| regex              | `/^(?=bar)\\/foo/`             |\n\nThe 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.\n\nA route configuration can take two forms:\n\n- A route that renders something:\n\n  ```js\n  {\n    path: String | RegExp | Function,\n    render: Function,\n  }\n\n  // example\n\n  {\n    path: \"/\",\n    render: () =\u003e \"Hello world!\",\n  }\n  ```\n\n  Return whatever you'd like from the `render` function. A few ideas:\n\n  - A React component class\n\n    ```js\n    render: () =\u003e HelloWorldPage,\n    ```\n\n  - Some JSX\n\n    ```jsx\n    render: () =\u003e \u003cdiv\u003eHi!\u003c/div\u003e,\n    ```\n\n  - A string\n\n    ```js\n    render: () =\u003e 'Howdy!',\n    ```\n\n  - A `React.lazy`-wrapped dynamically-imported component\n\n    ```js\n    render: () =\u003e React.lazy(() =\u003e import('./HelloWorld')),\n    ```\n\n  If it's a component class, Buttermilk will inject the [routing context](#routingstate) as props.\n\n- A route that redirects to another path:\n\n  ```js\n  {\n    path: String | RegExp | Function,\n    redirect: String,\n  }\n\n  // example\n\n  {\n    path: \"/bar\",\n    redirect: \"/\",\n  }\n  ```\n\nYou 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.\n\n### components\n\n#### `\u003cRouter\u003e`\n\nThe 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.)\n\nIn the browser, use either a `\u003cLink\u003e` 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.)\n\nAvailable props:\n\n```js\n/**\n * Provide a spinner or something to look at while the promise\n * is in flight if using async routes.\n */\nloadingComponent: PropTypes.oneOfType([\n  PropTypes.func,\n  PropTypes.string,\n]),\n\n/**\n * An optional app runtime component. Think of it like\n * the \"shell\" of your app, so perhaps the outer container,\n * nav bar, etc. You'll probably want to put any \"Provider\"\n * type components here that are intended to wrap your\n * whole application.\n */\nouterComponent: PropTypes.oneOfType([\n  PropTypes.func,\n  PropTypes.string,\n]),\n\nroutes: PropTypes.arrayOf(\n  PropTypes.shape({\n\n    /**\n     * A RegExp, string, or function accepting the URL as\n     * an argument and returning a boolean if valid.\n     */\n    path: PropTypes.oneOfType([\n      PropTypes.instanceOf(RegExp),\n      PropTypes.string,\n      PropTypes.func,\n    ]).isRequired,\n\n    /**\n     * A string URL path to a different route. If this is given,\n     * then \"render\" is not required.\n     */\n    redirect: PropTypes.string,\n\n    /**\n     * A function that returns one of the following:\n     *\n     * 1. JSX.\n     * 2. A React component class.\n     * 3. A `React.lazy`-wrapped dynamic component import.\n     */\n    render: PropTypes.func,\n  }),\n).isRequired,\n\n/**\n * A hook for reacting to an impending route transition.\n * Accepts a promise and will pause the route transition\n * until the promise is resolved. Return false or reject\n * a given promise to abort the routing update.\n *\n * Provides currentRouting and nextRouting as arguments.\n */\nrouteWillChange: PropTypes.func,\n\n/**\n * A hook for reacting to a completed route transition. It\n * might be used for synchronizing some global state if\n * desired.\n *\n * Provides currentRouting and previousRouting as arguments.\n */\nrouteDidChange: PropTypes.func,\n\n/**\n * A hook for synchronizing initial routing state.\n *\n * Providers initialRouting as an argument.\n */\nrouterDidInitialize: PropTypes.func,\n\n/**\n * The initial URL to be used for processing, falls back to\n * window.location.href for non-SSR. Required for\n * environments without browser navigation eventing, like Node.\n */\nurl: PropTypes.string,\n```\n\n#### `\u003cRoutingState\u003e`\n\nA render prop higher-order component (HOC) for arbitrarily consuming routing state.\n\n```jsx\n\u003cRoutingState\u003e\n  {routingProps =\u003e {\n    // routingProps.location\n    // (the parsed current URL in window.location.* form)\n\n    // routingProps.params\n    // (any extracted dynamic params from the URL)\n\n    // routingProps.route\n    // (the current route)\n\n    return /* some JSX */;\n  }}\n\u003c/RoutingState\u003e\n```\n\n#### `\u003cLink\u003e`\n\nA 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.\n\n\u003e A _polymorphic_ component is one that can change shape as part of its public API. In the case of `\u003cLink\u003e`, `props.as` allows the developer to pass in their own base link component if desired.\n\u003e\n\u003e 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.\n\nIf something other than an anchor tag is specified via `props.as`, a `[role=\"link\"]` attribute will be added for basic assistive technology support.\n\nAdds `[data-active]` if the given href matches the active route.\n\n```jsx\n\u003cLink as=\"button\" href=\"/somewhere\" target=\"_blank\"\u003e\n  Somewhere over the rainbow…\n\u003c/Link\u003e\n```\n\nAvailable props:\n\n```js\n/**\n * An HTML tag name or valid ReactComponent class to\n * be rendered. Must be compatible with React.createElement.\n *\n * Defaults to an anchor \"a\" tag.\n */\nas: PropTypes.oneOfType([\n  PropTypes.func,\n  PropTypes.string,\n]),\n\n/**\n * A valid relative or absolute URL string.\n */\nhref: PropTypes.string.isRequired,\n\n/**\n * Any valid value of the anchor tag \"target\" attribute.\n *\n * See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target\n *\n * Defaults to \"_self\".\n */\ntarget: PropTypes.string,\n```\n\nAn example using a styled-components element base:\n\n```js\nimport { Link } from 'buttermilk';\nimport styled from 'styled-components';\n\nconst Anchor = styled.a`\n  color: red;\n`;\n\nexport default function StyledButtermilkLink(props) {\n  return \u003cLink {...props} as={Anchor} /\u003e;\n}\n```\n\n### utilities\n\n#### `match(routes, url)`\n\nThis 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.\n\n```js\nimport { match } from 'buttermilk';\n\nconst url = 'https://fizz.com/buzz';\n\nconst routes = [\n  {\n    path: '/foo',\n    render: () =\u003e FooPage,\n  },\n  {\n    path: '/bar',\n    render: () =\u003e BarPage,\n  },\n  {\n    path: '*',\n    render: () =\u003e NotFoundPage,\n  },\n];\n\nconst { location, params, redirect, route } = match(routes, url);\n```\n\nWhen using this API, you'll probably want to have a more streamlined `\u003cRouter\u003e` setup for the server since we're doing all the work upfront to find the correct route:\n\n```js\nimport { match, Router } from 'buttermilk';\nimport React from 'react';\nimport ReactDOMServer from 'react-dom/server';\n\nimport routes from '../routes';\n\n/**\n * An example express middleware.\n */\nexport default function renderingMiddleware(req, res, next) {\n  const url = req.protocol + '//' + req.get('host') + req.originalUrl;\n\n  const { location, params, redirect, route } = match(routes, url);\n\n  if (redirect) return res.redirect(redirect);\n\n  const html = ReactDOMServer.renderToString(\n    \u003cRouter\n      url={url}\n      routes={[\n        {\n          ...route,\n          path: '*',\n        },\n      ]}\n    /\u003e\n  );\n\n  /**\n   * route.title below is an example arbitrary prop\n   * you could add to the route configuration if desired\n   */\n  res.send(`\n    \u003c!doctype html\u003e\n    \u003chtml\u003e\n      \u003chead\u003e\u003ctitle\u003e${route.title}\u003c/title\u003e\u003c/head\u003e\n      \u003cbody\u003e${html}\u003c/body\u003e\n    \u003c/html\u003e\n  `);\n}\n```\n\n#### `route()`\n\nUse 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.\n\n```js\n// signature: route(url: String, addNewHistoryEntry: Boolean = true)\n\nroute('/some/other/url');\n```\n\n### misc\n\n#### `RoutingContext`\n\nUsed 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.\n\n```js\nimport { RoutingContext } from 'buttermilk';\nimport React, { useContext } from 'react';\n\nfunction MyComponent(props) {\n  const routing = useContext(RoutingContext);\n\n  return \u003cdiv {...props}\u003eThe current path is: {routing.location.pathname}\u003c/div\u003e;\n}\n```\n\n### holistic example\n\nSee it live: \u003chttps://codesandbox.io/s/2xrr26y2lp\u003e\n\n```jsx\n/* Home.js */\nexport default () =\u003e 'Home';\n\n/* index.js */\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport { Router, RoutingState, Link } from 'buttermilk';\n\nconst App = props =\u003e (\n  \u003cdiv\u003e\n    \u003cheader\u003e\n      \u003ch1\u003eMy sweet website\u003c/h1\u003e\n    \u003c/header\u003e\n\n    \u003cnav\u003e\n      \u003cLink href=\"/\"\u003eHome\u003c/Link\u003e\n      \u003cLink href=\"/blep/kitter\"\u003eKitter Blep!\u003c/Link\u003e\n      \u003cLink href=\"/blep/corg\"\u003eCorg Blep!\u003c/Link\u003e\n    \u003c/nav\u003e\n\n    \u003cmain\u003e{props.children}\u003c/main\u003e\n  \u003c/div\u003e\n);\n\nconst NotFound = () =\u003e (\n  \u003cdiv\u003e\n    \u003ch2\u003eOh noes, a 404 page!\u003c/h2\u003e\n    \u003cRoutingState\u003e\n      {routing =\u003e (\n        \u003cp\u003e\n          No page was found with the path:\n          \u003ccode\u003e{routing.location.pathname}\u003c/code\u003e\n        \u003c/p\u003e\n      )}\n    \u003c/RoutingState\u003e\n\n    \u003cp\u003e\n      \u003cLink href=\"/\"\u003eLet's go back home.\u003c/Link\u003e\n    \u003c/p\u003e\n  \u003c/div\u003e\n);\n\nconst routes = [\n  {\n    path: '/',\n    render: () =\u003e React.lazy(() =\u003e import('./Home')),\n  },\n  {\n    path: '/blep/:animal',\n    render: routing =\u003e (\n      \u003cimg\n        alt=\"Bleppin'\"\n        src={\n          routing.params.animal === 'corg'\n            ? 'http://static.damnlol.com/media/bc42fc943ada24176298871de477e0c6.jpg'\n            : 'https://i.imgur.com/OvbGwwI.jpg'\n        }\n      /\u003e\n    ),\n  },\n  {\n    path: '*',\n    render: () =\u003e NotFound,\n  },\n];\n\nconst root = document.body.appendChild(document.createElement('div'));\n\nReactDOM.render(\u003cRouter routes={routes} outerComponent={App} /\u003e, root);\n```\n\n### without a bundler\n\nYou can also use consume Buttermilk from a CDN like unpkg:\n\n```\nhttps://unpkg.com/buttermilk@2.0.0/dist/standalone.js\nhttps://unpkg.com/buttermilk@2.0.0/dist/standalone.min.js\n```\n\nThe exports will be accessible at `window.Buttermilk`. Note that this requires `react \u003e= 16.8` (`window.React`),`react-is \u003e= 16.8` (`window.ReactIs`), and `prop-types` (`window.PropTypes`) to also be accessible in the `window` scope.\n\nBoth the minified and development versions ship with source maps for ease of debugging.\n\n## more examples\n\n- holistic example + animated route transitions: \u003chttps://codesandbox.io/s/vvr16kyqy7\u003e\n- using Buttermilk, React, etc from CDN: \u003chttps://codesandbox.io/s/n3lq32ppxp\u003e\n\n## goals\n\n- centrally-managed routing\n- fast\n- first-class async support\n- HMR-friendly\n- obvious API\n- small\n- SSR\n\n## browser compatibility\n\n### internet explorer\n\nInternet Explorer requires a polyfill to support the [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) constructor.\n\nNote 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.\n\nSuggested: [events-polyfill](https://github.com/lifaon74/events-polyfill)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquantizor%2Fbuttermilk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquantizor%2Fbuttermilk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquantizor%2Fbuttermilk/lists"}