@reactions/router

0.0.0-alpha.10 • Public • Published

Reactions Router

Next generation routing for React.

Installation

yarn add @reactions/router

And then import it:

import { Router, Link, Redirect } from "@reactions/router";

Or use script tags and globals.

<script src="https://unpkg.com/@reactions/router"></script>
<script>
ReactionsRouter.Router;
ReactionsRouter.Link;
ReactionsRouter.Redirect;
</script>

Why?

  • I needed a router to play with React Suspsense, this showed up in my editor and I loved it.
  • It appears from npm stats that about 1/2 of React Router downloads haven't upgraded to v4. Why? I think two things:
    • Backwards compatibility. This router brings a commitement to backwards compatibility and smooth upgrade paths like React itself, starting with pretty good backwards compatibility with React Router v3. It's here to help you keep up with the great new stuff React is offering.
    • Nested route configuration was awesome. This brings that back without losing any of what made React Router v4 innovative (actually rendering components).
  • Also, don't you think it's about time we had relative links?

How To

Rendering

Routers select a child to render based on the child's path. The children are just other components that could be rendered on their own outside of a Router.

import { React } from "react";
import { render } from "react-dom";
import { Router, Link } from "@reactions/router";

let Home = () => <div>Home</div>;
let Dash = () => <div>Dash</div>;

render(
  <Router>
    <Home path="/" />
    <Dash path="dashboard" />
  </Router>
);

Navigate With Link

To navigate around the app, render a Link somewhere.

let Home = () => (
  <div>
    <h1>Home</h1>
    <nav>
      <Link to="/">Home</Link> | <Link to="dashboard">Dashboard</Link>
    </nav>
  </div>
);

let Dash = () => <div>Dash</div>;

render(
  <Router>
    <Home path="/" />
    <Dash path="dashboard" />
  </Router>
);

Parse Data From the URL

If you need to parse the data out of the URL, use a dynamic segment--they start with a :. The parsed value will become a prop sent to the matched component.

// at url "/invoice/23"

render(
  <Router>
    <Home path="/" />
    <Invoice path="invoice/:invoiceId" />
  </Router>
);

const Invoice = props => (
  <div>
    <h1>Invoice {props.invoiceId}</h1>
  </div>
);

It's the same as rendering the component directly.

<Invoice invoiceId={23} />

Ambiguous Paths and Ranking

Even though two paths might be ambiguous--like "/:invoiceId" and "/invoices"--Router ranks the paths and renders the one that makes the most sense.

render(
  <Router>
    <Home path="/" />
    <Invoice path=":invoiceId" />
    <InvoiceList path="invoices" />
  </Router>
);

The URL "/invoices" will render <Invoices/> and "/123" will render <Invoice invoiceId={123}/>. Same thing with the Home component. Even though it’s defined first, and every path will match "/", Home won't render unless the path is exactly "/". So don't worry about the order of your paths.

Nested Component Paths

You can nest components inside of a Router, and the paths will nest too. The matched child component will come in as the children prop, the same as if you'd rendered it directly. (Internally Router just renders another Router, but I digress...)

const Dash = ({ children }) => (
  <div>
    <h1>Dashboard</h1>
    <hr />
    {children}
  </div>
);

render(
  <Router>
    <Home path="/" />
    <Dash path="dashboard">
      <Invoices path="invoices" />
      <Team path="team" />
    </Dash>
  </Router>
);

If the URL is "/dashboard/invoices" then the Router will render <Dash><Invoices/></Dash>. If it's just "/dashboard", children will be null and we’ll only see <Dash/>.

Most apps probably have some sort of global chrome/navigation, that works out just fine:

const Main = ({ children }) => (
  <div>
    <h1>Welcome to the App!</h1>
    <ul>
      <li>
        <Link to="dashboard">Dashboard</Link>
      </li>
      <li>
        <Link to="invoices">Invoices</Link>
      </li>
    </ul>
    <hr />
    {children}
  </div>
);

render(
  <Router>
    <Main path="/">
      <Invoices path="invoices" />
      <Dash path="dashboard" />
    </Main>
  </Router>
);

Relative Links

You can link to relative paths. The relativity comes from the path of the component that rendered the Link. These two links will link to "/dashboard/invoices" and "/dashboard/team" because they're rendered inside of <Dash/>. This is really nice when you change a parent's URL, or move the components around.

render(
  <Router>
    <Home path="/" />
    <Dash path="dashboard">
      <Invoices path="invoices" />
      <Team path="team" />
    </Dash>
  </Router>
);

const Dash = ({ children }) => (
  <div>
    <h1>Dashboard</h1>
    <nav>
      <Link to="invoices">Invoices</Link> <Link to="team">Team</Link>
    </nav>
    <hr />
    {children}
  </div>
);

This also makes it trivial to render any section of your app as its own application with its own router. If all your links are relative, it can be embedded inside any other router and just work.

"Index" Paths

Nested components can use the path / to signify they should render at the path of the parent component, like an index.html file inside a folder on a static server. If this app was at "/dashboard" we'd see this component tree: <Dash><DashboardGraphs/></Dash>

render(
  <Router>
    <Home path="/" />
    <Dash path="dashboard">
      <DashboardGraphs path="/" />
      <InvoiceList path="invoices" />
    </Dash>
  </Router>
);

Not Found "Default" Components

Put a default prop on a component and Router will render it when nothing else matches.

const NotFound = () => <div>Sorry, nothing here.</div>;

render(
  <Router>
    <Home path="/" />
    <Dash path="dashboard">
      <DashboardGraphs path="/" />
      <InvoiceList path="invoices" />
    </Dash>
    <NotFound default />
  </Router>
);

Multiple Routers

If you want to match the same path in two places in your app, just render two Routers. Again, a Router picks a single child to render based on the URL, and then ignores the rest.

render(
  <div>
    <Sidebar>
      <Router>
        <HomeNav path="/" />
        <DashboardNav path="dashboard" />
      </Router>
    </Sidebar>

    <MainScreen>
      <Router>
        <Home path="/">
          <About path="about" />
          <Support path="support" />
        </Home>
        <Dash path="dashboard">
          <Invoices path="invoices" />
          <Team path="team" />
        </Dash>
      </Router>
    </MainScreen>
  </div>
);

Nested Routers

You can render a router anywhere you want in your app, even deep inside another Router, just makes sure to use a splat (*) on the parent component so nested paths match it.

render(
  <Router>
    <Home path="/" />
    <Dash path="dashboard/*" />
  </Router>
);

const Dash = () => (
  <div>
    <p>A nested router</p>
    <Router>
      <DashboardGraphs path="/" />
      <InvoiceList path="invoices" />
    </Router>
  </div>
);

This allows you to have all of your routes configured at the top of the app, or to configure only where you need them, which is really helpful for code-splitting and very large apps. You can even render Dash as an independent application.

Navigating Programmatically

If you need to navigate programmatically (like after a form submits), import navigate.

import { navigate } from "@reactions/router";

const Invoices = () => (
  <div>
    <NewInvoiceForm
      onSubmit={async event => {
        const newInvoice = await createInvoice(event.target);
        navigate(`/invoices/${newInvoice.id}`);
      }}
    />
  </div>
);

Or better, yet, use props.navigate passed to your route components and then you can navigate to relative paths:

const Invoices = ({ navigate }) => (
  <div>
    <NewInvoiceForm
      onSubmit={async event => {
        const newInvoice = await createInvoice(event.target);
        // can navigate to relative paths
        navigate(newInvoice.id);
      }}
    />
  </div>
);
<Router>
  <Invoices path="invoices" />
  <Invoice path="invoices/:id" />
</Router>;

Navigate returns a promise so you can await it. It resolves after React is completely finished rendering the next screen, even with React Suspense.

class Invoices extends React.Component {
  state = {
    creatingNewInvoice: false
  };

  render() {
    return (
      <div>
        <LoadingBar animate={this.state.creatingNewInvoice} />
        <NewInvoiceForm
          onSubmit={async event => {
            this.setState({ creatingNewInvoice: true });
            const newInvoice = await createInvoice(event.target);
            await navigate(`/invoice/${newInvoice.id}`);
            this.setState({ creatingNewInvoice: false });
          }}
        />
        <InvoiceList />
      </div>
    );
  }
}

React Suspense Ready (as it can be, ofc)

History Stack Handling

With React Suspense a user may click on a link, and while data is loading they may change their mind and click on a different link. The browser navigation history will contain all three entries:

/first-page -> /cancelled-page -> /desired-page

If they click the back button from /desired-page, they'll get an unexpected /cancelled-paged screen. They didn't see it on their way to /desired-page so it doesn’t make sense for it to be there on the way back to /first-page.

Reactions Router will not add the cancelled page to the history stack, so it would look like this:

/first-page -> /desired-page

Now when the user clicks back, they don’t end up on a page they never even saw. This is how browsers work with plain HTML pages, too.

Low Priority Updates

Router's state changes are marked as "low priority". It's very common to hook a user input up to a query string in the URL. Every time the user types, the url updates, and then React rerenders. Router state is given low priority so these inputs will not bind the CPU like they would have otherwise.

API

import { Router, Link, Redirect, MatchPath, navigate } from "@reactions/router";
<Router>
  <AnyComponent path={string} />
  <AnyComponent path={string}>
    <AnyComponent path={relativeString} />
  </AnyComponent>
  <AnyComponent default />
  <Redirect from={string} to={string} />
</Router>
<Link to={str} replace={bool} state={obj} {...anchorProps} />
navigate(to);
navigate(to, { replace, state });

await navigate(...)
navigate.then(...)
<MatchPath path={string}>
  {({ match, location, navigate }) => (...)}
</MatchPath>

React Router v4 Compatibility

  • Remove <BrowserRouter>, don't need it.
  • Swap out <Switch> for <Router>
    • Swap out <Route> inside a <Switch> for just your component:
      • this: <Route path="/users" component={Users} />
      • becomes <Users path="/users" />
    • remove exact props, it'll know what you meant
  • Swap out <Route> outside of <Switch> for <MatchPath>
    • Remove exact props
      • exact match <MatchPath path="/foo" />
      • partial match <MatchPath path="/foo/*" />

React Router v3 Backward Compatibility

Because Reactions Router brings back nested route configuration, it was plausible to add in a lot of backward compatibility for React Router v3 (😍). While not everything is supported, some React Router v3 apps will be able to drop in Reactions Router without any code changes (almost all of the RR v3 demos worked w/o modification). Other apps will need to make a few tweaks and you're on your way.

First, install Reactions Router

yarn add @reactions/router

Then do a find/replace across your app:

// from
import { Router, Link, Route } from 'react-router'

// to
import { Router, Link, Route } from '@reactions/router/compat'

Next, cross your fingers and hope everything still works.

FYI, I'd love to help out with this for a few people, DM me on twitter and we'll do some pairing.

Unsupported RRv3 API

Here's what's not supported (and probably never will be), check if your app is using these APIs:

  • Custom histories - whatever you were doing there, you can do in a top-level component somewhere.
  • hashHistory - Sorry, won't be supporting these.
  • browserHistory is partially supported, here are the supported methods
    • listen - supported
    • push - supported
    • replace - supported
  • <Router> is supported but these props are not:
    • <Router createElement> - Move to actual route component render method.
    • <Router onError> - use componentDidCatch in a component above your Router.
    • <Router onUpdate/> - Make a component that checks if prevProps.location !== this.props.location in componentDidUpdate.
    • <Router render/> - Move to actual route component render method
  • <Route> is supported, but these props are not:
    • <Route getIndexRoute> - Use getChildRoutes and give the index route a / path.
    • <Route components> - Render two Routers in the parent that rendered the different "components".
    • <Route getComponents> - Render two Routers in the parent that rendered the different "components".
  • <IndexRedirect> - Use <Redirect from="/" to="/some/where"/> because "/" is the same as an IndexRoute
  • Route Component props
    • route - can use your component props
    • routes - matching works too differently to support this
    • routeParams - you still have normal params
  • match Not sure what to do here, only a problem for server rendered apps ... only suggestion is stop server rendering and rely on service workers? When React Suspense ships we won't need this for server rendering anymore anyway.
  • <RouterContext> - probably only used for server rendering
  • createRoutes
  • PropTypes
  • Any React Router Redux integrations. I'd recommend not reading from redux and just reading from your route component props, this uses the new context API so you'll get updates w/o needing to synchronize routing state to redux. You can also use the navigate singleton export to navigate in action creators. Delete all that stuff!

Migrating Slowly

Once your app is running on Reactions Router, you'll have a console-full of warnings to help guide the migratin. You can start to migrate one route at a time to the official API.

// pick a route and switch it to the official API,
// let's do the index route first
<Router>
  <Route path="/" component={App}>
    <IndexRoute component={Index}/>
    <Route path="users/:userId" component={User}/>
  </Route>
</Router>

// becomes
<Router>
  <Route path="/" component={App}>
    <Index path="/"/>
    <Route path="users/:userId" component={User}/>
  </Route>
</Router>

// Then change the Users component to read from `this.props.userId`
// instead of `this.props.params.userId`

const User = ({ params }) => <div>{params.userId}</div>

const User = ({ userId }) => <div>{userId}</div>

// now go update the route config:
<Router>
  <Route path="/" component={App}>
    <Index path="/"/>
    <User path="users/:userId"/>
  </Route>
</Router>

// And finally update the App route
<Router>
  <App path="/">
    <Index path="/"/>
    <User path="users/:userId"/>
  </App>
</Router>

// When all of your routes run without warnings, you can update the import

import ... from '@reactions/router/compat'
// becomes
import ... from '@reactions/router'

Congratulations, you're not gonna get left behind anymore!

Legal

MIT License Copyright (c) 2018-present, Ryan Florence

Readme

Keywords

Package Sidebar

Install

npm i @reactions/router

Weekly Downloads

0

Version

0.0.0-alpha.10

License

MIT

Unpacked Size

175 kB

Total Files

11

Last publish

Collaborators

  • ryanflorence