adam-26/react-router-dispatcher

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# react-router-dispatcher

[![Greenkeeper badge](https://badges.greenkeeper.io/adam-26/react-router-dispatcher.svg)](https://greenkeeper.io/)
[![npm](https://img.shields.io/npm/v/react-router-dispatcher.svg)](https://www.npmjs.com/package/react-router-dispatcher)
[![npm](https://img.shields.io/npm/dm/react-router-dispatcher.svg)](https://www.npmjs.com/package/react-router-dispatcher)
[![CircleCI branch](https://img.shields.io/circleci/project/github/adam-26/react-router-dispatcher/master.svg)](https://circleci.com/gh/adam-26/react-router-dispatcher/tree/master)
[![Maintainability](https://api.codeclimate.com/v1/badges/5ca5fb8baef7a77d54bf/maintainability)](https://codeclimate.com/github/adam-26/react-router-dispatcher/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/5ca5fb8baef7a77d54bf/test_coverage)](https://codeclimate.com/github/adam-26/react-router-dispatcher/test_coverage)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)

react-router-dispatcher is designed to work with [react-router v4.x](https://github.com/ReactTraining/react-router), 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](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config) route configuration

#### Looking for **version 1.x**??
>[You can find it on the _V1_ branch](https://github.com/adam-26/react-router-dispatcher/tree/v1).
Version 2+ has been simplified and **no longer requires [redux](redux.js.org)**

## Install
```sh
// npm
npm install --save react-router-dispatcher

// yarn
yarn add react-router-dispatcher
```

## Available actions

  * [react-router-dispatcher-status-code](https://github.com/adam-26/react-router-dispatcher-status-code) set HTTP status code of streaming responses
  * [react-router-dispatcher-redirect](https://github.com/adam-26/react-router-dispatcher-redirect) redirect routes that support SSR streams by redirecting before render
  * [react-router-dispatcher-metadata](https://github.com/adam-26/react-router-dispatcher-metadata) SSR stream supported HTML metadata
  * [react-router-dispatcher-chunk](https://github.com/adam-26/react-router-dispatcher-chunk) react-chunk dynamic import support to support code-splitting

## Usage

#### Universal rendering

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

```js
// 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
```js
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
```js
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.

```js
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_.

```js
// 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

```js
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`
  * Routes defined using the [react-router-config](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config) format.

**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`
  * Routes defined using the [react-router-config](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config) format.

**actionNames**: `string | Array<string> | Array<Array<string>> | (location, actionParams) => string|Array<string>|Array<Array<string>>`
  * Configure the **action(s)** defined any any _route component_ to invoke before rendering.
  * See [createRouteDispatchers.orderedActionNames for more information](https://github.com/adam-26/react-router-dispatcher#API)

**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.

```js
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.

```js
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](https://github.com/adam-26/react-router-dispatcher/issues), 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