Avoid creating new promises for deduped revalidation - #4282
Draft
devjiwonchoi wants to merge 3 commits into
Draft
devjiwonchoi wants to merge 3 commits into
devjiwonchoi wants to merge 3 commits into
Conversation
React Suspense retries track the thenable object passed to use(). An async revalidate function always creates a new outer promise, even when the underlying fetch request is deduped. Move the async work into an inner function and return that wrapper promise explicitly. This does not cache the promise yet, but it makes the returned promise available for a follow-up change to store and reuse.
React Suspense retries need to see the same thenable while a request is pending. SWR already dedupes the fetcher result, but revalidate returns a wrapper promise that resolves after the request updates cache state. Store that wrapper promise in the fetch cache for deduped revalidations and return it on later deduped calls. This lets retries reuse the exact revalidation promise without changing non-deduped revalidation or mutation invalidation semantics.
Suspense only needs to unwrap the revalidation promise when there is no returned data to show. When keepPreviousData supplies returnedData, starting the deduped revalidation without passing it to use() preserves the old data and removes the need to mark the wrapper promise fulfilled by hand. Add a regression test so key changes with keepPreviousData do not show the fallback during revalidation.
devjiwonchoi
force-pushed
the
jiwon/revalidate-promise-caching
branch
from
June 24, 2026 14:11
1168a32 to
80015f8
Compare
eps1lon
reviewed
Jun 25, 2026
Comment on lines
+834
to
+837
| if (isUndefined(returnedData)) { | ||
| // No current or previous data is available to render, so suspend on the request. | ||
| use(revalidation) | ||
| } |
Member
There was a problem hiding this comment.
isn't that still conditionally suspending on data usage? If we have data, we don't suspend. If we don't have data, we do suspend. I don't get the relationship between revalidation and returned data. I feel like we should just always use() the data and SWR should set the revalidation Promise into the data if we don't have any currently.
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Tip
Recommended to review commit by commit and hide whitespace.
Why?
SWR already dedupes the actual fetch, so repeated renders share one network request.
But
revalidatewas anasyncfunction, which meant every call returned a new promise. Withsuspense: true, React could see a different promise fromrevalidate()on each render, even though SWR was waiting for the same deduped request underneath.SWR worked around this by instrumenting the promise with
statusandvalue. This change instead uses a cached revalidation promise souse()does not see a new promise for the same request.How?
Store the current deduped revalidation promise in
FETCH[key]and reuse it while the same request is active. The existing fetch cleanup still clears the wholeFETCH[key]entry, including the cached promise.When previous data is available, start revalidation but skip
use(revalidation), so SWR keeps showing the previous data instead of suspending.