Skip to content

react router

Created: 2016-05-11 09:55:04 -0700 Modified: 2018-01-16 16:23:14 -0800

https://github.com/reactjs/react-router

Suppose we have this configuration:

render((

<Router history={hashHistory}>

<Route path=”/” component={App}>

<IndexRoute component={Home} />

<Route path=“about” component={About} />

<Route path=“inbox” component={Inbox} />

</Route>

</Router>

), document.body)

When you navigate to ”/”, it will render <App><Home/></App> because each route contains all of its children. That’s why it’s important that App looks something like this:

import { Component } from ‘react’;

class App extends Component {

render() {

return (

<div>

This is a sample page

{this.props.children}{/THIS IS IMPORTANT/}

</div>

);

}

}

Rendering children is what allows routes to be nested like that. That way, you can have a kind of “shell” at your index route (”/”) and just populate whatever it is you happen to be looking at. You need to make sure that each component is also rendering its children, otherwise your inner routes will seemingly not work.

For example, at one point I had something like this:

class RootWithMain extends Component {

render() {

return (

<Root>

{this.props.children}

</Root>

);

}

}

class Root extends Component {

render() {

return (

<Provider store={this.props.store}>

<BotLandApp />

</Provider>

);

}

}

The problem with the above is that “this.props.children” is passed to Root, but then Root never renders the children. The solution is either to make RootWithMain render the children itself (solution #1 below) or to have Root render them (solution #2 below):

Solution #1

class RootWithMain extends Component {

render() {

return (

<div>

{this.props.children}

<Root>

</Root>

</div>

);

}

}

Solution #2

class Root extends Component {

render() {

return (

<Provider store={this.props.store}>

<div>

{this.props.children}

<BotLandApp />

</div>

</Provider>

);

}

}

Note: if Root has your <Provider> tag from react-redux as shown above, then this.props.children should appear underneath it if you expect the children to be connected to the Redux store.

I don’t think I ever figured out the “correct” way to do this. EJ suggested something like this:

Router.run(routes, function(Handler) {

React.render(<Handler locales={i18n.locales}/>, document.body);

});

This captures “i18n” from the enclosing scope into the render function.

The way I did it though is this potentially hacky way:

ReactDOM.render((

<Router history={hashHistory}>

<Route path=”/” component={RootWithMain} mainInstance={this}>

</Route>

</Router>

), document.getElementById(‘root’))

class RootWithMain extends Component {

render() {

const mainInstance = this.props.route.mainInstance;

return (

<Root

onKeyUpHandler={mainInstance.onKeyUp.bind(mainInstance)}

{this.props.children}

</Root>

);

}

}

If you want to be able to take advantage of the routing properties of react-router without necessarily being able to press the browser’s back/forward buttons, and you also don’t want the URL gunk that hashHistory gives, you can use memory history.

To switch routes, you could either use “<Link to=“/foo”>Click me</Link>”, or you could programmatically change routes by using “this.history.push(‘/foo’);” as shown below. If you want to “pop” something off of the history, it’s actually called “goBack”, as in “this.history.goBack()”.

‘use strict’;

const ReactDOM = require(‘react-dom’);

import React from ‘react’;

import Root from ‘./react_and_redux/containers/root’;

import TestButton from ‘./react_and_redux/containers/testbutton’;

import { Router, Route, createMemoryHistory } from ‘react-router’;

import configureStore from ‘./react_and_redux/store/configurestore’;

const store = configureStore();

class Main {

constructor() {

this.setupReact();

}

testChangeRoute() {

this.history.push(‘/about’);

}

setupReact() {

this.history = createMemoryHistory(’/’);

ReactDOM.render((

<Router history={this.history}>

<Route path=”/” component={Root} store={store} mainInstance={this}>

<Route path=‘about’ component={TestButton} />

</Route>

</Router>

), document.getElementById(‘root’));

}

}

First of all, react-router probably will work for you, but if it doesn’t, consider changing how you serve files:

  • If you can dynamically serve files (e.g. using a Node server instead of static pages), then do that and switch to browserHistory. This solution is totally reasonable.
    • Update (9/5/2017): I don’t know why I didn’t use browserHistory to begin with. It looks like I could just drop it in by importing browserHistory and specifying that instead of createMemoryHistory. However, then Login is broken until I type “debugMain.reactRouterHistory.push(‘/login’)” in the console. Also, you’re able to use the browser back/forward buttons which is PROBABLY fine but I haven’t fully tested that. One case where it’s not fine is when you’re done logging in; I need to clear the history so that you don’t go back to the login back.
      • Also, if I do this, I need to get rid of everything that has to do with query parameters.
  • Look into whether your static host can reroute 404s to your page, that way “/users”, “/about”, and “/doesnotexist” will all go to the same page. S3 and CloudFront should be able to do this.
    • If you do this, you’ll get a 404 status in addition to getting the page served up, so it’s not a good idea. On CloudFront, you could have these be 200s instead.

Note: I last tried this out on 6/7/2016 and the problem I ran into was that the enter/exit animations both worked, but the positions were totally wrong. I could fix this in transit by adding “position: absolute” and “top: 0”, but then it would be stuck that way afterward.

There’s a demo for react-router-transition here that shows what I wanted to do when you click “another <animation>” on the second row at the top.

I changed setupReact to look like this:

ReactDOM.render((

<Router history={history}>

<Route path=”/” component={Root} store={reduxStore} mainInstance={mainInstance}>

<Route path=‘news’ component={RootWithTransition} left={false} >

<Route path=‘test’ component={News} onPlay={playGame} />

</Route>

<Route path=‘store’ component={RootWithTransition} left={true}>

<Route path=‘test’ component={Store} onPlay={playGame} />

</Route>

<Route path=‘blueprints’ component={Blueprints} onPlay={playGame} />

<Route path=‘editblueprint’ component={EditBlueprint} />

<Route path=‘login’ component={Login} />

</Route>

</Router>

), document.getElementById(‘root’));

Then, I made RootWithTransition:

import React from ‘react’; // eslint-disable-line no-unused-vars

import { Component } from ‘react’;

import { RouteTransition, presets } from ‘react-router-transition’;

const styles = require(’../../../stylesheets/main.less’);

export default class RootWithTransition extends Component {

constructor(props) {

super(props);

}

render() {

const styleToUse = this.props.route.left ? presets.slideRight : presets.slideLeft;

return (

<RouteTransition

pathname={this.props.location.pathname}

component={false}

className={styles.transitionWrapper}

{…styleToUse}

>

<div>

{this.props.children}

</div>

</RouteTransition>

);

}

}

I added a style for the transition-wrapper:

/I’m using CSS modules, so I use camel case/

.transitionWrapper {

opacity: 1;

}

Finally, I changed all of my routes to look like “/store/test” or “/news/test”.

Bluebird promise not being returned

Section titled Bluebird promise not being returned

I wasted a couple of hours looking into an issue where I got this Bluebird warning: “Warning: a promise was created in a handler but was not returned from it” (reference)

My code looked something like this:

this.reactRouterHistory = syncHistoryWithStore(this.reactRouterMemoryHistory, this.store);
this.reactRouterHistory.listen((location) => {
if (_.isNil(location)) {
return;
}
const pathname = location.pathname;
if (pathname.startsWith('/news')) {
return someAsyncFunction();
}
});

The problem was around “someAsyncFunction” above. My conclusions after all of the research that I did:

  1. The warning is due to how react-router-redux works with Bluebird and isn’t a “real” warning.
  2. If you ever have this pattern in your code, it’s probably better to just add the “someAsyncFunction” call to your componentDidMount of the “news” (in this case) component.
  3. If you ever want to work around this without actually fixing anything, just put a _.delay(() => someAsyncFunction(), 0); there. (reference)

I ended up going with solution #2, but I still had a problem because of how I was pushing the route to /news:

someOtherAsyncFunction()

.then(() => {

dispatch(push(‘/news’)); // this is synchronous, so there’s no reason to end the Promise chain here.

})

.then(() => {

// more normal code…

});

This ends up being a VERY particular case about Bluebird. See this note.