-
Notifications
You must be signed in to change notification settings - Fork 47.7k
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
useMutableSource: bugfix for new getSnapshot with mutation #18297
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9bee11d
useMutableSource: bugfix for new getSnapshot with mutation
4d25d4e
Split mutable source pending expiration times into first and last
1a73af5
Fixed remaining tearing cases
db511f4
Swapped first/last logic in setPendingExpirationTime and removed no l…
86da5ce
Recreate queue and dispatch when source/subscribe/getSnapshot change
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -999,56 +999,44 @@ function useMutableSource<Source, Snapshot>( | |
subscribe, | ||
}: MutableSourceMemoizedState<Source, Snapshot>); | ||
|
||
// Sync the values needed by our subscribe function after each commit. | ||
// Sync the values needed by our subscription handler after each commit. | ||
dispatcher.useEffect(() => { | ||
const didGetSnapshotChange = !is(refs.getSnapshot, getSnapshot); | ||
refs.getSnapshot = getSnapshot; | ||
|
||
// Normally the dispatch function for a state hook never changes, | ||
// but in the case of this hook, it will change if getSnapshot changes. | ||
// In that case, the subscription below will have cloesd over the previous function, | ||
// so we use a ref to ensure that handleChange() always has the latest version. | ||
// but this hook recreates the queue in certain cases to avoid updates from stale sources. | ||
// handleChange() below needs to reference the dispatch function without re-subscribing, | ||
// so we use a ref to ensure that it always has the latest version. | ||
refs.setSnapshot = setSnapshot; | ||
|
||
// This effect runs on mount, even though getSnapshot hasn't changed. | ||
// In that case we can avoid the additional checks for a changed snapshot, | ||
// because the subscription effect below will cover this. | ||
if (didGetSnapshotChange) { | ||
// Because getSnapshot is shared with subscriptions via a ref, | ||
// we don't resubscribe when getSnapshot changes. | ||
// This means that we also don't check for any missed mutations | ||
// between the render and the passive commit though. | ||
// So we need to check here, just like when we newly subscribe. | ||
const maybeNewVersion = getVersion(source._source); | ||
if (!is(version, maybeNewVersion)) { | ||
const maybeNewSnapshot = getSnapshot(source._source); | ||
if (!is(snapshot, maybeNewSnapshot)) { | ||
setSnapshot(maybeNewSnapshot); | ||
|
||
const currentTime = requestCurrentTimeForUpdate(); | ||
const suspenseConfig = requestCurrentSuspenseConfig(); | ||
const expirationTime = computeExpirationForFiber( | ||
currentTime, | ||
fiber, | ||
suspenseConfig, | ||
); | ||
setPendingExpirationTime(root, expirationTime); | ||
|
||
// If the source mutated between render and now, | ||
// there may be state updates already scheduled from the old getSnapshot. | ||
// Those updates should not commit without this value. | ||
// There is no mechanism currently to associate these updates though, | ||
// so for now we fall back to synchronously flushing all pending updates. | ||
// TODO: Improve this later. | ||
markRootExpiredAtTime(root, getLastPendingExpirationTime(root)); | ||
} | ||
// Check for a possible change between when we last rendered now. | ||
const maybeNewVersion = getVersion(source._source); | ||
if (!is(version, maybeNewVersion)) { | ||
const maybeNewSnapshot = getSnapshot(source._source); | ||
if (!is(snapshot, maybeNewSnapshot)) { | ||
setSnapshot(maybeNewSnapshot); | ||
|
||
const currentTime = requestCurrentTimeForUpdate(); | ||
const suspenseConfig = requestCurrentSuspenseConfig(); | ||
const expirationTime = computeExpirationForFiber( | ||
currentTime, | ||
fiber, | ||
suspenseConfig, | ||
); | ||
setPendingExpirationTime(root, expirationTime); | ||
|
||
// If the source mutated between render and now, | ||
// there may be state updates already scheduled from the old getSnapshot. | ||
// Those updates should not commit without this value. | ||
// There is no mechanism currently to associate these updates though, | ||
// so for now we fall back to synchronously flushing all pending updates. | ||
// TODO: Improve this later. | ||
markRootExpiredAtTime(root, getLastPendingExpirationTime(root)); | ||
} | ||
} | ||
}, [getSnapshot]); | ||
}, [getSnapshot, source, subscribe]); | ||
|
||
// If we got a new source or subscribe function, | ||
// we'll need to subscribe in a passive effect, | ||
// and also check for any changes that fire between render and subscribe. | ||
// If we got a new source or subscribe function, re-subscribe in a passive effect. | ||
dispatcher.useEffect(() => { | ||
const handleChange = () => { | ||
const latestGetSnapshot = refs.getSnapshot; | ||
|
@@ -1089,29 +1077,6 @@ function useMutableSource<Source, Snapshot>( | |
} | ||
} | ||
|
||
// Check for a possible change between when we last rendered and when we just subscribed. | ||
const maybeNewVersion = getVersion(source._source); | ||
if (!is(version, maybeNewVersion)) { | ||
const maybeNewSnapshot = getSnapshot(source._source); | ||
if (!is(snapshot, maybeNewSnapshot)) { | ||
setSnapshot(maybeNewSnapshot); | ||
|
||
const currentTime = requestCurrentTimeForUpdate(); | ||
const suspenseConfig = requestCurrentSuspenseConfig(); | ||
const expirationTime = computeExpirationForFiber( | ||
currentTime, | ||
fiber, | ||
suspenseConfig, | ||
); | ||
setPendingExpirationTime(root, expirationTime); | ||
|
||
// We missed a mutation before committing. | ||
// It's possible that other components using this source also have pending updates scheduled. | ||
// In that case, we should ensure they all commit together. | ||
markRootExpiredAtTime(root, getLastPendingExpirationTime(root)); | ||
} | ||
} | ||
|
||
return unsubscribe; | ||
}, [source, subscribe]); | ||
|
||
|
@@ -1126,31 +1091,27 @@ function useMutableSource<Source, Snapshot>( | |
// | ||
// In both cases, we need to throw away pending udpates (since they are no longer relevant) | ||
// and treat reading from the source as we do in the mount case. | ||
const didGetSnapshotChange = !is(prevGetSnapshot, getSnapshot); | ||
if ( | ||
!is(prevGetSnapshot, getSnapshot) || | ||
!is(prevSource, source) || | ||
!is(prevSubscribe, subscribe) || | ||
didGetSnapshotChange | ||
!is(prevSubscribe, subscribe) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is neat that this is now the same condition that the effect hook uses when comparing deps. In the future we could "cheat" and read the deps off the effect hook instead of using a ref. |
||
) { | ||
if (didGetSnapshotChange) { | ||
// Create a new queue and setState method, | ||
// So if there are interleaved updates, they get pushed to the older queue. | ||
// When this becomes current, the previous queue and dispatch method will be discarded, | ||
// including any interleaving updates that occur. | ||
const newQueue = { | ||
pending: null, | ||
dispatch: null, | ||
lastRenderedReducer: basicStateReducer, | ||
lastRenderedState: snapshot, | ||
}; | ||
newQueue.dispatch = setSnapshot = (dispatchAction.bind( | ||
null, | ||
currentlyRenderingFiber, | ||
newQueue, | ||
): any); | ||
stateHook.queue = newQueue; | ||
} | ||
|
||
// Create a new queue and setState method, | ||
// So if there are interleaved updates, they get pushed to the older queue. | ||
// When this becomes current, the previous queue and dispatch method will be discarded, | ||
// including any interleaving updates that occur. | ||
const newQueue = { | ||
pending: null, | ||
dispatch: null, | ||
lastRenderedReducer: basicStateReducer, | ||
lastRenderedState: snapshot, | ||
}; | ||
newQueue.dispatch = setSnapshot = (dispatchAction.bind( | ||
null, | ||
currentlyRenderingFiber, | ||
newQueue, | ||
): any); | ||
stateHook.queue = newQueue; | ||
stateHook.baseQueue = null; | ||
snapshot = readFromUnsubcribedMutableSource(root, source, getSnapshot); | ||
stateHook.memoizedState = stateHook.baseState = snapshot; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 if you go with this approach then you don't have to track
getSnapshot
orsetSnapshot
at all. Can use the closed over values, sincegetSnapshot
is now a dep, andsetSnapshot
only changes when one of the other deps does.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.
Edit: Got confused, thought you had removed the
getSnapshot
optimization. Never mind.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 worries. This is something we can further optimize when we replace the two composed effects with our own pushed effect. Then we can just use one single effect and selectively unsubscribe/subscribe only when necessary.
Sounds like that will be a fun little cleanup refactor anyway. 😄