-
Notifications
You must be signed in to change notification settings - Fork 46.8k
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
Failing test case for useMutableSource #18296
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 371c14e:
|
Nice! Thanks for the test case. I'll fix this in the morning. |
Thanks again! |
9ec20dc
to
8bcc4bf
Compare
@bvaughn I pushed another test case. Mind taking a look at that, too? |
On it. |
Also I probably shouldn't have written the description as if this were specific to passive effects, since it applies to a mutation in an interleaved event during render, too. |
I think what the second test case indicates is that we aren't resetting the queue properly here: stateHook.baseQueue = null;
snapshot = readFromUnsubcribedMutableSource(root, source, getSnapshot);
stateHook.memoizedState = stateHook.baseState = snapshot; That clears the updates, but what we also need to do is create an entire new |
Yeah, I was noticing that. Thanks for the pointer 😄 |
More concretely: const newQueue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: whatever,
};
newQueue.dispatch = dispatchAction.bind(currentlyRenderingFiber, newQueue);
// Now when this becomes current, the previous queue and dispatch method
// are complete discarded, including any interleaving updates that occur.
hook.queue = newQueue; |
😆 Yes, thanks... |
From my investigation, the effect that updates the It doesn't fix this case though, and it also breaks several others 😅so I need to dig some more. |
It's interesting that removing the first hook "fixes" the problem above for the second hook. I commented out the first hook temporarily (and hard-coded the value of |
One noticeable difference is eager state computation. Not sure if it's relevant. It only happens for the first state hook on a given fiber that schedules an update. So if Update It seems the eager comparison prevents an update from being scheduled on the fiber when the change handler gets called for |
I think I can write a similarly broken test case with just the state hook, but maybe I'm misunderstanding something. it("repro", async () => {
let setStateA;
let setStateB;
function App({ count }) {
const [a, setA] = React.useState("initial-a");
const [b, setB] = React.useState("initial-b");
React.useEffect(() => {
if (count === 1) {
setA("cascading-a");
setB("cascading-b");
}
}, [count]);
setStateA = setA;
setStateB = setB;
return `${a}:${b}`;
}
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App count={0} />);
});
await act(async () => {
root.render(<App count={1} />);
// Render and finish the tree, but yield before passive effects fire.
expect(Scheduler).toFlushUntilNextPaint([]);
// This is at high priority so that it doesn't get batched with default
// priority updates that might fire during the passive effect
ReactNoop.discreteUpdates(() => {
setStateB("high-pri-b");
});
expect(Scheduler).toFlushUntilNextPaint([]);
expect(root.getChildrenAsJSX()).toEqual("cascading-a:high-pri-b");
});
}); Rather than a final state of |
The test you wrote is correct. I think the confusion is because there's another paint after that one, at normal priority, that does the cascading updates. Here's that same test with more assertions added: it('repro', async () => {
let setStateA;
let setStateB;
function App({count}) {
const [a, setA] = React.useState('initial-a');
const [b, setB] = React.useState('initial-b');
React.useEffect(() => {
if (count === 1) {
setA('cascading-a');
setB('cascading-b');
}
}, [count]);
setStateA = setA;
setStateB = setB;
return `${a}:${b}`;
}
const root = ReactNoop.createRoot();
await act(async () => {
root.render(<App count={0} />);
});
await act(async () => {
root.render(<App count={1} />);
// Render and finish the tree, but yield before passive effects fire.
expect(Scheduler).toFlushUntilNextPaint([]);
expect(root).toMatchRenderedOutput('initial-a:initial-b');
// This is at high priority so that it doesn't get batched with default
// priority updates that might fire during the passive effect
ReactNoop.discreteUpdates(() => {
setStateB('high-pri-b');
});
expect(Scheduler).toFlushUntilNextPaint([]);
// High pri updates have finished, but not the cascading ones,
// which have normal pri
expect(root).toMatchRenderedOutput('initial-a:high-pri-b');
});
// Now the final state has rendered, including the cascading updates
expect(root).toMatchRenderedOutput('cascading-a:cascading-b');
}); |
I see. Thanks for clarifying. I wonder why I'm seeing the update clobber in the way I'm seeing for uMS's internal state hook. |
Okay, looking at this again today. Disconnecting dispatch from the update queue works to prevent the old B selector from overriding the new A selector value when A is mutated- but the mutation to A is also missed temporarily (since the subscribe function is still using the previous The effect that updates I think the |
Scenario: `getSnapshot` changes during render. The render finishes and commits. But before the passive effects fire, the source is mutated again. The subscription still thinks the old `getSnapshot` is current.
Not sure the best fix for this one.
8bcc4bf
to
ffefb4e
Compare
Gah I did it again! |
I think that's the right idea, except it's not only limited to high priority updates. It could be a lower priority update, too. (I pushed another test case to cover that.) What's tricky is there could be multiple pending mutations at different priorities. None of them should be allowed to commit without also committing the new update. We've been using the word "entanglement" to describe this behavior. We don't have a mechanism to do this without de-opting, currently, but I'm planning to add one as part of the In the meantime, I think we should synchronously flush the pending mutation updates. Then we can do the entanglement thing once that exists.
import {markRootExpiredAtTime} from './ReactFiberRoot';
markRootExpiredAtTime(root.mutableSourcePendingUpdateTime); |
b9e7a73
to
371c14e
Compare
); | ||
}); | ||
expect(Scheduler).toHaveYielded([ | ||
'TODO: This currently tears. Fill in with correct values once bug', |
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 think this was meant to be a comment? 😄
Thanks for the added context, Andrew. Using I've pushed an update to #18297 though. Edit I wonder if it's worth considering pushing a layout effect when |
Scenario:
getSnapshot
changes during render. The render finishes and commits. But before the passive effects fire, the source is mutated again. The subscription still thinks the oldgetSnapshot
is current.I think this means that whenever
getSnapshot
changes, we need to check if there was a mutation since render. Similar to what we do when resubscribing.