-
-
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
fix CM tearing #1455
fix CM tearing #1455
Conversation
Deploy preview for react-redux-docs ready! Built with commit 16a6253 |
this should fix this demo |
This is probably a similar approach to my old implementation of reactive-react-redux. It is important to keep consistency during intermediate updates, so you want to keep the test unchanged. It would actually be interesting to develop user-subscription based react-redux, |
finally it fix even useTransition and isPending https://codesandbox.io/s/test-redux-in-concurrent-mode-with-react-redux-pytbh |
the only test that i fix, is the one where the parent force the same value to the child, that need 2 render to CORRECTLY process all store updates. reading store on render is an hack to have 1 render less on that "rarely usecase" but that break the normal queque from Redux to useState. |
See #1351 (comment) |
@@ -31,7 +31,7 @@ function useSelectorWithStoreAndSubscription( | |||
selector !== latestSelector.current || | |||
latestSubscriptionCallbackError.current |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be CM friendly, could we do something like:
try {
...
} catch(e) {
if (typeof e === 'object' && e !== null && typeof e.then === 'function') throw e
let errorMessage = ...
So we could do this, for example:
const status = useSelector(s => {
if (s.status === STATUS.PENDING) throw promise;
return s.status;
})
}
In React docs, it's using something like:
const value = resource.read()
where read()
either returns the value or throws so it can bail out before the selector is set. Not sure if here is the best place to do it, but yeah, with CM not everything "thrown" is an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry i think that thow promise to comunicate with Suspense does NOT matter with useSelector, that has to select a state without tearing on render. that's another problem.
here is the fix 😛😛😛 https://codesandbox.io/s/test-redux-in-concurrent-mode-with-react-redux-salvoravidareact-redux-9jo9z but please note that is a bug of the current "experimental react" with "SimpleMemoComponent" react-dom-internal function updateMemoComponent(current$$1, workInProgress, Component, nextProps, updateExpirationTime, renderExpirationTime) {
if (current$$1 === null) {
var type = Component.type;
if (isSimpleFunctionComponent(type) && Component.compare === null && // SimpleMemoComponent codepath doesn't resolve outer props either.
Component.defaultProps === undefined) {
var resolvedType = type;
{
resolvedType = resolveFunctionForHotReloading(type);
} // If this is a plain function component without default props,
// and with only the default shallow comparison, we upgrade it
// to a SimpleMemoComponent to allow fast path updates.
workInProgress.tag = SimpleMemoComponent;
workInProgress.type = resolvedType;
{
validateFunctionComponentInDev(workInProgress, type);
}
return updateSimpleMemoComponent(current$$1, workInProgress, resolvedType, nextProps, updateExpirationTime, renderExpirationTime);
} i will open a issue on React. So this "issue" is not related to useSelector! |
I'm not actually sure what this PR is trying to accomplish. Can you clarify? Also, whatever it's doing only seems to involve |
|
the problem was reading store on render phase. |
The point of Now, the exact mechanics of that may be up for discussion. For any potential change, I would specifically like to see some unit tests that fail under the existing implementation, and pass under a new implementation. I'd also like to know how this affects our performance benchmarks. I'm not sure if we ever actually added the |
yes i know. the only change is that reduxStore.getState() must be done ONLY on useEffect and non on render otherwise there is the tearing effect when react render 50 items in a random way. anyway, ok i will update with 2 test that simulates the 2 demos |
Sure. The other half of this is that we need an equivalent implementation for |
@markerikson I did. Please check out the PR and I can adjust if you have suggestions. |
@salvoravida In my experience, if you read store in render, you need to deal with the case that the store state is change from the time when checkForUpdates is called. That's what use-subscription is dealing with. |
done, fixed connect too. Meanwhile if someone would like to try, here is a temp build : @salvoravida/react-redux 7.1.6 🎉 |
if (count !== parentCount) { | ||
sawInconsistentState = true | ||
} | ||
lastRenderInconsistentState = count !== parentCount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this change. The whole purpose of this test was to ensure the selector never sees inconsistent state, not just on the latest render.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we NO more read the last state in the render phase, the "parent-child-issue" is resolved with 2 render, as every change of the store make 2 event-callback one on the parent and one on the child.
Before the issue was resolved with the "hack" of reading always the fresh Store state, even if we are on the "preview" scheduled refresh came out from "checkForUpdate" callback.
That hack while resolve the "parent-issue" with 1 render less, broke the changes normal queue on the the componente, that produce tearing on CM where every component could rendered in a random way, jumping the "changhes queue"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please try the demo of @dai-shi with my version and with old one. you will see the differences on a fast store changes, with slow render.
expect(childMapStateInvokes).toBe(2) | ||
expect(childCalls).toEqual([['a', 'a'], ['ac', 'ac']]) | ||
expect(childMapStateInvokes).toBe(3) | ||
expect(childCalls).toEqual([['a', 'a'], ['a', 'ac'], ['ac', 'ac']]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this changes the whole point of the test. this test ensures that mapState
never sees inconsistent state. with this change you are asserting the exact opposite
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we NO more read the last state in the render phase, the "parent-child-issue" is resolved with 2 render, as every change of the store make 2 event-callback one on the parent and one on the child.
Before the issue was resolved with the "hack" of reading always the fresh Store state, even if we are on the "preview" scheduled refresh came out from "checkForUpdate" callback.
That hack while resolve the "parent-issue" with 1 render less, broke the changes normal queue on the the componente, that produce tearing on CM where every component could rendered in a random way, jumping the "changhes queue"
please try the demo of @dai-shi with my version and with old one. you will see the differences on a fast store changes, with slow render.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you are developing your own fork, it's fine and I think it's one possible approach.
But, if you would like to support all existing RR apps (which is necessary for the official RR), you want to keep the backward compatibility. As I said, it should be technically possible. Seems like you are used to the code base, I think you can do it. I'm not 100% sure though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dai-shi i'm not dev my own fork, otherwise i havent opened a pr. that said i think that was a mistake reading fresh store state on render, to have 1 less render on that particular use case. Moreover it is of course back compatible with 1 render more (for that usecase)
Anyway i will try to explain better with a flow chart as soon as i can. That's all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Exactly point is: when a componet render it MUST read the storeState that was from HIS last storeChangeCallback!. Otherwise reading in render store.getState() could be newer than HIS callback queque. THAT'S THE POINT. Think more about that. test the demo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you clarify what you mean by "more renders"? It sounds like you're saying we will cause additional render cycles to occur, which is going to be bad for performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@markerikson I believe this is not due to an additional render. let me try to outline the order of events here:
- the store updates
- the parent sees the change first (due to tiered subscription) and renders
- since the parent rendered and changed the props, the child renders as well. with this change, during the render the child will see the last state it knows about, which is the previous store state (since due to tiered subscription the child didn't see the update yet). funnily enough this is actually the inverse of the stale props issue, since now we actually see newer parent props with an old state instead of old props with a new state
- once the render of the parent finishes the child finally sees the state update and re-renders itself with consistent props and state
it feels to me that tiered subscription and CM might just be incompatible
@salvoravida as @dai-shi pointed out, this discussion is not about the correctness of your change, but about preserving the existing behavior for backwards compatibility. there are likely tons of apps out there that have mapStateToProps
written in a way that depends on the props and state being consistent. that's why it is important we think of a way to keep the existing behavior. in the useSelector
hook we took the approach of simply ignoring these situations and dealing with errors by forcing a re-render. that was a deliberate decision we could make because it was a new API. connect
is sadly different
this pr does not read store on render, but on callback handler. |
Sorry, that's my misunderstanding from your original comment. I will try to understand again how it's working. Have you come up with any idea to keep the test unchanged? |
Hi folks, i have added dai-shi CM tests on /test/cm !
|
Please calm down. So, there are two issues in my mind. One is the changed test case, and the other is the useRef. I'm not sure but they might be from the single issue. Based on your idea, I would think something like this might work. const useSelector = (selector) => {
const [selected, setSelected] = useState(selector(store.getState()));
if (selected !== selector(store.getState())) {
setSelected(selector(store.getState()));
}
...
}; Hmm, I'm not sure if this solves the first issue... You want to try this? It seems like you are so exited (which is good), and I'm not sure I can convince you. |
There are so many points that we should discuss and find all an agreement, step by step, without mixing different points. i will try to start with these:
this fix also the others @dai-shi CM tests. This is also why i do NOT think that your last example of useSelector can work in CM! |
I understand we are not on the same page.
I disagree if I stand for [First Goal]. For [Future Goal], yes. |
you cannot have the [FG] CM safe (no tearing) while you want to read on render phase! the test that i have changed is safe, was an hack before to force read store on render phase. |
Both are [FG]... I assume [First Goal]. I think we can based on my snippet (which is from use-subscription which is basically for external store for CM safe). |
I don't think it's trying to do that. There will always be state branching as long as it uses If you think in I'm trying to describe this in the other thread. Also, it'd be backwards compatible of course, because existing RR apps just don't use |
@eddyw Thanks for the clarification. I agree fixing tearing is nothing to do with branching. |
fixed new @dai-shi CM tests. now reduxState is 100% "streamed" from Redux to Components, without reading from the future or jumping queue callbacks! CM should be fine. |
|
||
if (equalityFn(newSelectedState, latestSelectedState.current)) { | ||
setReduxState(prev => { | ||
prev.value = newReduxState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@salvoravida As I said in other thread, I'm not confident this works for the future. Do you ever feel like just trying my previous snippet???
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what i have understood, useRef is not CM safe. its a shared mem between branches. we need an useStateRef, a way to save data to current branch states queue.
I think it is safe, just says React to not re-render, but store a value on that branch.
i dont' think that reading value from the future is ok in CM and event streaming, but i will try your idea asap.
Have you tried it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is mutating the existing state value, though, which is definitely not CM-safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, because it is executed only on next update, before next render.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on every branch, so every branch has it's own reduxState queue changes. this is why there is NO more tearing, even if with last test, that simulate this edge case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm, so it's more or less like...:
const [refState, forceUpdateRef] = React.useState({ current: null })
React.useImperativeHandle(refState, () => 'some value', [ ...optional_deps ])
Isn't this just like a short cut?, I mean, if keeping state in ref and just using forceUpdate
. If it's within a transition, forceUpdate
will be scheduled for later but the ref will still change which I think this does. Using useState
for refs is just like merging a ref with forceUpdate into one line of code:
const ref = useRef()
const forceUpdate = useReducer(c => c + 1, 0)[1]
somewhere {
ref.current = someValue
forceUpdate()
}
...
const [ref, forceUpdate] = useState({ current: null })
somewhere {
ref.current = someValue
forceUpdate({ ...ref })
}
Both forceUpdate
will schedule "for later" if within transition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but using ref, another rendering brahch could update the same value before or after, like shared mem between 3d. Instead states ref are duplicated on every branch and every state ref updater will work on its own branch rendering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idk, this is how I see it:
queue no transition => [
setReduxState(p => { p.value = reduxState 1; return p }),
setReduxState(p => { p.value = reduxState 2; return p }), // before p.value is reduxState 1
setReduxState(p => { p.value = reduxState 3; return p }), // before p.value is reduxState 2
]
start transition queue => [
later setReduxState(p => { p.value = reduxState 4; return p }), // previous 'p' is unknown
]
queue outside transition => [
setReduxState(p => { p.value = reduxState 5; return p }), // before p.value is reduxState 3
]
stop transition queue => [
run previous:
initial state: reduxState 5 <<<< this is previous 'p' now
setReduxState(p => { p.value = reduxState 4; return p }), // <<< inconsistent state
]
Now, current @dai-shi tests using transition are not exactly right. It's testing dispatching in transition.. but, since there is no Suspense, no component is waiting for anything, so there is no need to queue for later.
Sometimes there may be some flickering on screen, when you use transition but nothing causes interrupting of rendering (like throw promise on Suspense), I've seen on some demos I was testing with, I think this is a bug related to what Dan mentioned on twitter, maybe? Point is, you can't test using transition without causing interrupt on render (throw on Suspense), otherwise it is pointless to wrap it in startTransition
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not following the discussion in deep, but:
without causing interrupt on render (throw on Suspense)
My understanding is a bit different. We can cause interrupt with heavy computation without throwing promise. That's the whole point of my tests.
P.S. Did I ever test using transition? Just today, I made new checks using useTransition
based on @eddyw 's demo. No Suspense. No throwing promises. Is it pointless?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never mind, ignore the above 🙈
I've copied the tests over this PR locally which did have useTransition
but you finished committing later, so I had them broken
This PR is stale, and it's unlikely we're going to make any CM-specific changes to v7 at this point. Instead, we're more likely to rewrite things into a v8 release that builds on top of |
fix #1351