react-router: ActiveState changes don't propagate if any single parent in hierarchy declares shouldComponentUpdate

If I have a component using ActiveState mixin and some of its parents declare shouldComponentUpdate, the child won’t update when active state changes.

This is not normal because usually parent’s shouldComponentUpdate doesn’t magically cut the child off updates. If child listens to some store and calls setState, it will be updated. But this is not the case with context.

Even if parent’s shouldComponentUpdate implementation takes nextContext into account, it won’t even get nextContext if it doesn’t declare contextTypes itself. So basically, for ActiveState to work in a project that heavily uses shallow-comparing shouldComponentUpdate or something similar, all ancestors of ActiveState-using component need to also have ActiveState.

See this React issue and this fiddle.

Obviously it’s a React’s problem per se but since router relies on context so much, it’s worth documenting.

About this issue

  • Original URL
  • State: closed
  • Created 10 years ago
  • Comments: 87 (80 by maintainers)

Commits related to this issue

Most upvoted comments

For anyone who’s already using React Router Redux, I made a pretty simple Link component and it’s working on two apps I’m working on:

import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';

const ReduxLink = ({ className = '', activeClassName, to, path, children }) => {
  let finalClassName = className.split(' ');

  if (activeClassName && path === to) {
    finalClassName.push(activeClassName);
  }

  return (
    <Link to={to} className={finalClassName.join(' ')}>
      {children}
    </Link>
  );
};

export default connect(
  (state, ownProps) => Object.assign({}, ownProps, { path: state.routing.locationBeforeTransitions.pathname })
)(ReduxLink);

Btw, it can replace both Link and IndexLink components.

Actually, better implementation:

function createContextSubscriber(name, contextType = React.PropTypes.any) {
  return class ContextSubscriber extends React.Component {
    static propTypes = {
      children: React.PropTypes.isRequired,
    }

    static contextTypes = {
      [name]: contextType,
    };

    constructor(props, context) {
      super(props, context);

      this.state = {
        lastRenderedEventIndex: context.eventIndex,
      };

      this.unsubscribe = null;
    }

    componentDidMount() {
      this.unsubscribe = this.context[name].subscribe(eventIndex => {
        if (eventIndex !== this.state.lastRenderedEventIndex) {
          this.setState({ lastRenderedEventIndex: eventIndex });
        }
      });
    }

    componentWillReceiveProps() {
      this.setState({ lastRenderedEventIndex: this.context.eventIndex });
    }

    componentWillUnmount() {
      if (!this.unsubscribe) {
        return;
      }

      this.unsubscribe();
      this.unsubscribe = null;
    }

    render() {
      return this.props.children;
    }
  };
}

Thanks for chiming in! It will be great when React solves that.

But right now people do extreme things (that’s a base Component class for the whole framework!) just to get around the issue, and many aren’t even aware that the issue exists.

It’s easy to fix on the router level. Let _subscribe be a private API for now if we don’t want people to depend on it. When React solves this, we can bump the minimum version and remove the hack.

I really want RR 1.0 to fix this, and if it comes out before React fixes sCU with context, I’d vote for doing a sideways subscription.

This is now out in 3.0.0-alpha.1. Please give it a try!

@gaearon Yeah, I know. And it also changes the activeClassName before animations, but at least it’s a fallback until this is solved “natively” in react-router. Same to your gist, but probably in a more performant way.

We don’t currently expose a non-deprecated imperative way to listen to the router state

API-wise, shouldn’t this.context.router.listen() do this? I would expect that it abstracts away the history from me and knows about the component states since I get it from the context managed by a component.

No, you need to track the actual rendered status of the <Link> itself.

The way i’m solving this right now is to create a HOC around <Link />:

import React, { PropTypes, Component } from 'react';
import { Link } from 'react-router';

/**
 * Forces <Link /> to rerender itself on route change
 * This is not officially supported on react-router so we compose this behavior in a HOC
 *
**/
class ActivatedLink extends Component {
    static contextTypes = {
        history: PropTypes.object.isRequired
    }

    componentDidMount() {
        this.unlisten = this.context.history.listen(() => {
            this.forceUpdate();
        });
    }

    componentWillUnmount() {
        this.unlisten();
        this.unlisten = null;
    }

    render() {
        return <Link { ...this.props } />;
    }
}

export default ActivatedLink;

I think its probably better to keep this behavior out of the core and document some patterns to solve this problem as not everyone wants this behavior.

Might be a hack, but <Link ignoreChanges={animating}/> could work, you’ll know you’re animating, so it doesn’t require extra finesse on the app’s part.

It’s hard to imagine any other use-case than animation, and a quick prop hack will work w/o requiring a bunch of changes for this one thing.