Closed ematipico closed 6 years ago
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 getDerivedStateFromProps
usage pattern is incorrect. Lack of comparisons in it is concerning. The tests probably don’t catch it because they’re not using 16.4 yet.
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:
props change -> getDerivedStateFromProps -> runUpdater -> setState(props, shouldComponentUpdate: true) -> getDerivedStateFromProps -> runUpdater -> setState(shouldComponentUpdate: false) -> shouldComponentUpdate -> returns false
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 connect.spec
, in test should pass state consistently to mapState
I see that we have a case where batched updates don't actually trigger a store change before subscriptions are updated. Specifically:
// 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
@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 mapState
would run before the parent re-rendered and potentially explode because the data it needed was no longer in the store. This was resolved in v5 by enforcing top-down subscription behavior. It does look like the test has been tweaked some over time as a couple other assumptions have changed. To be honest, though, I'm not entirely sure what the test is actually proving in relation to batching, especially since it's only dispatching once inside the unstable_batchedUpdates()
call. @gaearon , @epeli , can either of you comment on that?
Per your questions at the end, here's the expected sequence when an action is dispatched as of v5:
connect
callback checks to see if prevStoreState === currentStoreState
. If they're the same, it assumes that the action did not result in store state updates, and bails out.connect
then calls your mapState
function, and does a shallow equality check on the returned object to determine if a re-render is needed.Sequence in this test:
mapState
runs, parent re-rendersmapState
runs, return value is an empty object, no need to re-renderSo sure, I'd expect the child's mapState
to have executed once for that one action, but I'm not sure what that proves.
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 filter
So that is not an issue.
Hmm. Interesting.
@gaearon , @bvaughn , @acdlite : can any of you provide some clarification around use of component instance variables and async safety?
FYI, I don't think instance properties are async-unsafe.
Instance properties can be tricky to get right.
Function bindings are okay. (In fact they're often necessary!) But be wary of this.props
or this.state
references inside of your bound functions, because they might hold stale values (if the component has been rendered but not yet committed, or if an error occurred while rendering). If you need to access something in e.g. component state, use a setState
updater function and read from the parameter passed to it as mentioned in the docs.
Other instance properties can be okay as well, although I would suggest avoiding them when possible. If you look at our create-subscription
package, for example, you'll see that we use a couple of instance variables in it. The key here is to only update these values from a commit phase lifecycle (componentDidMount
or componentDidUpdate
) to ensure they are correct.
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.
connect
has always implemented shouldComponentUpdate
. Unfortunately, it's not just a question of whether the incoming "own props" from a parent have changed - there's the results of mapState
and mapDispatch
, which could be dependent on ownProps
, and the user could have customized how those results should be merged together as well.
In connect
v4, we set an instance flag in cWRP
to indicate that props had changed, and the Redux store subscription callback set a similar flag and basically called this.setState({storeState})
. From there, sCU
was just:
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: https://github.com/reduxjs/react-redux/blob/3e53ff96ed10f71c21346f08823e503df724db35/src/connect/selectorFactory.js#L74-L84
We then re-run this giant selector in both the Redux store callback and, up until now, in cWRP
. Those both update the selector, and it spits out a boolean that gets used in shouldComponentUpdate
, as well as the resulting new combined props for the wrapped component.
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 ownProps
, and that would cause incorrect determinations of whether to re-render.
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
@cellog
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).
@markerikson
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 ownProps, and that would cause incorrect determinations of whether to re-render.
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 this.selector
) state to be compared twice. Because of the memoization, it's comparing the same result to itself and saying there has been no change.
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
instance variables not being updated properly due to the render phase re-running multiple times with alternate values
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 (componentDidMount
or componentDidUpdate
) to ensure they are correct.
So long as you're following this rule, re-running render multiple times should not cause any improper updates.
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.
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 pure
option does currently, which is enabled by default. We act essentially as a PureComponent already.
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:
<ConnectAdvanced props={...} store=store>
<WrappedComponent props={derivedProps} />
</ConnectAdvanced>
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:
<GenerateDerivedProps props={...} store=store>
<DecideWhetherToRender props={derivedProps}>
<WrappedComponent props={derivedProps} />
</DecideWhetherToRender>
</GenerateDerivedProps>
This way, we can use gDSFP and handle subscriptions in cDM and cDU in the GenerateDerivedProps
component, which will always re-generate and pass to the DecideWhetherToRender
component. This component can memoize the derivedProps, and use that in sCU to decide whether to update. Also, the cDU can contain notification of child subscriptions unconditionally, or we can do that directly in sCU if we would return false (react-redux 5.0 behavior that is missing in 5.1 test)
And, to continue, if pure
option is false, then we simply don't use sCU in DecideWhetherToRender
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
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
Right. This is why I mentioned that you might want a cache size of > 1 so that your cache doesn't get busted in async high/low priority situations. (Don't go with an unbounded cache though, or you'll leak memory.)
and also effectively update an "instance variable" in the render phase.
Still not sure I understand this conclusion though. 😄 You'd be calculating a different value during render, which is fine, but you would not actually be updating an instance variable. (Unless your "handleNew*" methods write to the instance, which would be a problem.)
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:
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 <StrictMode>
warnings, but we're also doing stuff that <StrictMode>
presumably isn't catching. Can we get away with 5.x not being totally "safe", if we're going to try to have a 6.0 release that hopefully is different enough to fix all the concerns?
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 shouldComponentUpdate
flags.
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 create-subscription
package, I've felt that it looks very relevant to what we're doing over here with React-Redux. (In fact, I specifically tried to ask Brian about this in https://github.com/reduxjs/react-redux/issues/890#issuecomment-370666906 .) Ironically, the README says "Redux/Flux stores should use the context API instead.". Sure, agreed, but we haven't gotten that far yet :)
create-subscription
lays out a pattern of best practices for working with a non-React subscription. We really ought to use that as our model, and make connect
conform to the approach shown there.
Meanwhile, we've established that updating instance variables in the render phase is a no-no. We also know that gDSFP
will run both when the parent re-renders and when setState()
has actually executed a state update. However, note that it does not run if you use the functional form of setState()
and return null
.
Also important: if you pass a completion callback to setState()
, it does execute regardless of whether there was an actual state update applied or not, while componentDidUpdate()
only runs if there was a state update and ensuing re-render.
I threw together a quick example over at https://codesandbox.io/s/yjx19v21nz that logs out some of the differences in execution between functional setState
that applies an update, vs returning null
for no update.
Looking in create-subscription
, I see that the subscription callback it generates immediately calls setState()
with an updater function, and does the "has anything changed?" check inside:
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 5.1.0-test.1
release does use functional setState
inside the Redux subscription callback, but note that it always returns a new state value:
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 gDSFP
.
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 gDSFP
and a setState()
updater, and do checks to completely bail out of updates in both cases if necessary.
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:
state
(the Redux store state)ownProps
stateProps
(mapState
result)dispatchProps
(mapDispatch
result)mergedProps
(mergeProps
result)Well, the sample I just wrote up would keep ownProps
, reduxStoreState
, and childProps
(same as mergedProps
) in the connect
component state. Why not just keep stateProps
and dispatchProps
there too? We can rewrite the selector to return all of the above as in a wrapper result object, put all of that into component state, and pass all of them in as arguments to the selector instead of having it memoize on them internally. Then, we don't need to have the selector tell us if the component needs to update or not - we just run standard shallow equality checks in shouldComponentUpdate
(and maybe just on state, not on props).
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 <Provider>
subscribe and pass down the store state via context, because the store state would only be accessible inside the <ReactReduxContext.Consumer>
. That might necessitate the double-wrapper component approach after all for v6. (Frankly, one of the reasons I dislike the double-wrapper idea is simply because it's that many more "irrelevant" tree nodes in the React DevTools, and I'd be hesitant to go that route until there's a good way to mark components as "automatically hide this in the DevTools" or something similar.)
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 setState
callback runs even if there was no state update applied (ie, the updater function returned null
). Also, I see that the callback runs after cDU
. So, that looks like it should work either way, and we don't need to do the "temp-assign cDUNotifyNestedSubs
" bit any more.
Yes, you're right about needing to update the cached copy of the Redux store state. Really, the sCU
implementation might just be return nextState.childProps !== this.state.childProps
.
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 5.1.0-test.1
tag as a starting point - I think it's actually not too far off where this needs to go.
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 connectAdvanced()
function, where all the component behavior is implemented, and the higher-level connect()
function, which has our standard default memoized selector implementation. Overall, connectAdvanced()
expects to get a selectorFactory
argument, which returns a function that looks like (reduxStoreState, ownProps) => mergedChildProps
.
My comments above conflated the two parts of the implementation. I was thinking of changing connect
's selector factory to return an object containing {stateProps, dispatchProps, mergedProps}
, rather than directly returning mergedProps
, but that would break if anyone actually calls connectAdvanced()
with their own selector factory.
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 connect
's default behavior inside of connectAdvanced
. I find that conceptually annoying, but it might be necessary.
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 connect.spec
if I change out the expect
inside @connect
for the C
component to instead log the calls to mapState, I see 3 calls. The first is the initial, the second is the state update from child notification, and the third is when parent props are propagated down. Also worth noting is that setState callbacks don't trigger until the entire tree has been reconciled, so we may see a big performance hit as the whole tree renders with props changes, then the children get the new redux state, then the tree reconciles, then the next children get their redux state...
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 getDerivedStateFromProps
which is actually impossible with just 1 component.
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, I am facing similar issue. I have a connected component that in some circumstances can trigger 2 updates to the store. The latter update might not actually change anything. As a result, the change in first update was not re-rendered.
props change #1 -> runUpdater -> setState(props, shouldComponentUpdate: true) -> props change #2 -> runUpdater -> setState(props, shouldComponentUpdate: false) -> React dropped updates from re-rendering queue
May that be a React issue instead?
Thanks
@nguyenanduong : which version of React-Redux are you using?
Also, we've already established that 5.1.0-test.1
is completely broken, and we're not going to be publishing a 5.1
release anyway, so I'm going to close this issue.
Yes, I am on 5.1.0-test.1 When is the 5.1 release is out? Which branch can I get it? Thanks
Please DO NOT use this version. It is entirely broken. Revert to version 5.0.7.
As I said, we will not be releasing a 5.1 version. We are instead working on version 6.0, which is a major internal rewrite. See #950, #995, and #1000 for details.
Thanks, downgrading to 5.0.7 seems to work for me
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