Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hot swapping routes #2182

Closed
laurentvd opened this issue Oct 7, 2015 · 63 comments
Closed

Hot swapping routes #2182

laurentvd opened this issue Oct 7, 2015 · 63 comments
Labels

Comments

@laurentvd
Copy link

Love your work on react-router! I'm implementing i18n routes using 1.0.0-rc1 and I'm running into the following. The relevant code:

render() {
    const childRoutes = this.props.generateRoutes();

    return (<Router ref="router" routes={ childRoutes } history={ this.props.history }/>);
}

The child routes are not updated, even though I'm positive the childRoutes object has changed. When I navigate to a new url, it doesn't match any routes. Is hot-swapping routes a scenario the router should support?

@mjackson
Copy link
Member

Is hot-swapping routes a scenario the router should support?

I'd like to support it, yes. But it's going to be tricky to support. Here's why:

We need to be able to identify which routes are effectively the same as the ones you gave us previously. This is similar to the way React needs to know which virtual DOM elements you give it are effectively the same as the ones you gave it the last time it called render. In order to do this we need a way to uniquely identify a route across prop changes (i.e. data-reactid for route objects).

Currently, we consider route objects to be unique (i.e. === unique, you generate your route objects once). There are two places (that I can think of right now, maybe more) where this is important:

  1. Registering route hooks
  2. Computing changed routes

Registering route hooks

When registering routerWillLeave hooks, we assign ids to route objects so we can keep those hooks in a map keyed by route id.

Computing changed routes

When we compute which routes are changing in a transition we rely on route object identity to determine if a route was active in the previous state, so we know whether or not we're transitioning away from that route.

If we can work around these issues, hot swapping of routes should be doable.

/cc @gaearon who knows a lot about hot reloading

@mjackson mjackson changed the title Replacing child routes Hot swapping routes Oct 10, 2015
@laurentvd
Copy link
Author

Thank you for your extensive response! And great to hear you'd like to support this. I think it can be really beneficial (and even necessary for i18n sites, like mine).

About identifying which routes are effectively the same; I agree this would be ideal, but I think just replacing the routes and manually triggering a history.pushState would suffice in many cases.

Registering route hooks

This is probably going to be a tough one. We could allow the developer to assign a name (or ID) to a route which should match the replacing route's name. If it doesn't match, it's an 'entering' route.

Computing changed routes

Not sure if I understand this correctly, but would't it make sense to always leave all routes that are being replaced and enter the relevant new routes?

Small disclaimer; I'm fairly new to react-router, so sorry if I misunderstand things!

@garborg
Copy link

garborg commented Oct 24, 2015

This worked in [email protected], right? Just confirming that I'm hitting the same issue and not missing something else in the upgrade path. In beta3, when I grew the routes array prop passed to the following render method, the router registered new apps/[...] child routes, but it does not in rc3:

    render() {
        const { history, routes } = this.props;
        return (
                    <Router history={history}>
                        <Route component={App}>
                            <Route path="apps" component={AppsOrApp}
                                               onEnter={requireAuth}
                                               childRoutes={routes} />
                        </Route>
                    </Router>
        );
    }

@taion
Copy link
Contributor

taion commented Oct 24, 2015

I recall that @acdlite hit something with referential equality breaking on routes - is that something supported by redux-router or something?

@garborg
Copy link

garborg commented Oct 24, 2015

I'm not sure -- I didn't see that and haven't used redux-router.

@taion
Copy link
Contributor

taion commented Nov 19, 2015

To add some color on #2182 (comment), I actually resolved the issue there by allowing users to optionally specify names on routes, which would be used in preference to referential equality for asserting that routes were "equal".

I'm curious, though - could i18n routes be generated with the existing getChildRoutes API?

@taion
Copy link
Contributor

taion commented Nov 20, 2015

#2581 will add a warning when you attempt to do this.

@ryanflorence
Copy link
Member

Could probably render a <Router key={something}/> and then change the key to trigger react to unmount it and set up a new one.

You'd only want that key to change when the routes change though, and definitely don't want it in production, but it could maybe kinda work?

@matthewgertner
Copy link

I ran into a related issue using react-router with Redux. I would like some of my routes to require authentication, where the user's login status is kept in Redux's centralized data store. My router component looks something like:

const mapStateToProps = reduction => {
  return {
    loginStatus: reduction.getIn(['appState', 'auth', 'loginStatus'])
  };
};

export default connect(mapStateToProps)(class App extends Component {
  static propTypes = {
    dispatch: PropTypes.func,
    loginStatus: PropTypes.number
  }

  render() {
    const { loginStatus } = this.props;

    return (
      <Router history={history}>
        <Route path="/" component={Root}>
          <Route path="Login" component={Login} />
          <Route
              path="SomeAuthenticatedRoute"
              component={SomeComponent}
              onEnter={(nextState, replace) => requireLogin(nextState, replace, loginStatus)}
          />
          </Route>
        </Router>
      );
    }

    return false;
  }
});

Of course this doesn't work since the Route is bound to the Router in componentWillMount and never updated when loginStatus changes (so the closure is always bound to the same old value). This looks to me like a fundamental incompatibility between Redux and react-router.

What I don't understand is why you can't check the routes in componentWillRender by comparing each existing route to the new route with the same path. And if any of the attributes of the Route element have changed, the route would be updated. I guess this is very naive since I only took a quick look at the source code, but I'd be interested to understand why an approach like this wouldn't work.

@taion
Copy link
Contributor

taion commented Jan 12, 2016

What do we do if something doesn't end up matching?

You can always use getChildRoutes or getComponent as needed. But really in this case you could just close over loginStatus differently via an instance method on your app.

@matthewgertner
Copy link

@taion If something doesn't match you just replace the route, right? I would think you would want the routes to always correspond to the current Route elements and that you could achieve that by diff-ing the elements in componentWillRender as I suggested. But I don't know much about the internals.

Not sure what you mean by "an instance method on your app". With Redux you really only have access to the app state in mapStateToProps without resorting to hacks. I can and will use a hacky solution for now, but it would be awesome if there were a way to make react-router play nicely with Redux.

@matthewgertner
Copy link

I took another quick look at Router.js. I guess what I don't understand is what bad thing would happen if you essentially ran the code from componentWillMount in componentWillRender as well so the transition manager gets the updated routes. Even if nothing changes, I can't see what bad thing would have if the routes are recalculated on every render cycle. Doing some sort of diff would be more of an optimization.

@taion
Copy link
Contributor

taion commented Jan 13, 2016

It's like 3 pages up on this comment thread... #2182 (comment)

@matthewgertner
Copy link

@taion Got it, thanks. Wouldn't https://facebook.github.io/react/docs/reconciliation.html work well for this use case as well?

@taion
Copy link
Contributor

taion commented Mar 11, 2016

For better or for worse, we already implicitly support this via the dynamic getChildRoutes hook.

Also, we tear down all the old leave routes upon leaving a route anyway.

I don't think there's much of a good reason to only have that one backdoor into route hot swapping support, so we should just add proper support for this on <Router>, since we effectively have such support anyway.

@BerkeleyTrue
Copy link

Ran into this today while trying to implementing vanilla hot module replacement and calling ReactDOM.render with new routes.

@ryanflorence ryanflorence added this to the next-sometime milestone Apr 16, 2016
@ryanflorence
Copy link
Member

@taion let's do it.

@taion
Copy link
Contributor

taion commented Apr 16, 2016

Cool! We should think about how exactly this works. I guess we should re-run the match if the top-level routes object breaks referential equality. It'd be nice if there were a way to simultaneously navigate to a new location and change the routes, though.

@taion
Copy link
Contributor

taion commented May 5, 2016

@gaearon How does it work? We don't reload the routes in the transition manager, so if you e.g. add a new route, the router won't route to it.

@gaearon
Copy link
Contributor

gaearon commented May 9, 2016

How does it work? We don't reload the routes in the transition manager, so if you e.g. add a new route, the router won't route to it.

Yeah, hot reloading of routes themselves won’t work, but this isn’t a big priority for me. As long as updating individual components works (and now it does) I’m content.

@taion
Copy link
Contributor

taion commented May 9, 2016

Okay, that makes sense then. Just wanted to make sure I wasn't missing something.

@aight8
Copy link

aight8 commented Jun 6, 2016

Any news about this issue? It's totally priority for me too and a total coverage of dev tool support is not the most important thing I think. My routes are absolute and heavily based on redux. I think this library should be more dynamic and not so strict. Maybe this issue can be solved when you give to any route a unique key. When the routes are nested it's delimited by a slash or so. This name can be used for url generation as well. So instead generate the whole url in the Link component by yourself you just pass the name and parameters. This is more flexible by the way and the only way to support i18n etc.

@aight8
Copy link

aight8 commented Jul 6, 2016

Any news about this issue? Where can I help?

@tomconroy
Copy link

FWIW this project demonstrates a workaround, simply re-mount your app in module.hot.accept

@taion
Copy link
Contributor

taion commented Jul 6, 2016

If we split out LocationProvider per #3611, this might just work straightforwardly.

@moljac024
Copy link

@tomconroy wouldn't that destroy local state though?

@Swiftwork
Copy link

Until this issue is resolved could we possibly reduce the number of warning messages to once per page request? That way subsequent hot reloads wont fill the console with warnings (which currently are displayed as errors).

image

@kachkaev
Copy link

@Swiftwork how about trying to hide the warnings with a regex? You can use filter input field for this similarly to how this is done for unknown props in React 15.2: react-bootstrap/react-bootstrap#1994 (comment)

@lucasbento
Copy link

Any updates/workarounds (except changing console.error directly to disable the warnings) to solve this?

@chibicode
Copy link

I've changed console.error directly:

// Keep this until this is fixed: https://github.com/reactjs/react-router/issues/2182
console.error = (() => {
  const error = console.error
  return function (exception) {
    (exception && typeof exception === 'string' && exception.match(/change <Router /))
    ? undefined
    : error.apply(console, arguments)
  }
})()

@taion taion mentioned this issue Aug 30, 2016
@ryanflorence
Copy link
Member

v4 has no routes, just components that work with HMR naturally.

@laurentvd
Copy link
Author

Thanks @ryanflorence ! Really like where v4 is going. Makes much more sense.

@benwiley4000
Copy link
Contributor

@gaearon On May 9 earlier in this thread you mentioned that React Hot Loader 3 now works with component updates in React Router. Not sure which version you referred to. Is this true for React Router version 2, or version 3? I'd prefer to adopt React Router 4 when it's at least in beta.

@vdh
Copy link

vdh commented Sep 27, 2016

For anyone like me still on v2/v3 and looking for a workaround, I've managed to get moderate success by writing a custom module.hot.accept that mutates the existing routes object to swap in new components.

Example provided in a gist for brevity:
https://gist.github.com/vdh/c241560382d7b3c4aebf68aac66b9446

It's not pretty at all (object mutation 😢 ), but it can at least hot-reload new components. It will unfortunately not preserve component state (since Root is renewed each time), and will not be able to hot-reload any actual structural changes to the routes. It's only capable of swapping out old components for new ones. It also doesn't support the <Route> syntax, only the less-popular object syntax. Frankly I never liked the way the <Route> syntax worked at all in v1/v2/v3 to begin with, so I look forward to v4 turning Pinocchio into a real boy… 😜

@leebenson
Copy link

@vdh, thanks for sharing the gist - unfortunately it breaks with Relay. Any known workarounds for Relay props?

@leebenson
Copy link

actually @vdh, for 'regular' React components, I've managed to do away with any workarounds entirely using this format.

Basically, re-loading 'routes' and re-rendering based on the original props does the trick. It also preserves component state.

Relay is still broken, though, as explained in the above link.

@hnordt
Copy link

hnordt commented Nov 10, 2016

I love React Router, but I have created my own solution (it's a lightweight alternative to React Router).

It supports dynamic routes and other cool features. It has only 77 lines and uses the awesome @mjackson's history library and render callbacks.

@laurentvd run this with Create React App (you'll need to run npm install smalldots before), and be happy: 😄

import React, { Component } from 'react'
import Router from 'smalldots/lib/experimental/Router'

const Debug = ({ value }) => <pre>{JSON.stringify(value, null, 2)}</pre>

const Link = ({ to, children }) => (
  <Router>
    {({ push }) => (
      <a href={to} onClick={event => {
        event.preventDefault()
        push(to)
      }}>
        {children}
      </a>
    )}
  </Router>
)

class App extends Component {
  state = this.getRandomRoutes()

  // each 10 seconds new routes will be generated
  componentDidMount() {
    this.interval = setInterval(() => this.setState(this.getRandomRoutes()), 10000)
  }

  componentWillUnmount() {
    clearInterval(this.interval)
  }

  getRandomRoutes() {
    return {
      route1: Math.ceil(Math.random() * 1000000),
      route2: Math.ceil(Math.random() * 1000000)
    }
  }

  render() {
    return (
      <div>
        <p>
          Route 1: <Link to={`/${this.state.route1}`}>/{this.state.route1}</Link>
          {' '}
          &middot;
          {' '}
          Route 2: <Link to={`/${this.state.route2}`}>/{this.state.route2}</Link>
        </p>
        <Router>
          {/*
            Router has a simple usage:
            it's children should be a func that receives the router and need to return
            a valid element or an object with keys being the routes and the values
            being a func that receives info about the current route
            and returns another valid element

            In a nutshell, Router has two use cases, one I have showed
            in the Link component (it returns a valid element)
            and another I'll show below (it returns an object being the routes)

            You can have as many Routers as you need in your app because
            it's just a React component

            Valid routes:
            - /foo
            - /foo/:bar (:bar is a param)
            - /foo(/:bar) ((/:bar) is an optional param)
            - /foo/*bar (*bar is a catch all param)
          */}
          {router => ({
            [`/${this.state.route1}`]: route => <Debug value={{ code: 200, router, route }} />,
            [`/${this.state.route2}`]: route => <Debug value={{ code: 200, router, route }} />,
            '/*notfound': route => <Debug value={{ code: 404, router, route }} />
          })}
        </Router>
      </div>
    )
  }
}

export default App

Another cool use case would be something like:

const publicRoutes = {
  '/signup': () => <SignupScene />,
  '/recover-password': () => <RecoverPasswordScene />,
  '/*other': () => <LoginScene />
}

const privateRoutes = {
  '/': () => <ProfileScene />,
  '/dashboard': () => <DashboardScene />,
  '/*other': () => <NotFoundScene />
}

const App = ({ token }) => <Router>{() => token ? privateRoutes : publicRoutes}</Router>

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests