npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-router-manager

v3.3.0

Published

React Router server side rendering with data fetching

Downloads

735

Readme

react-router-manager

React Router server side rendering with data fetching. This module is based on react-router-config and expands on the same concepts. The goal is to create a one stop solution for routing and data fetching on both client and server. It is particularly useful for people migrating from redial.

Installation

npm install --save react-router-manager

Usage

There are 2 distinct stages to working with this library, the first is data fetching and the second is rendering. In order for both to work you need to create "route configs". These configs allow for both routing and data fetching to work. Please use them for every route that needs to fetch data.

IMPORTANT: the utilities here default to exact: true for route matching, which is the opposite of what Route components in React Router do. Partial matching is prone to errors and creates more issues than it solves. Override it on per route basis if needed.

Route configs

A route config is an array of objects like this:

[
  // A Route
  {
    // The only difference with default Route paths is that you need
    // to use "*" for match-all data fetching and intercept functions.
    path: '/stuff/:id?',

    // This will be called both before the route data is fetched and before the route is rendered.
    // It allows you to completely rewrite the route object and return different components
    // or redirect conditionally. Return a new route configuration object here.
    // Anything except `path` and `intercept` can be returned (overriden).
    intercept: ({ match, location, route }) => {
      // Returning null will prevent fetching and rendering of the entire subtree.
      if (!isAuthorized()) return null;

      // Simply return a Route or a Redirect configuration object.
      if (match.params.id === '0') return { to: '/otherstuff' };
      return { component: MyComponent };
    },

    // These props will be passed on to the component and also available in the data fetching function
    props: {
      type: 'awesome',
    },

    // Component to render
    component: MyComponent,

    // You may create your own render function if you wish. It will receive the usual arguments
    // along with `children` which is the rendered nested routes for this route (or null).
    // NOTE: This way data fetching will not work for this route as it relies on `component` prop.
    render: ({ match, location, history, staticContext, children }) => {
      return <MyComponent>{children}</MyComponent>;
    }

    // Useful for handling 404s, but you can use it in any case.
    statusCode: 404,

    // You can nest routes like this.
    routes: [
      // {...}
    ],

    // Any other props you would pass to a Route
    // We default this to `true` unlike the React Router. Pass `false` to override.
    exact: false,
    // ...
  },

  // A redirect
  {
    // Set this statusCode for the redirect
    statusCode: 302,

    // Any other props you would pass to a Redirect
    from: '/this/:id',
    to: '/that/:id',
    // ...
  },

  // To use syntax like this also use a `.filter` function at the end
  myCondition && {
    from: '/old',
    to: '/new',
  },

  //...
]
// Use this if you had any conditions, any falsy values in these arrays will get filtered out
.filter(Boolean)

Every route must have either component, render or intercept for rendering. For data fetching to work component is required. Everything else is optional.

Connecting components

To specify a data fetching function you need to create a static resolver method:

// You can return either a promise or an array of promises
MyComponent.resolver = ({ match, route }) => ([
  getMyAsyncDataAndReturnPromise(match.params, route.props.type),
  andAnotherPromise(match.params),
]);
export default MyComponent;

runResolver

When you are ready to collect your promises and fetch the data use runResolver:

import { runResolver } from './modules/resolver';
const routes = [/* routes config */];
const { location } = history;

// By default each static resolver method gets { match, route, location } object as an argument.
// You can modify that object here as you wish.
const getLocals = (request) => ({ ...request, store: reduxStore })

// You get back an array of promises to resolve as you will
Promise.all(runResolver(routes, location, getLocals));

You might want to use Promise.allSettled on the server side to wait for every promise to resolve or reject. Or you can go for a "fail early" approach with Promise.all everywhere. Returning an array gives you that choice.

RouterManager or renderRoutes

This simply renders the route configuration for you. This is the default export and you can use it as a function or as a component. Pass directly into React Router like this:

import RouterManager from 'react-router-manager';
import { Router } from 'react-router';
import { createBrowserHistory } from 'history';
import { hydrate } from 'react-dom';

const routes = [/* routes config */];

hydrate(
  <Router history={createBrowserHistory()}><RouterManager routes={routes} /></Router>,
  document.getElementById('root')
);

Alternatively:

import renderRoutes from 'react-router-manager';
// ...
hydrate(
  <Router history={createBrowserHistory()}>{renderRoutes({ routes })}</Router>,
  document.getElementById('root')
);

Utility functions

These are used internally, but you might want to use them too.

injectStatusCode

import { injectStatusCode } from 'react-router-manager';

const MyComponent = (props) => {
  // Simply injects statusCode when static context is available. Use in any Component where
  // route information is available.
  injectStatusCode(props.staticContext, 403);
  //...
};

RouteStatus

import { RouteStatus } from 'react-router-manager';

const MyComponent = (props) => {
  return (
    // This is essentially the same as injectStatusCode but in a Route object form.
    <RouteStatus statusCode={403}>
      {/* content */}
    </RouteStatus>
  );
};

Route

You can use it as a function or as a component.

import { Route } from 'react-router-manager';

// Will simply render a single route
return <Route path="/stuff/:id?" component={MyComponent} />;

Alternatively:

import { Route as renderRoute } from 'react-router-manager';

// Will simply render a single route
return renderRoute({ path: '/stuff/:id?', component: MyComponent });

Redirect

You can use it as a function or as a component.

import { Redirect } from 'react-router-manager';

// Will simply render a single redirect
return <Redirect from="/this/:id" to="/that/:id" />;

Alternatively:

import { Redirect as renderRedirect } from 'react-router-manager';

// Will simply render a single route
return renderRedirect({ from: '/this/:id', to: '/that/:id' });

A complete example

On the client side:

import { render, hydrate } from 'react-dom';

import { Router } from 'react-router';
import { createBrowserHistory } from 'history';
import RouterManager, { runResolver } from 'react-router-manager';

import routes from './routes';


// You may want to disable prerendering during development
const isAppPrerendered = global.__APP_PRERENDERED__;

const renderPage = (history, routes) => {
  const content = <Router history={history}><RouterManager routes={routes} /></Router>

  const renderer = isAppPrerendered ? hydrate : render;
  return renderer(content, document.getElementById('main'));
};

const startRouter = (store, history) => {
  let shouldFetch = !isAppPrerendered;

  const handleFetch = (location) => {
    if (shouldFetch) runResolver(routes, location);
    shouldFetch = true;
  };

  // This allows to fetch data after navigation. You may use a different strategy if you wish.
  history.listen(handleFetch);
  handleFetch(history.location);

  renderPage(store, history, routes);
};

// Call setup functions. First setup store, then initialize router.
const history = createBrowserHistory();
startRouter(store, history);

An expressjs middleware on the server side:

import { renderToString } from 'react-dom/server';

import { StaticRouter } from 'react-router';
import { createLocation } from 'history';
import RouterManager, { runResolver } from 'react-router-manager';

import routes from './client/routes';


const renderContent = (routes, context, location) => {
  const content = (
    <StaticRouter {...{ context, location }}><RouterManager routes={routes} /></StaticRouter>
  );
  return renderToString(content);
};

export default (req, res) => {
  const location = createLocation(req.url);

  const matchPage = (result, error) => {
    const data = error ? {} : result;
    const context = {};
    const content = renderContent(routes, context, req.url);

    if (context.url) return res.redirect(context.statusCode || 301, context.url);
    // In your template serialize data into `__APP_PRERENDERED__`;
    res.status(context.statusCode || 200).render('index', { data, content });
  };

  const handleError = (error) => {
    console.error(`Request ${req.url} failed to fetch data:`, error);
    matchPage(null, error);
  };

  runResolver(routes, location).then(matchPage).catch(handleError);
};