-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ActiveState changes don't propagate if any single parent in hierarchy declares shouldComponentUpdate #470
Comments
Here's my temporary workaround (to be used instead of // -----------------
// index.js
// -----------------
var Navigation = require('./Navigation');
function handleRouteChange() {
Navigation.emitChange();
}
var routes = React.render(
<Routes location='history' scrollBehavior='browser' onChange={handleRouteChange}>
...
</Routes>,
document.body
);
Navigation.injectInstance(routes);
// -----------------
// Navigation.js
// -----------------
'use strict';
var invariant = require('react/lib/invariant'),
assign = require('react/lib/Object.assign'),
PathStore = require('react-router/modules/stores/PathStore'),
{ EventEmitter } = require('events');
var CHANGE_EVENT = 'change',
_routes;
/**
* Provides react-router/Navigation API that can be used
* from outside the components (e.g. from action creators).
*/
module.exports = assign({
injectInstance(routes) {
invariant(!_routes, 'Expected injection to happen once.');
_routes = routes;
},
/**
* Call this method from <Routes onChange /> handler to keep listeners in sync.
*/
emitChange() {
this.emit(CHANGE_EVENT);
},
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
getCurrentPath() {
if (_routes) {
return _routes.getCurrentPath();
} else {
// If _routes have not yet been injected, ask the Store
return PathStore.getCurrentPath();
}
},
makePath(to, params, query) {
return _routes.makePath(to, params, query);
},
makeHref(to, params, query) {
return _routes.makeHref(to, params, query);
},
transitionTo(to, params, query) {
_routes.transitionTo(to, params, query);
},
replaceWith(to, params, query) {
_routes.replaceWith(to, params, query);
},
goBack() {
_routes.goBack();
}
}, EventEmitter.prototype);
// -------------------
// ActiveStateMixin.js
// -------------------
'use strict';
var { PropTypes } = require('react'),
Navigation = require('./Navigation');
/**
* Provides functionality similar to react-router's ActiveState mixin
* but without relying on context and woes associated with it:
*
* https://github.com/rackt/react-router/issues/470
* https://github.com/facebook/react/issues/2517
*/
var ActiveStateMixin = {
contextTypes: {
// Only use context for a function that we know won't change.
// Changes in context don't propagate well:
// https://github.com/facebook/react/issues/2517
makePath: PropTypes.func.isRequired
},
getRouterState() {
return {
currentPath: Navigation.getCurrentPath()
};
},
getInitialState() {
return this.getRouterState();
},
componentWillMount() {
Navigation.addChangeListener(this.handleRouterStateChange);
},
componentWillUnmount() {
Navigation.removeChangeListener(this.handleRouterStateChange);
},
handleRouterStateChange() {
this.setState(this.getRouterState());
},
isActive: function (to, params, query) {
return this.state.currentPath ===
this.context.makePath(to, params, query);
}
};
module.exports = ActiveStateMixin; |
As of RR 0.11, the easiest way to fix it is to have a Flux store for router state like in this tutorial. Then you can implement a custom 'use strict';
var { State } = require('react-router'),
RouterStore = require('../stores/RouterStore');
/**
* Provides exact same functionality to react-router's ActiveState mixin
* but without relying on context propagation that breaks shouldComponentUpdate:
*
* https://github.com/rackt/react-router/issues/470
* https://github.com/facebook/react/issues/2517
*/
var RouterStateMixin = {
mixins: [State],
getRouterState() {
return {
routerState: RouterStore.getRouterState()
};
},
getInitialState() {
return this.getRouterState();
},
componentWillMount() {
RouterStore.addChangeListener(this.handleRouterStateChange);
},
componentWillUnmount() {
RouterStore.removeChangeListener(this.handleRouterStateChange);
},
handleRouterStateChange() {
this.setState(this.getRouterState());
}
};
module.exports = RouterStateMixin; |
@gaearon It should be noted that this solution will only work in a Flux environment, not on the server. That's the reason for using context instead of stores in the first place. |
It proxies to RR's proper It doesn't rely on information from the store—the only reason |
Is |
Oh, nm. In the page you linked to @rpflorence is dispatching "router actions" ... which will be picked up by the |
Yeah, it's something along the lines of this tutorial. index.jsrouter.run((Handler, state) => {
RouterActionCreators.changeRoute(state);
React.render(<Handler />, document.body);
}); RouterActionCreators.js'use strict';
var AppDispatcher = require('../dispatcher/AppDispatcher'),
ActionTypes = require('../constants/ActionTypes');
var RouterActionCreators = {
changeRoute(routerState) {
AppDispatcher.handleViewAction({
type: ActionTypes.ROUTE_CHANGE,
routerState: routerState
});
}
};
module.exports = RouterActionCreators; RouterStore.js'use strict';
var AppDispatcher = require('../dispatcher/AppDispatcher'),
ActionTypes = require('../constants/ActionTypes'),
{ createStore } = require('../utils/StoreUtils');
var _routerState;
var RouterStore = createStore({
getRouterState() {
return _routerState;
}
});
AppDispatcher.register((payload) => {
var { action } = payload;
switch (action.type) {
case ActionTypes.ROUTE_CHANGE:
_routerState = action.routerState;
RouterStore.emitChange();
break;
}
});
module.exports = RouterStore; |
I still don't see how the In order for us to be able to use the actions/dispatcher/store pattern server-side we'd have to have at least one store per router, which is essentially what |
Maybe we're talking about different uses? I don't mean to use This is just a workaround I'm using until we get this shouldComponentUpdate behavior. The only reason for it to exist is to force re-rendering of components that aren't reached due to falsy My mixin proxies to var RouterStateMixin = {
mixins: [State], but adds logic to make it This is kinda equivalent if it makes more sense: var RouterStateMixin = {
mixins: [State],
componentDidMount() {
RouterStore.addChangeListener(this._handleRouterStoreChange);
},
componentWillUnmount() {
RouterStore.removeChangeListener(this._handleRouterStoreChange);
},
_handleRouterStoreChange() {
this.forceUpdate();
}
}; For the server side, it works exactly like |
Ah, yes. That makes sense now. Thank you for explaining. I wonder if we should include this in |
I'd say wait for other reports of this issue. It doesn't seem like a lot of people are using |
You'll have a |
@rpflorence That's not the way I'd do it. I'd reuse the same |
Router.run lives inside of a server side handler, you also have to deal with dumping data into that template, etc. etc. etc. Its very different. |
Says the guy who hasn't done it :P |
Haha. Even if I had two calls to |
what is even |
I don't think this is solved. (Sorry for re-opening if it is; I'm just afraid we'll forget this in 1.0.) The The real solution here is to expose something like It doesn't sound “beautiful” but it solves the problem. This is how I do it in Redux too. |
In other words, don't use the context to pass data, even @sebmarkbage said this. :-) Use the context to wire DI. For passing data not through props, you still need some sideways loading mechanism, and today the best we have is manual subscription model + |
Actually, I think the opposite. You SHOULD pass data through context, not subscriptions. Let React optimize the update model so that it works despite shouldComponentUpdate. React can chose to use subscriptions to do that, facebook/react#3973 or we can use a strategy that walks the tree to find places that needs to be updated. We can make tradeoffs depending on other work that is going on etc. |
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 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. |
@gaearon Sure, let's do as you suggest for now. I have a feeling that React 0.14 is going to be released about the same time as we're ready for a 1.0 though, so we might be doing unnecessary work sidestepping an issue that will soon be fixed for us. Maybe @zpao can give us an idea about when 0.14 stable is going to land.. |
Context is still unofficial API so I’d avoid creating libraries that rely on it as part of their public API. If context changes, React Router or React Redux will shield its consumers, but this would be harder for a library that has a primary purpose of doing something with context. This might also send a wrong signal (“this is how you deal with context”) when all we want to do is to work around a bug in an unofficial feature of React. I’d rather prefer this stays in the router, or at least starts there. We can always extract later. |
I don't know, it just seems odd to me to have code per se here that largely accomplishes the goal of Redux/Relay compatibility. I don't think the experimental nature of context is relevant for a specialized library like the one I'm proposing – it'd only be relevant for people who were already using context, who would be broken anyway by upstream changes in context. |
I might understand it incorrectly but it feels to me that it’s React Router that chooses to rely on a broken React feature (updates to context), not Redux or Relay. It’s a conscious choice that can be avoided (e.g. React Redux avoids it exactly the same way by putting I’m happy to take a stab at a PR for this. Not blaming anyone. I just don’t see what’s wrong with having this code here. |
In practical terms this is something that comes up everywhere – not just React Router but also React Intl, and also any form library that wants to give the user enough flexibility around form layout such that form elements don't have to be immediate children (but don't depend on a separate data store). I think factoring this support out into a separate library gives us cleaner and more reusable way to implement this feature, and gives us a clear domain separation between "this is the workaround for context + SCU" and "this is what React Router needs to do to be Redux-friendly". What do you think is the downside of pulling this support out into a separate library, then using that library as a dependency for React Router? I think we end up at roughly the same place, but we make life easier for other users of React context. |
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.unsubscribe = null;
this.lastRenderedEventIndex = null;
}
componentDidMount() {
this.unsubscribe = this.context[name].subscribe(eventIndex => {
if (eventIndex !== this.lastRenderedEventIndex) {
this.forceUpdate();
}
});
}
componentWillUnmount() {
if (!this.unsubscribe) {
return;
}
this.unsubscribe();
this.unsubscribe = null;
}
render() {
this.lastRenderedEventIndex = this.context[name].eventIndex;
return this.props.children;
}
};
} |
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;
}
};
} |
For anyone who's already using React Router Redux, I made a pretty simple 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 |
Note however that, as |
@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 |
This is now out in |
Yahhooo!! |
This leads to a pretty interesting edge case when working with Relay.
What happens with this logic, then, when using Relay (either with react-router-relay or otherwise) is:
This is not ideal for performance – it results in rendering the links an extra time for (at best) no benefit. I don't think there's a good way around this with the current implementation, since the RR v2/v3 architecture treats all this data loading stuff as entirely downstream, and doesn't give the router any way to know about it. It's really just a caveat emptor now. For the time being, I'm going to recommend that users of react-router-relay continue to use React Router v2 to avoid this performance quirk. |
If I have a component using
ActiveState
mixin and some of its parents declareshouldComponentUpdate
, 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 callssetState
, it will be updated. But this is not the case withcontext
.Even if parent's
shouldComponentUpdate
implementation takesnextContext
into account, it won't even getnextContext
if it doesn't declarecontextTypes
itself. So basically, forActiveState
to work in a project that heavily uses shallow-comparingshouldComponentUpdate
or something similar, all ancestors ofActiveState
-using component need to also haveActiveState
.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.
The text was updated successfully, but these errors were encountered: