React-router: Allow to run hooks before or after location changes

Created on 16 Dec 2016  路  5Comments  路  Source: ReactTraining/react-router

First of all: thanks sirs for you great work!

I would like the be able to register hooks which will be run before or after location changes.

New feature proposal

Add beforeRouteChange and afterRouteChange additional properties on BrowserRouter component which will accept function (or array of functions) run before or after dispatching location changes.

Use case

  • Suppose we have a <Match /> instance which will load asynchronously the component <X />
  • The component <X /> is connected to a redux store
  • For performance purposes, the initial store hasn't the reducer piece needed by <X /> (because the user may never come to the route matched to <X />)
  • So we need to load that reducer piece before dispatch the route change event (otherwise the connection to the redux store may can fail or be wrong)

Usage examples

Fake lag time

In the dumb example below: we will add a lag time of 0.5 seconds before every location change.

class Root extends React.Component {
  constructor () {
    super()
    this.beforeRouteChange = this.beforeRouteChange.bind(this)
  }
  beforeRouteChange ({location, action}) {
    return new Promise((resolve) =>
      setTimeout(resolve, this.props.lagTime || 500))
  }
  render () {
    return (
      <BrowserRouter beforeRouteChange={this.beforeRouteChange}>
        <Link to="/foo">foo</Link>
        {' '}
        <Link to="/bar">bar</Link>
      </BrowserRouter>
    )
  }
}

Loading indicator

class Root extends React.Component {
  constructor () {
    super()
    this.afterRouteChange = this.afterRouteChange.bind(this)
    this.beforeRouteChange = this.beforeRouteChange.bind(this)
    this.state = {routeLoading: false}
  }
  beforeRouteChange ({location, action}) {
    console.log('navigate to:', location)
    this.setState({routeLoading: true})
  }
  afterRouteChange ({location, action}) {
    console.log('route loaded:', location)
    this.setState({routeLoading: false})
  }
  renderLoadingIndictor () {
    if (!this.state.routeLoading) {
      return null
    }
    return (
      <div>Loading...</div>
    )
  }
  render () {
    return (
      <div>
        {this.renderLoadingIndictor()}
        <BrowserRouter
          beforeRouteChange={this.beforeRouteChange}
          afterRouteChange={this.afterRouteChange}
          >
          <Link to="/foo">foo</Link>
          {' '}
          <Link to="/bar">bar</Link>
        </BrowserRouter>
      </div>
    )
  }
}

Implementation suggestion (branch v4 - 553b56a)

Note: I wrote the code below for this post only. I haven't test it because I didn't succeed to build react-router from sources (strange "module not found" errors were thrown by babel).

import React, { PropTypes } from 'react'
import StaticRouter from './StaticRouter'

class Router extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      location: props.history.location,
      action: props.history.action
    }
  }

  // v +++
  componentDidMount() {
    const { history, afterRouteChange, beforeRouteChange } = this.props
    let lastRouteKey = history.location.key

    this.unlisten = history.listen(() => {
      const routeKey = lastRouteKey = history.location.key
      const nextState = {
        location: history.location,
        action: history.action
      }
      ;[]
        .concat(beforeRouteChange, this.setState.bind(this), afterRouteChange)
        .map((f) => () => f && routeKey === lastRouteKey && f(nextState))
        .reduce((v, f) => v.constructor === Function ? v() : v.then ? v.then(v => f(v)) : f(v))
    })
  }
  // ^ +++

  componentWillUnmount() {
    this.unlisten()
  }

  render() {
    const { location, action } = this.state
    const { history, ...rest } = this.props
    return (
      <StaticRouter
        action={action}
        location={location}
        onPush={history.push}
        onReplace={history.replace}
        blockTransitions={history.block}
        {...rest}
      />
    )
  }
}

if (__DEV__) {
  Router.propTypes = {
    afterRouteChange: PropTypes.oneOfType([   // < +++
      PropTypes.func,                         // < +++
      PropTypes.arrayOf(PropTypes.func)       // < +++
    ]),                                       // < +++
    beforeRouteChange: PropTypes.oneOfType([  // < +++
      PropTypes.func,                         // < +++
      PropTypes.arrayOf(PropTypes.func)       // < +++
    ]),                                       // < +++
    history: PropTypes.object.isRequired
  }
}

export default Router

Most helpful comment

Should be pretty easy to roll your own hooks with v4 through composition, so we shouldn't actually need to expose any more API here. Just an example.

All 5 comments

This would be fantastic - flexible, and solves issues like this one from v3:

https://github.com/ReactTraining/react-router/issues/3338

For our purposes, we'd like to introduce an artificial lag / loading (similar to github route changes) because otherwise things literally change on screen TOO fast, sometimes users don't notice :).

Could something like this instead be implemented by composing something such as <LoadableMatch>? It could wrap children in a component that orchestrates the loading state, returning <Match> with a loading indicator, or the component which has access to a loaded redux state.

It's more work, but reduces API surface.

Should be pretty easy to roll your own hooks with v4 through composition, so we shouldn't actually need to expose any more API here. Just an example.

Been eagerly following the recent v4 refactors and progress. I know speaking to timelines is tough, but any idea on the order of magnitude to next release? Days, weeks, months?

I described what I think is a workable solution to this problem in https://github.com/ReactTraining/react-router/issues/4300#issuecomment-274165691. Hopefully that's enough to get the gears turning to figure out how we can solve this problem using composition instead of adding more API.

As for the next release, I'd say we'll have it within a week.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

imWildCat picture imWildCat  路  3Comments

tomatau picture tomatau  路  3Comments

maier-stefan picture maier-stefan  路  3Comments

ArthurRougier picture ArthurRougier  路  3Comments

stnwk picture stnwk  路  3Comments