react-router-dispatcher

react-router v4 action dispatcher

Usage no npm install needed!

<script type="module">
  import reactRouterDispatcher from 'https://cdn.skypack.dev/react-router-dispatcher';
</script>

README

react-router-dispatcher

Greenkeeper badge npm npm CircleCI branch Maintainability Test Coverage Conventional Commits

react-router-dispatcher is designed to work with react-router v4.x, it:

  • uses actions to encapsulate behaviors that can be invoked before rendering
  • supports server-side rendering, including resolving async promises before rendering
  • requires using react-router-config v4.x route configuration

Looking for version 1.x??

You can find it on the V1 branch. Version 2+ has been simplified and no longer requires redux

Install

// npm
npm install --save react-router-dispatcher

// yarn
yarn add react-router-dispatcher

Available actions

Usage

Universal rendering

If your building a universal application, use the createRouteDispatchers factory method.

// dispatcher.js
import { createRouteDispatchers } from 'react-router-dispatcher';
import { LOAD_METADATA } from 'react-router-metadata-action';
import { LOAD_DATA } from './loadDataAction';

// === route dispatcher configuration ===
// 1. define react-router-config route configuration
const routes = [...];

// 2. define the ORDER that actions are invoked
const orderedActionNames = [[LOAD_DATA], [LOAD_METADATA]];

// Use the createRouteDispatchers factory,
// it returns everything required for rendering dispatcher actions
const {
  UniversalRouteDispatcher,
  ClientRouteDispatcher,
  dispatchClientActions,
  dispatchServerActions
} = createRouteDispatchers(routes, orderedActionNames /*, options */);
server-side rendering
import Html from 'react-html-metadata';
import { dispatchServerActions, UniversalRouteDispatcher } from './dispatcher';
import apiClient from './someOtherPackage';

const location = request.url; // current request URL, from expressjs or similar
const actionParams = { apiClient }; // passed to all dispatch action methods

dispatchServerActions(location, actionParams /*, options */).then(({ metadata, store }) => {
  const staticRouterCtx = {};

  // Render the response, supports rendering to stream and string
  const stream = renderToNodeStream(
    <Html metadata={metadata}>
      <StaticRouter location={location} context={staticRouterCtx}>
        <UniversalRouteDispatcher appData={store} />
      </StaticRouter>
    </Html>);

  res.write("<!DOCTYPE html>");
  stream.pipe(res);
});
client-side rendering
import { hydrate, render } from 'react-dom';
import Html from 'react-html-metadata';
import {
  dispatchClientActions,
  UniversalRouteDispatcher,
  ClientRouteDispatcher
} from './dispatcher';

const location = window.location.pathname; // current url, from browser window
const appData = window.__AppData; // data serialized from the server render

// This is synchronous
// It uses the appData to recreate the metadata on the client
const { metadata } = dispatchClientActions(location, appData);

// Use hydrate() with server-side rendering,
// otherwise use render() with <ClientRouteDispatcher />
hydrate(
  <Html metadata={metadata}>
    <BrowserRouter>
      <UniversalRouteDispatcher />
    </BrowserRouter>
  </Html>
);

client-only rendering

For the client app, use the exported <RouteDispatcher> component to render your application.

import { RouterDispatcher } from 'react-router-dispatcher';

const routeCfg = []; // same as server (react-router-config routes)

// render your app
<Router ...>
    <RouterDispatcher routes={routeCfg} actionNames={[['loadData']]} />
</Router>

Actions

You must assign actions to route components (components that are assigned directly to react-router-config style routes)

Define an action

Packages that support react-router-dispatcher should export actions.

// loadDataAction.js - a simple action for loading async data
import getDisplayName from 'react-display-name';

export const LOAD_DATA = 'LOAD_DATA_ACTION';

export default function loadDataAction() {
  return {
    name: LOAD_DATA,
    staticMethodName: 'loadData',
    initServerAction: (params) => ({
      store: params.store || {}
    }),
    filterParamsToProps: (params) => {
      store: params.store
    }
  };
}

Applying actions to components

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withActions } from 'react-router-dispatcher';
import loadDataAction from './loadDataAction';

class ExampleComponent extends Component {
  static propTypes = {
    store:     PropTypes.object.isRequired,
    apiClient: PropTypes.object.isRequired
  };

  // loadDataAction invokes this method to load data from an api
  static loadData(actionProps, routerCtx) {
    const {
      location,
      match: {
        params
      },
      store,
      apiClient
    } = actionProps;

    // async functions must return a Promise
    return apiClient.loadById(params.id).then((data) => {
      store.exampleData = data;
    });
  }

  render() {
    const {store: { exampleData }} = this.props;
    return <div>{exampleData}</div>
  }
}

// the mapper must return the 'propTypes' expected by the component
const mapParamsToProps = ({ apiClient }) => { apiClient };
export default withActions(mapParamsToProps, loadDataAction())(ExampleComponent);

API

Actions

It's recommended that all actions are defined as factory functions that return new action instances. It can be useful to allow actions to accept parameters to customize the actions behavior.

Action Schema

name: string

  • required
  • The action name should also be exported as a string, to be used for configuring action order

staticMethod: (props, routerCtx) => any

  • One of staticMethod or staticMethodName is required
  • Action method implementation, can be defined here or using static methods on components actions are assigned to
  • return a Promise for async actions
  • for non-async actions, return data

staticMethodName: string

  • One of staticMethod or staticMethodName is required
  • the name of the static method required on any Component that the action is applied to

filterParamsToProps: (params) => Object

  • required
  • filters all actionParams to include on params required by this action

hoc: (Component, ActionHOC) => node

  • Optional
  • Defines a higher-order component that is applied to all components that have the action assigned
  • Using higher-order components makes actions very versatile!

initServerAction: (actionParams) => Object

  • Optional, but required if the action supports being invoked on the server before rendering
  • if your action supports server-side usage but does not need to perform any init, return an empty object
    • initServerAction: (params) => {}

initClientAction: (actionParams) => Object

  • Optional, but required if the action supports being invoked on the client before rendering
  • if your action supports client-side usage but does not need to perform any init, return an empty object
    • initClientAction: (params) => {}

successHandler: (props, routerCtx) => void

  • Optional, invoked after this action is successfully invoked on each matching route
  • Params will include any value(s) assigned from the static action methods
  • NOTE: params are the raw dispatcher parameters

errorHandler: (err, props) => void

  • Optional, invoked if any static action methods or success handler fails

stopServerActions: (props, routerCtx) => boolean

  • Optional, allows an action to short-circuit/prevent invocation of following action sets with dispatchOnServer()
    • For example; An action may determine a redirect is required, therefore invoking following action sets is a waste of resources

Methods

createRouteDispatchers(routes, orderedActionNames, options)

routes: Array

orderedActionNames: string | Array<string> | Array<Array<string>> | (location, actionParams) => string|Array<string>|Array<Array<string>>

  • Configures the order that actions will be executed
  • A string can be used if only 1 action is used
  • An array of action names will execute all actions in parallel
  • A nested array enables actions to be executed serially
    • ie: [['loadData'], ['parseData']] first loadData is invoked on each component, then parseData is invoked on each component
  • A function, dispatchActions(location, actionParams). Return one of the previously defined types (string, array, nested array).

options: Object

  • routeComponentPropNames: Array<string>, route prop name(s) that are known to be react components
  • loadingIndicator: React Component, a component to display for client-side renders when loading async data

withActions(mapParamsToProps, actions)

A higher-order component function for assigned actions to components

mapParamsToProps: (params, routerCtx) => Object

  • A function that maps action parameters to prop values required by any actions applied to the component.
  • Pass null if no mapping function is required by the component.

actions:

  • one or more actions to be applied to a react component
  • separate multiple actions using a comma: withActions(null, loadData(), parseData())(Component)

Components

<RouteDispatcher> component

Props:

routes: Array

actionNames: string | Array<string> | Array<Array<string>> | (location, actionParams) => string|Array<string>|Array<Array<string>>

routeComponentPropNames: Array<string>

  • The prop names of route components that are known to be react components
  • The default value is component.

actionParams: any

  • Any value can be assigned to the action params, the value is passed to all action methods, common usages include passing api clients and application state (such as a redux store)

loadingIndicator: React Component

  • A custom component to display on the client when async actions are pending completion
  • note: this is only rendered on the client

render: (routes, routeProps) => node

  • A custom render method
  • you must invoke the react-router renderRoutes method within the render method

Utilities

defineRoutes

The defineRoutes utility method automatically assigns keys to routes that don't have a key manually assigned. This key can be accessed from actions to determine the exact route that is responsible for invoking the action.

import { defineRoutes } from 'react-router-dispatcher';

const routes = defineRoutes([
    // define react-router-config routes here
]);

matchRouteComponents

Resolves all route components for a requested location and a given set of routes.

import { matchRouteComponents } from 'react-router-dispatcher';

const matchedRoutes = matchRouteComponents(location, routes, routeComponentPropNames);
const [component, match, routerContext] = matchedRoutes[0];
const { route, routeComponentKey } = routerContext;

Contribute

For questions or issues, please open an issue, and you're welcome to submit a PR for bug fixes and feature requests.

Before submitting a PR, ensure you run npm test to verify that your coe adheres to the configured lint rules and passes all tests. Be sure to include unit tests for any code changes or additions.

License

MIT