-
-
Notifications
You must be signed in to change notification settings - Fork 3.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
[5.1.0-test.1] Connected components don't re render after state update #965
Comments
Thanks. Can you put together an example reproducing the issue? |
Closing, as the problem was probably due to an internal change of mine. Will re-open if I can understand the issue. EDIT: Actually it's not, previous version works and new version doesn't work. Will try to figure out what's going on and provide some example if I can |
My guess is that It’s worth reading https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html and applying that information to latest changes. |
(I’m referring to React Redux implementation, not your application code) |
@gaearon is correct. The current implementation calls gDSFP when props change, and then the updater changes the state, which causes gDSFP to re-run, which finds that the state is the same, which sets shouldComponentUpdate to false, and nothing re-renders. so we have:
because of this, react-redux can't store the derived props in state and shouldn't be using gDSFP at all. Instead, I think that Connect should be managing the 2 possible mutations independently. The store subscription can simply update the state to the new store state to force a re-render check, and we run updater in shouldComponentUpdate to retrieve the derived props from the new props and the store state. Once the derived props have been generated, we can store them to use in render(). As I'm relatively unfamiliar with the internals of react-redux, I have a fix in a fork, but I'm working on making existing tests pass before making a pull request. I have one question regarding one of the tests. in // The store state stays consistent when setState calls are batched
ReactDOM.unstable_batchedUpdates(() => {
store.dispatch({ type: 'APPEND', body: 'c' })
})
expect(childMapStateInvokes).toBe(2) Why is it expected that child mapStateToProps would be called? In this case, the props and the state remain unchanged. It seems to me that this test is wrong. Before I change this, I wonder if the experts could weigh in? Shouldn't we only run mapStateToProps to generate derived props when either (1) the connected component's props change or (2) the store's state is changed? Not on (3) when any action is dispatched? My assumption here is that mapStateToProps is always a pure function, but maybe I'm missing something? Thanks in advance |
This commit fixes reduxjs#965 The essence of the problem is that getDerivedStateFromProps is called when the incoming props OR incoming local state changes. So we cannot store anything in state that is needed in shouldComponentUpdate. This commit splits up the tracking of incoming props, incoming store state changes, and removes getDerivedStateFromProps and the usage of local state to store any information. Instead, local state is used as a flag solely to track whether the incoming store state has changed. Since derived props are needed in shouldComponentUpdate, it is generated there and then compared to the previous version of derived props. If forceUpdate() is called, this bypasses sCU, and so a check in render() compares the props that should have been passed to sCU to those passed to render(). If they are different, it generates them just-in-time. To summarize: 1) shouldComponentUpdate is ONLY used to process changes to incoming props 2) runUpdater (renamed to triggerUpdateOnStoreStateChange) checks to see if the store state has changed, and stores the state, then updates the counter in local state in order to trigger a new sCU call to re-generate derived state. Because of these changes, getDerivedStateFromProps and the polyfill are both removed. All tests pass on my machine, but there is at least 1 side effects to the new design: - Many of the tests pass state unchanged to props, and pass this to child components. With these changes, the two updates are processed separately. Props changes are processed first, and then state changes are processed. I updated the affected tests to show that there are "in-between" states where the state and props are inconsistent and mapStateToProps is called with these changes. If the old behavior is desired, that would require another redesign, I suspect.
@cellog : thanks for the research and the PR! I'm off on a vacation atm, but I've been meaning to take a day to try pushing some of the React-Redux work forward. No guarantees on a timeline, but this is definitely now on my list to look at. It looks like that test was added to fix #86 . At that point, React-Redux still had problems with connected children. The classic example is a connected list with connected list items, and if you deleted the data for a list item, the list item's Per your questions at the end, here's the expected sequence when an action is dispatched as of v5:
Sequence in this test:
So sure, I'd expect the child's |
I'm travelling at the moment and unable checkit for a week or so as don't remember it on top of my head. I'll check on this when I'm home again. |
FYI, I don't think instance properties are async-unsafe. In the memoization example: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization class Example extends Component {
// State only needs to hold the current filter text value:
state = { filterText: "" };
// Re-run the filter whenever the list array or filter text changes:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// Calculate the latest filtered list. If these arguments haven't changed
// since the last render, `memoize-one` will reuse the last return value.
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
<Fragment>
<input onChange={this.handleChange} value={this.state.filterText} />
<ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
</Fragment>
);
}
} The recommended approach uses an instance variable So that is not an issue. |
Instance properties can be tricky to get right. Function bindings are okay. (In fact they're often necessary!) But be wary of Other instance properties can be okay as well, although I would suggest avoiding them when possible. If you look at our Does this clarify? I can elaborate if not. |
@bvaughn : thanks, that helps. I think I have two general points of concern:
@cellog : based on those comments, I'm not sure that the current PR code is valid. Don't have any specific suggestions atm, though. |
@bvaughn : lemme expand on that a bit and see if you have any suggestions.
In shouldComponentUpdate() {
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
} In v5, we moved all the heavy lifting on the comparisons out into separate memoized selectors (homegrown, no use of Reselect). The core logic can be seen here: react-redux/src/connect/selectorFactory.js Lines 74 to 84 in 3e53ff9
We then re-run this giant selector in both the Redux store callback and, up until now, in What I'm concerned about is that it looks like having the selector get run multiple times during the render phase would cause its memoization to be wrong. It would be saving references to "unused" versions of In a sense, the selector really is a gigantic instance variable, as it's both an instance variable and stateful internally. The only idea that's coming to mind at the moment is if we were to somehow split up the selector logic to separate out the "has something changed?" and "save the results" steps. |
I think the short version of the question is: How can we calculate props for our child component and prevent unnecessary renders of the child component? React-redux does it before sCU currently |
It would be up to the wrapped component to provide its own sCU implementation. Our sCU is only concerned with if the mSTP selector result has changed or if the incoming props are different, which would require a re-render of the wrapped component. Any render blocking beyond that is up to the wrapped component (or a better use of mSTP).
It's not so much that the memoization is wrong. It's working as intended, but is called multiple times and causing the local "updater" (previously We're basically cheating in gDSFP by keeping a local state hidden and making the function impure/non-idempotent. In retrospect, I shouldn't have merged in that async safety PR as-is and required that to be fixed. React 16.3 hid the problem, but 16.4 makes it readily apparent. Async concerns aside, we have just a bad use of the React lifecycle here. I'll look through this stuff tonight and see if there's a better refactor that we're just missing. |
Also of some interest (although quite a ways away and very much breaking): facebook/react#13216 |
You shouldn't update instance values from render phase lifecycles. This is why my post above said to only update these values from a commit phase lifecycle ( So long as you're following this rule, re-running render multiple times should not cause any improper updates.
Not sure I follow here either. Why would calling a memoized function multiple times cause anything to be wrong? Presumably, the function would return the memoized value unless the input changed– in which case it would recalculate. In an async app, you might want a cache size that's larger than one– so you low and high priority updates don't unnecessarily bust your cache– but I don't see where multiple calls implies incorrectness, provided you aren't updating instance properties during render phase (which you shouldn't be). |
A bit of a new idea occurred to me on how to solve this. Basically, what we are looking for is a way to generate derived props and then run the complete life cycle based on those props, and only do it if they have changed. And we just learned the safest place to do this is post-render or in render itself. So we need a way to run shouldComponentUpdate after render, and the most useful one would do what PureComponent does, but with the generated props. So what if we made connect 2 components? The first would simply take incoming props and incoming store state, and call mSP to generate derived props. It would do this every time, and then pass the result to its child, a "middle man" PureComponent-based component. The component would only update on props changing, and so we could simply put updateChildSubs() in the componentDidUpdate() of the child. That component would have as its child the WrappedComponent. This way, no funny business is needed with instance variables, and we are fully async safe. If this indeed seems viable as a solution to @bvaughn (from cursory read) I can whip up a sample and see if I can get all the existing tests to pass with it. |
@cellog That's what the Again, the core issue is the misuse of gDSFP. We're literally doing the first thing the 16.4 blog post says not to do. So, the refactor would be to not do that. We should do something closer to @bvaughn's subscription example: https://gist.github.com/bvaughn/d569177d70b50b58bff69c3c4a5353f3 |
@timdorr's comment sounds right to me, although I'm skimming this thread a bit 😅 The only downside to that approach is that it can cause sync deopts for async rendering, but I think it's necessary at least until/unless a suspense-based solution is in place. |
Yes, the current source misuses gDSFP, that is clear. The issue I see is that the subscription example you linked to ONLY uses the data from the external source. the rest of the props are not used in deriving state. React-redux uses both props and the external data from the redux store to derive props. I'll see if I can turn these abstract words into code on another branch, it should make it easier to understand what I'm trying to say. Appreciate the patience and feedback. Basically, right now, we do:
And we try to handle the sCU and deriving and subscriptions in the single component, which causes issues since we can't get derived props in time for sCU unless we calculate in sCU, which is not OK, as it turns out. I'm suggesting that instead:
This way, we can use gDSFP and handle subscriptions in cDM and cDU in the And, to continue, if In short, this removes the race conditions of generating derived props and using those props to decide whether to render. The race condition is that we need to store BOTH the generated props, and the store state that we used to generate them in state to work properly. That's why gDSFP and a single connecting component is unlikely to work for react-redux (as I see it). Make more sense? |
@cellog : unfortunately, I don't think that's a viable option for now. Having only one wrapper component for connect could be considered part of our API contract so far, and there's plenty of code in the wild (even if only in tests) that expects just one wrapper. Someone else did suggest the 2-step approach in the comments of #898 (see some of the CodeSandbox links). I may take some time tomorrow to look at this more as well. |
@bvaughn : right, I was agreeing with you on the instance variables thing, and saying that our current approach violates that. Per memoization, the problem I'm seeing is that in an async scenario, rerunning the render phase with varying values would cause the memoization to not cache hit as desired, and also effectively update an "instance variable" in the render phase. |
What I'm getting from the React maintainer's comments and from the excited end-users commenting on that PR is that perhaps react-redux will work best if both subscription and dispatch is handled via React, and will resolve tearing and other async issues since React has to do that. I'm going to abandon this PR, and temporarily use my fork or something similar in the case where I need it (docs website for ion-router) until things settle a bit. I don't think it is wise to do a half version of the switch, the complexity is too large. I will say this is all very exciting, because React is finally going to be capable of managing state internally, which was always why redux was external: it couldn't do it properly before. |
@cellog : well, we still need to come up with a resolution for our current issue. This started with #897 and #919 , and our current goal is to patch up the existing implementation to resolve any deprecation warnings related to lifecycle methods. Per my writeup in #950 , after that I'd like to rework the internals to use the new context API, and that would be a major version bump. The chatter in that other issue can be ignored for now, especially around rearchitecting things to be React-centric. We need to figure out a solution that works with our current architecture. |
Well, this PR does solve the deprecation issue. So perhaps that's the answer. We know that solving async will require major surgery, so future-proofing becomes unnecessary |
What I mean by that is that the memoized selector is itself stored as an instance variable, and if I call it, it's going to cache the values I passed in. It may be stretching the concept, but I can see that being similar to directly updating actual instance variables on the component. Our selector logic is all homegrown, and definitely just has a cache size of 1 right now, as seen here: react-redux/src/connect/selectorFactory.js Lines 18 to 31 in 3e53ff9
|
So per the last couple comments: I guess there's a question of just how async-safe we need to make 5.x. We seem to have fixed the I think moving to new context in a 6.0 release is a step in the right direction, but we're probably going to run into the same issues regarding when we try to calculate props and |
The memoized selector is the instance variable, and it isn't changed. The value it returns may change, based on the input, but that's okay. 😄 It's no different than if you were computing the values inline each time, just more efficient. The important thing is that work that's done during render doesn't somehow stick around and cause problems if that render is bailed out on before committing. In this case, it wouldn't, since the memoized selector would only return the bailed-out-on-computations if it were passed the bailed-out-on-parameters (which it shouldn't be). |
So the problem is that input may change between sCU and render? When we are taking about "input" is that props/state, or are you talking external things like store state? |
I've got a couple pieces floating around in my head that I think may let us solve this. Ever since Brian posted his
Meanwhile, we've established that updating instance variables in the render phase is a no-no. We also know that Also important: if you pass a completion callback to I threw together a quick example over at https://codesandbox.io/s/yjx19v21nz that logs out some of the differences in execution between functional Looking in const callback = (value: Value | void) => {
if (this._hasUnmounted) {
return;
}
this.setState(state => {
// If the value is the same, skip the unnecessary state update.
if (value === state.value) {
return null;
}
// If this event belongs to an old or uncommitted data source, ignore it.
if (source !== state.source) {
return null;
}
return {value};
});
}; The onStateChange() {
this.runUpdater(this.notifyNestedSubs)
}
runUpdater(callback = noop) {
if (this.isUnmounted) {
return
}
this.setState(prevState => prevState.updater(this.props, prevState), callback)
}
function getDerivedStateFromProps(nextProps, prevState) {
return prevState.updater(nextProps, prevState)
}
function makeUpdater(sourceSelector, store) {
return function updater(props, prevState) {
try {
const nextProps = sourceSelector(store.getState(), props)
if (nextProps !== prevState.props || prevState.error) {
return {
shouldComponentUpdate: true,
props: nextProps,
error: null,
}
}
return {
shouldComponentUpdate: false,
}
} catch (error) {
return {
shouldComponentUpdate: true,
error,
}
}
}
} Also, note that the "updater" function closes over the store, and grabs the latest Redux state every time it runs. It also is reused as the entire implementation of I haven't fully nailed this down in my head yet, but my gut says we can come up with an approach that works based on all this info. Here's a 100% completely untested code sample that demonstrates what I'm thinking might work: class ConnectAdvanced extends Component {
constructor(props) {
super(props);
const store = props[storeKey] || context[storeKey];
const reduxStoreState = store.getState();
const childPropsSelector = createChildPropsSelector(),
this.state = {
ownProps : props,
childProps : childPropsSelector(reduxStoreState, ownProps)
childPropsSelector
store,
reduxStoreState,
};
}
shouldComponentUpdate(nextProps, nextState) {
// standard shallow equality check on props and state
}
static getDerivedStateFromProps(currProps, currState) {
const {ownProps, childProps, reduxStoreState, childPropsSelector} = currState;
if(shallowEqual(currProps, ownProps)) {
return null;
}
const newChildProps = childPropsSelector(reduxStoreState, currProps);
if(newChildProps === childProps) {
return null;
}
return {
ownProps : currProps,
childProps : newChildProps,
};
}
onStateChange = (notifyNestedSubs) => {
const newReduxStoreState = this.state.store.getState();
this.setState(currState => {
const {ownProps, childProps, reduxStoreState, childPropsSelector} = currState;
if(reduxStoreState === newReduxStoreState) {
return null;
}
const newChildProps = childPropsSelector(newReduxStoreState, ownProps);
if(newChildProps === childProps) {
return null;
}
return {
reduxStoreState : newReduxStoreState,
childProps : newChildProps,
};
}, notifyNestedSubs);
}
} So, the key here is that we keep all the relevant info in component state, access it in both My main concern here is still the idea of the memoized selector getting called multiple times in a row in an async scenario, and thus the cache wouldn't work properly. I think we can resolve this by making the selector not actually cache values internally. If you look at the snippet I pasted earlier, it stores references to:
Well, the sample I just wrote up would keep I'm babbling a bit here, but I think this can all work. Thoughts? |
Follow-up thought. This relies on having the store in the component and and still subscribing there. I don't think this would work with the POC approach I had in #898 where I switched to having |
That's a much more nuanced usage of sets tate, it might make a difference. The only thing I immediately see is that the notification of nested subs should happen if child props doesn't update, i.e. Inside the block that returns null. In addition, if the state changes, but our child props don't change, we still need to store the changed redux state, in case a props change would cause an update to child props (we will be storing a stale copy of store state). The first issue is fixable via doing the "componentDidUpdate should update nested subs" as the callback to setState, but then if we fix the 2nd issue, it will trigger a false update. Perhaps doing a narrower sCU that ignores the saved redux store state would solve that issue. The pseudocode is worth un-pseudoing and seeing how it plays out. |
Also relevant to 2 components is we need to use forwardRef, but that's a next step issue. Progress is yummy! P. S. I have been toying with writing a "redux in react" just to see how hard/easy it would be to do it and transparently support the existing store enhancers. It's an interesting thought experiment, since the state being in component state means initialization of state happens in the constructor, but anything fancy would have to be in componentDidMount. If I find anything relevant to the v7 talks, I'll share. Basically, if it ends up looking simpler, it might be a better way to implement react-redux. Right now, it's looking like the connect component will only need 1 element, the wrapped component itself, because we can do all the "should we update" stuff inside the render props context consumer. Stay tuned! |
@cellog : per my sandbox, the Yes, you're right about needing to update the cached copy of the Redux store state. Really, the I'm still really not sold on any of these "Redux in React" ideas, but I'd be interested in seeing what you put together nonetheless. I want to tackle implementing this idea, but I'll be busy over the next couple days, so I may not immediately have time. If someone else wants to try first, I'd suggest a new branch off the existing |
I've been trying to poke at this, but I'm realizing it's a bit more complicated than I first thought. The biggest issue is the separation between the lower-level My comments above conflated the two parts of the implementation. I was thinking of changing A quick search on Github suggests that there are a few people out there doing that, so I don't want to break that use case (especially in what's supposed to be a minor version). I supposed it could be handled by kind of special-casing knowledge of const selectorResult = selector(nextState, nextProps, otherArgs);
let childProps;
if(selectorResult.mergedProps) {
childProps = selectorResult.mergedProps; // default connect case
}
else {
childProps = selectorResult; // custom selector factory was provided
} |
I started poking too, and although it's only been a couple minutes, most of the tests are passing. However, subscriptions are not working yet, so that's the next step. I got my redux-in-react working, except the dev tools browser extension won't work with it because the author hard-coded in using redux's createStore instead of using the one passed to it :). So I haven't quite gotten over that stumbling block. In any case, I'm going to try to finish poking at the subscriptions issue and get a working PR today. If you beat me to it, all is good :) I'm learning tons about both react-redux and react, which is what I wanted in the first place. |
Yeah, I've got several tests failing on my end as well. Looks like it's mostly around children expecting to see updated props from parents before mapState runs. I'll try to find a couple minutes to throw my WIP up on a branch later, and we can compare notes. |
Ok, so I got it working but it's the same problem I found in the pull request I made. We can't get both props and Redux state update at the same time unless we do some dangerous caching magic. For example, in the "should subscribe properly when a middle connected component does not subscribe" test in The solution could be to create a second kind of subscription, which would simply tell child components that redux is about to update, and to delay processing props until the redux update comes in. But this is inherently dangerous in an async environment. So perhaps being 16.4-ready means the only way to do this is with the new context? Also worth considering and perhaps more to the point: getDerivedPropsFromState behaves completely differently in 16.3 and earlier. I think that the current 5.1-test.1 should be released as a version for 16.3 and earlier, and then release a new version for 16.4+ based on the new behavior. So the 16.3 version would use the polyfill, but the 16.4+ version wouldn't. |
https://github.com/cellog/react-redux/tree/another-fix-5.1 is the branch I'm working on. All tests pass but the 3 mentioned in the 2nd to most recent commit. Can't figure out an easy way to fix this yet, going to try again Also FYI, the thing seems to work in 16.3 the same way, because the gDSFP implementation doesn't care about state changes |
I can't figure it out. We need a way to store the last state and pass that to The best solution I see is to split up into 2 components, one that manages redux state by retrieving from a Context Consumer, and passes that into the component in order to process props and redux store simultaneously. Honestly, I'm not sure there is another solution! |
getDerivedPropsFromState was buggy, so we can't release code relying on that bug. React team really talked a lot about it. If no solution is possible, at this point a breaking change should be considered |
Yeah, I'm see mostly the same test failures myself. Just pushed up my WIP as https://github.com/reduxjs/react-redux/tree/5.1-rework-memoization . As @ematipico said, 16.3 is effectively buggy, and 16.4 is out, so I see no reason to try to target 16.3. I'm not ready to give up on this approach yet, but I agree this is awfully tricky. If necessary, yeah, we may just have to say that there is no stopgap 5.x release, and move on to seeing what a 6.0 would look like. |
One new possibility that occurred to me this morning: if we put the store in state instead of its value, it may be possible to delay updating until we know whether the parent component is updating state. It would require changing subscription updates so that if it is notifying a nested subscription, we tell it "heads up, a parent component just updated" and then it can watch and wait. If no props change comes down, it would trigger a redux update, otherwise edit could pull in redux state just in time for props. I may have time to turn this into code. It will likely break bathed redux actions, but hard to know until I write the code. |
@markerikson check out the pull request. The solution was so simply, it's stupid I didn't try it before. Instead of using our cached store state, I changed gDSFP to pull the current store state, and voila, the unnecessary mapState calls went bye-bye. Pull request #980 is the implementation. I hadn't tried this because in past incarnations it broke batched redux actions, but since we still use the cached store state in our check of whether to update the redux store state, that keeps us from breaking on batched dispatch. Yay! |
Hi all, props change #1 -> runUpdater -> setState(props, shouldComponentUpdate: true) -> May that be a React issue instead? Thanks |
@nguyenanduong : which version of React-Redux are you using? Also, we've already established that |
Yes, I am on 5.1.0-test.1 |
Thanks, downgrading to 5.0.7 seems to work for me |
@markerikson Could a note of the fact that 5.1.0-test.1 is broken be added to the releases description. Currently, it looks like it's encouraging usage. I eagerly upgraded, only to find that it did, in fact, break everything 😄 |
Hmm. Good point. Let's see.. Oh hey, you CAN edit release notes after the fact. Okay, will do. Thanks for the notice! |
I gave a first try to the new beta and I can see some problems (unless I should make some changes).
The components that are connected to the store via
connect
don't get updated (they don't re render). While debugging I can see that the functionmapStateToProps
gets called correctly (and I can see the changes) after the action but the component doesn't re-render hence it doesn't show the new values.I hope it could help
The text was updated successfully, but these errors were encountered: