4

I am using react-router 2.0.0. Consider the following example:

 import React from 'react';
    import ReactDOM from 'react-dom';

    import { Router, Route, IndexRoute, hashHistory } from 'react-router';

    const Main = React.createClass({
        getInitialState() {
            return {data: 0};
        },
        componentDidMount() {
            setInterval(() => {
                console.log("Imagine I am polling data from a server here");
                this.setState({data: Math.random().toFixed(2)});
            }, 2000);
        },
        render() {
            return <Router history={hashHistory}>
                <Route path="/">
                    <IndexRoute component={MainPage} data={this.state.data}/>
                    <Route path="page1" component={Page1} data={this.state.data}/>
                </Route>
            </Router>;
        }
    });
    const MainPage = React.createClass({
        render() {
            return <div>MainPage, data: {this.props.route.data}</div>;
        }
    });
    const Page1 = React.createClass({
        render() {
            return <div>Page1, data: {this.props.route.data}</div>;
        }
    });

    ReactDOM.render(<Main />, document.getElementById('app'));

My understanding of react.js is that data should usually be passed to child components as props. In the example I am polling some data from a server that should be used by two different child components. Since it is dynamic data I put it in the state.

However, if I define routing in that same component, react-router breaks down because it gets re-rendered. The updated state will not be passed to the child and the console will print:

Imagine I am polling data from a server here
Warning: [react-router] You cannot change <Router routes>; it will be ignored

One workaround that I dislike is to use global state to access the data. Which is what I did in my case.

Is there an elegant solution to this use-case?

Ramusesan
  • 854
  • 2
  • 11
  • 32
Christoph Dietze
  • 869
  • 2
  • 7
  • 14

1 Answers1

4

Option 1: Facebook is offering a way to do this using contexts.

I quickly put together an example using contexts on codepen. MainLayout defines some properties that could be used by the children using the context: users and widgets. These properties are used by the UserList and WidgetList components. Notice they need to define what they need to access from the context in the contextTypes object.

var { Router, Route, IndexRoute, Link } = ReactRouter

var MainLayout = React.createClass({
  childContextTypes: {
    users: React.PropTypes.array,
    widgets: React.PropTypes.array,
  },
  getChildContext: function() {
    return {
      users: ["Dan", "Ryan", "Michael"], 
      widgets: ["Widget 1", "Widget 2", "Widget 3"]
    };
  },
  render: function() {
    return (
      <div className="app">
        <header className="primary-header"></header>
        <aside className="primary-aside">
          <ul>
            <li><Link to="/">Home</Link></li>
            <li><Link to="/users">Users</Link></li>
            <li><Link to="/widgets">Widgets</Link></li>
          </ul>
        </aside>
        <main>
          {this.props.children}
        </main>
      </div>
      )
  }
})

var Home = React.createClass({
  render: function() {
    return (<h1>Home Page</h1>)
  }
})

var SearchLayout = React.createClass({
  render: function() {
    return (
      <div className="search">
        <header className="search-header"></header>
        <div className="results">
          {this.props.children}
        </div>
        <div className="search-footer pagination"></div>
      </div>
      )
  }
})

var UserList = React.createClass({
  contextTypes: {
    users: React.PropTypes.array
  },
  render: function() {
    return (
      <ul className="user-list">
        {this.context.users.map(function(user, index) {
          return <li key={index}>{user}</li>;  
        })}
      </ul>
      )
  }
})

var WidgetList = React.createClass({
  contextTypes: {
    widgets: React.PropTypes.array
  },
  render: function() {
    return (
      <ul className="widget-list">
        {this.context.widgets.map(function(widget, index) {
          return <li key={index}>{widget}</li>;  
        })}
      </ul>
      )
  }
})

var Routes = React.createClass({
  render: function() {
    return <Router>
        <Route path="/" component={MainLayout}>
          <IndexRoute component={Home} />
          <Route component={SearchLayout}>
            <Route path="users" component={UserList} />
            <Route path="widgets" component={WidgetList} />
          </Route> 
        </Route>
      </Router>;
  }
})

ReactDOM.render(<Routes/>, document.getElementById('root'))

Option 2: One solution I am using is the following: In the parent component render method, I do something like this:

{this.props.children && React.cloneElement(this.props.children, {
   prop1: this.props.prop1,
   prop2: this.props.prop2
})}

Then in all the components which are part of a child route, they will get these properties automatically, and can be accessed through their props.

For example, in my project, Admin is the parent component, check out its render method.

As you can see in the routes file, a component which is in a child route under /admin, is PostList. PostList is using the function getAdminPost which is coming from Admin, using its props.

I suggest you define the routes in a separate file, like described in the react-router tutorial.

Alex Chirițescu
  • 688
  • 4
  • 13
  • Let me transfer that solution to my example: I would introduce a new component like so: ``. Store the data in its state and make it propagate data updates via the `cloneElement` stuff as you describe. Hm, interesting and at least this avoids global state but it feels really hacky. It's like doing the state-props update stuff manually that react.js should handle. Also, the props passing is really hidden inside `DataComponent`. Having it explicit as attributes in the HTML is much easier to grasp. – Christoph Dietze Mar 05 '16 at 23:56
  • I agree that it is not that great of a solution either. Now that I think about it, I remember that I was looking at [contexts as well](https://facebook.github.io/react/docs/context.html), just that I can't remember why I haven't used them and decided to go for that solution. Contexts are the official solution for doing this, but they don't recommend abusing it. Ideally, if you know your children component, you would simply pass properties to them as attributes. – Alex Chirițescu Mar 06 '16 at 07:11
  • Passing as attributes does not really work, that's what my example does. Well it does not work when you have state because the Router breaks when it gets re-rendered. – Christoph Dietze Mar 06 '16 at 10:57
  • I quickly put together an example using contexts on [codepen](http://codepen.io/alexchiri/pen/WwQVRG). `MainLayout` defines some properties that could be used by the children using the context: `users` and `widgets`. These properties are used by the `UserList` and `WidgetList` components. Notice they need to define what they need to access from the context in the `contextTypes` object. – Alex Chirițescu Mar 06 '16 at 11:28
  • Although facebook consider contexts as experimental, the `react-router` project relies heavily on it. Whenever you want to programatically navigate to a Route, you need to get access to the router object which can be done through the context: https://github.com/reactjs/react-router/blob/master/upgrade-guides/v2.0.0.md#navigating-in-route-components – Alex Chirițescu Mar 06 '16 at 11:34
  • The last link is dead. – newguy Nov 23 '17 at 11:31