React: "passthrough" children don't re-render when parent changes

Created on 9 Jun 2015  Â·  12Comments  Â·  Source: facebook/react

Given the following Components with the following implementations:

home.js:

render () {
  return (
    <div className="home">
      <Router>
        <Route path='/'>
          <span>Hi from Index</span>
        </Route>
        <Route path='/user/martin'>
          <span>Hi Martin</span>
        </Route>
      </Router>
    </div>
  )
}

router.js:

componentDidMount () {
    var self = this;

    history.on('pushstate', function(pathname) {
      self.setState({});
    })

    history.on('popstate', function(pathname) {
      self.setState({})
    })
}

render () {
    return React.createElement('div', { className: 'Router' }, this.props.children);
}

route.js:

render () {
  if (!match(this.props.path)) return null;
  return React.createElement('div', { className: 'Route'}, children);
}

I'm running into the situation that since I'm not creating the <Route> in the parent and passing it through from the owner, the children do not re-render when the parent's state changes.

What's the best way around this? Seems like a pretty common use case.

Most helpful comment

This is intentional. It is an optimization we've had for a long time and extremely rarely does it ever show up as an issue (as evident by the fact that @syranide didn't even know about it).

Here is an example of one kind of optimization you can do (manually or automatically): https://github.com/facebook/react/issues/3226

It is also useful for container components that only updates their own state but their parents doesn't rerender. E.g. a scrolling surface.

We do support mutation of your own state as a convenience but we do make certain assumptions about your code style.

This optimization relies heavily on the hard requirement that render must be pure, which is the only hard FP requirement of React:

1) render should be idempotent. I.e. it should render the same thing regardless when it is called. Neither of these examples uses idempotent render functions since they're reading from (globally) mutated state. (match and Date are not immutable) We've been thinking about ways of trying to enforce that. The ideal solution is to put time into your own state, but as a convenience you can call forceUpdate on something that you know is reading from global state whenever it changes. I.e. the thing calling match or getTime needs to call forceUpdate.

2) render should not cause side-effects. Even if you use mutable state in your objects, you shouldn't mutate them within the render.

ReactElements and their props are immutable. That is already enforced through Object.freeze and warnings. However, it is ok for state within them to be mutable. E.g. <Foo bar={this.state.mutableObject} />

We do have one heuristic. We assume that you won't combine mutable state with reusable elements. E.g:

var mutableObject = { value: 1 };
var reusableElement = <Foo bar={mutableObject} />;

class App extends React.Component {
  componentDidMount() {
    mutableObject.value = 2;
    this.forceUpdate();
  }
  render() {
    return reusableElement;
  }
}

This last part is a heuristic assumption which has a theoretical case that doesn't violate any of the other rules but still breaks. This case has never showed up yet AFAIK, because it is so awkward and unnatural way to structure your app in React.

However, the OP isn't related to that, this is simply breaking rule number 1.

render should be idempotent.

All 12 comments

I'm running into the situation that since I'm not creating the in the parent and passing it through from the owner, the children do not re-render when the parent's state changes.

Perhaps I'm missing something... they don't? When each component re-renders the children it renders re-render too (unless prevented by shouldComponentUpdate). However, them being created externally in home.js they won't receive any new props as it doesn't re-render too.

On a personal note, I think doing routing that way is poor practice, I don't see any benefit to specifying routes as JSX elements instead of using regular JS structures. I just see downsides.

Perhaps I'm missing something... they don't? When each component re-renders the children it renders re-render too (unless prevented by shouldComponentUpdate). However, them being created externally in home.js they won't receive any new props as it doesn't re-render too.

So in this example you're right that home.js isn't being re-rendered. What's being re-rendered is the <Router> when pushstate or popstate triggers. Intuitively, I would think that would cause the <Route> components to re-render since they're children of the <Router> but that doesn't seem to be the case.

On a personal note, I think doing routing that way is poor practice, I don't see any benefit to specifying routes as JSX elements instead of using regular JS structures. I just see downsides.

Thanks for your feedback, can you elaborate a little bit more on this? I'm not sure what you mean by regular JS structures and what the downsides might be.

So in this example you're right that home.js isn't being re-rendered. What's being re-rendered is the when pushstate or popstate triggers. Intuitively, I would think that would cause the components to re-render since they're children of the but that doesn't seem to be the case.

Yeah that's what surprises me, if Router re-renders then any elements it render will re-render too (you're not using a PureRenderMixin right?). Do you have a working jsfiddle I could look at?

Thanks for your feedback, can you elaborate a little bit more on this? I'm not sure what you mean by regular JS structures and what the downsides might be.

I should rephrase that as defining routes as children to a React element (using JSX is not _necessarily_ a bad idea).

addRoute('/', function(...) { return <span>Hi from Index</span>; });
addRoute('/user/martin', function(...) { return <span>Hi Martin</span>; });

render() {
  return (
    <div className="home">
      {routeObject[route](...)}
    </div>
  );
}

Technically, lookups can now be constant (or log) time and preprocessed/cached instead, doesn't involve React overhead nor does re-rendering Home cause all route elements to be recreated, additionally routes no longer have to be centralized in a single file (may or may not be a good idea). I don't have a neat bullet-proof argument on hand... but you should always strive to use the simplest data structures (not easiest/most flexible) to solve your problem, using UI elements to define routes does not fit that. Just like using a Map to define a pre-defined structure is a bad idea too.

Yeah that's what surprises me, if Router re-renders then any elements it render will re-render too (you're not using a PureRenderMixin right?). Do you have a working jsfiddle I could look at?

No it's not using PureRenderMixin. so here's the simplest example I can think of to illustrate this point: http://jsbin.com/cuxexulajo/1/edit?js,output

In the above example, you'll notice that the <Router> gets updated, but the <Route/>'s do not.

you should always strive to use the simplest data structures (not easiest/most flexible) to solve your problem, using UI elements to define routes does not fit that

Yah, I agree with that sentiment. Mostly just looking into alternatives to react-router, because it's a beast.

No it's not using PureRenderMixin. so here's the simplest example I can think of to illustrate this point: http://jsbin.com/cuxexulajo/1/edit?js,output

That's interesting... cc @sebmarkbage @spicyj It seems React skips re-rendering for elements that pass prev === next. Is this an optimization gone bad or now intentional?

http://jsbin.com/wuzinaqari/1/edit?html,js,output

This is intentional. It is an optimization we've had for a long time and extremely rarely does it ever show up as an issue (as evident by the fact that @syranide didn't even know about it).

Here is an example of one kind of optimization you can do (manually or automatically): https://github.com/facebook/react/issues/3226

It is also useful for container components that only updates their own state but their parents doesn't rerender. E.g. a scrolling surface.

We do support mutation of your own state as a convenience but we do make certain assumptions about your code style.

This optimization relies heavily on the hard requirement that render must be pure, which is the only hard FP requirement of React:

1) render should be idempotent. I.e. it should render the same thing regardless when it is called. Neither of these examples uses idempotent render functions since they're reading from (globally) mutated state. (match and Date are not immutable) We've been thinking about ways of trying to enforce that. The ideal solution is to put time into your own state, but as a convenience you can call forceUpdate on something that you know is reading from global state whenever it changes. I.e. the thing calling match or getTime needs to call forceUpdate.

2) render should not cause side-effects. Even if you use mutable state in your objects, you shouldn't mutate them within the render.

ReactElements and their props are immutable. That is already enforced through Object.freeze and warnings. However, it is ok for state within them to be mutable. E.g. <Foo bar={this.state.mutableObject} />

We do have one heuristic. We assume that you won't combine mutable state with reusable elements. E.g:

var mutableObject = { value: 1 };
var reusableElement = <Foo bar={mutableObject} />;

class App extends React.Component {
  componentDidMount() {
    mutableObject.value = 2;
    this.forceUpdate();
  }
  render() {
    return reusableElement;
  }
}

This last part is a heuristic assumption which has a theoretical case that doesn't violate any of the other rules but still breaks. This case has never showed up yet AFAIK, because it is so awkward and unnatural way to structure your app in React.

However, the OP isn't related to that, this is simply breaking rule number 1.

render should be idempotent.

One possible solution is to use subscriptions in your Route components that listens to the global broadcasting of a new route change (then calls this.forceUpdate()). In componentDidMount you can subscribe and in componentWillUnmount you unsubscribe.

We have various future options pending, like #3398 or #3973, that could both make this easier.

@sebmarkbage thanks for the extremely detailed response. That's what I suspected, though I'm not sure the best way around this. The solution you proposed with observables will lead to multiple re-rendering of nested elements, which I cannot figure out a solution for.

I saw the shouldChildContextUpdate stuff, that seems like it would solve my issue. I tend to think that this particular "optimization" should be off by default though. In the same way that PureRenderMixin is not the default. Seems more in line with the thinking that React gives you fine-grained tools for optimization when you're ready for that.

More generally, I'm failing to understand how this isn't problematic anytime you want to encapsulate the complexity of a global, window-related event (browser history, resize events, geolocation, etc.) into a component. I guess it gets more obscure when you'd like to support nesting.

Now I'm pretty new to React, so I guess what I'm really trying to understand is how to properly inject these global events into the React framework in an encapsulated and clean way.

For example, the way react-router works seems pretty hacky to me, but maybe that's the state of dealing with these global events in React or more likely I'm just not familiar with this concept enough.

@matthewmueller

More generally, I'm failing to understand how this isn't problematic anytime you want to encapsulate the complexity of a global, window-related event (browser history, resize events, geolocation, etc.) into a component.

The recommended approach is to listen to whatever info you need and store it in state, reading from state in your render method – each time you update state, the component will be rerendered.

@spicyj

The recommended approach is to listen to whatever info you need and store it in state, reading from state in your render method – each time you update state, the component will be rerendered.

Yah, that makes sense. How does that fit in with nested Elements of the same type? Is that just one of those tough to deal with edge cases? Or is re-rendering idempotent functions multiple times not a big deal.

More concretely, if the parent <Route> triggers a re-render, it'll also trigger the child <Route>. But then the child <Route> would also get notified of the event and re-render itself again.

Yeah, that could cause multiple renders in some cases. With https://github.com/facebook/react/issues/3398, React could be intelligent about scheduling updates and batching them together though. Also you'll only get unnecessary renders in the case that you have a new element from the parent _and_ new data that's sideloaded, so that may be relatively uncommon.

Awesome, yah I'm looking forward to that.

Appreciate all the help guys – keep up the great work.

Was this page helpful?
0 / 5 - 0 ratings