Christoph Dietze
Christoph Dietze

Reputation: 902

How to pass state from a Router to a Sub-Component

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?

Upvotes: 4

Views: 1613

Answers (1)

Alex Chirițescu
Alex Chirițescu

Reputation: 688

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.

Upvotes: 4

Related Questions