Universal data fetching and lifecycle management for react router with multiple components

react-router-hook

Universal data fetching and lifecycle management for react-router@3 with multiple components. Inspired by redial, react-router-redial and async props.

Installation

npm install --save react-router-hook

Usage

import { browserHistory, Router, applyRouterMiddleware } from 'react-router';
import { useRouterHook, routerHooks } from 'react-router-hook';

const locals = {
  dispatch: store.dispatch, // redux store and dispatch, you can use any locals
  getState: store.getState,
};

const onAborted = () => {
  console.info('aborted');
};
const onCompleted = () => {
  console.info('completed');
};
const onError = (error) => {
  console.error(error);
};

const routerHookMiddleware = useRouterHook({
  locals,
  routerWillEnterHooks: ['fetch'],
  routerDidEnterHooks: ['defer', 'done'],
  onAborted,
  onStarted,
  onCompleted,
  onError,
});

ReactDOM.render((
  <Router
    history={browserHistory}
    render={applyRouterMiddleware(routerHookMiddleware))}
  >
    <Route path="/" component={App}>
      <Route path="users" components={{main: Users, footer: lazy({ style: { height: 500 } })(UserFooter)}} />
    </Route>
  </Router>
), node)
class App extends React.Component {
  render() {
    // the matched child route components become props in the parent
    return (
      <div>
        <div className="Main">
          {/* this will either be <Groups> or <Users> */}
          {this.props.main}
        </div>
        <div className="Footer">
          {/* this will either be <GroupsSidebar> or <UsersSidebar> */}
          {this.props.footer}
        </div>
      </div>
    )
  }
}

@routerHooks({
  fetch: async () => {
    await fetchData();
  },
  defer: async () => {
    await fetchDeferredData();
  },
})
class Users extends React.Component {
  render() {
    return (
      <div>
        {/* if at "/users/123" this will be <Profile> */}
        {/* UsersSidebar will also get <Profile> as this.props.children.
            You can pick where it renders */}
        {this.props.children}
      </div>
    )
  }
}

@routerHook({
  fetch: async () => {
    await fetchData();
  },
  defer: async () => {
    await fetchDeferredData();
  },
})
class UserFooter extends React.Component {
  render() {
    return (
      <div>
        UserFooter
      </div>
    )
  }
}

On server side


import { match } from 'react-router';
import { triggerHooksOnServer } from 'react-router-hook';
// Other imports

import routes from './routes';

app.get('*', (req, res) => {
  // create redux store (Optional);
  const store = createStore();

  match({
    history,
    routes,
    location: req.url,
  }, (err, redirectLocation, renderProps) => {
    if (err) {
      // Error Handler
    }
    const locals = {
      dispatch: store.dispatch,
      getState: store.getState,
    };
    triggerHooksOnServer(
      renderProps,
      ['fetch', 'defer'],
      {
        dispatch,
        getState,
      },
      // If onComponentError is null, callback will be immediately called with the error
      onComponentError: (err) => {
        console.error(err.Component, err.error);
      },
      // triggerHooksOnServer() will return a Promise if there is no callback
      (err) => {
        if (err) {
          res.status(500).end();
          return;
        }
        const body = ReactDOMServer.renderToString(
          <Provider store={store}>
            <RouterContext {...renderProps} />
          </Provider>,
        );
        res.send(`<html><head></head><body>${body}</body>`);
      },
  });
});
triggerHooksOnServer

Monitoring router status


@routerHook({
  fetch: async () => {
    await fetchData();
  },
  defer: async () => {
    await fetchDeferredData();
  },
})
class SomeComponent extends React.Component{

  static contextTypes = {
    routerHookContext: routerHookContextShape,
  };

  constructor(props, context) {
    super(props, context);
    this.state = {
      routerLoading: false,
    };
  }

  componentWillMount() {
    if (this.context.routerHookContext) {
      this.removeListener = this.context.routerHookContext.addLoadingListener((loading, info) => {
        const { total, init, defer, done } = info;
        console.info(loading, total, init, defer, done);
        this.setState({
          routerLoading: loading,
        });
      });
    }
  }

  componentWillUnmount() {
    if (this.removeListener) {
      this.removeListener();
    }
  }

  render() {
    return (
      <div>
        is loading: {this.state.routerLoading}
      </div>
    );
  }
}

GitHub