-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Docs for Promise subclasses #8125
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -436,6 +436,277 @@ To use the Promise's <CodeStep step={1}>`catch`</CodeStep> method, call <CodeSte | |
|
|
||
| --- | ||
|
|
||
| ### Avoiding fallbacks by passing Promise subclasses {/*avoiding-fallbacks-by-passing-promise-subclasses*/} | ||
|
|
||
| If you are implementing a Suspense-enabled library, you can help React avoid unnecessarily suspending when you know the Promise has already settled, by using `status` and `value` or `reason` fields. | ||
|
|
||
| React will read the `status` field on the Promise to synchronously read the value without having to wait for a microtask. If the Promise is already settled (resolved or rejected), React can read the value immediately without suspending and showing a fallback if the update was not part of a Transition (e.g. [`ReactDOM.flushSync()`](/reference/react-dom/flushSync)). | ||
|
|
||
| React will set the `status` field itself if the passed Promise does not have this field set. Suspense-enabled libraries should set the `status` field on the Promises they create to avoid unnecessary fallbacks. | ||
|
|
||
| Calling `use` conditionally depending on whether a Promise is settled or not is discouraged. `use` should be called unconditionally so that React DevTools can show that the Component may suspend on data. | ||
|
|
||
| <Recipes titleText="Difference between setting the status field in your library and not setting the status field" titleId="difference-between-setting-the-status-field-in-your-library-and-not-setting-the-status-field"> | ||
|
|
||
| #### Passing a basic Promise {/*passing-a-basic-promise*/} | ||
|
|
||
| <Sandpack> | ||
|
Member
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. Can we do something like this instead:
And structure it so that the most relevant code (the promise creation) is what's visible first? Like this you should see this: With a Promise subclassfunction getUser(id) {
return {
status: 'fulfilled',
value: `User #${id}`,
then: () => {}
}
}
function UserDetails() {
const user = use(getUser());
return <p>Hello, {user}!</p>;
}Without a Promise subclassfunction getUser(id) {
return Promise.resolve(`User #${id}`);
}
function UserDetails() {
const user = use(getUser());
return <p>Hello, {user}!</p>;
}
Member
Author
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. done 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. I like this! Many examples in the docs that compares approaches like this tend to point out what to look for, like "Note how loading the users shows a loading state", but I don't think that's necessary here. |
||
|
|
||
| ```js src/App.js active | ||
| import { Suspense, use, useState } from "react"; | ||
|
|
||
| function preloadUser(id) { | ||
| // This is not a real implementation of getting the | ||
| // Promise for the user. A real implementation would | ||
| // probably call `fetch` or another data fetching method. | ||
| // The actual implementation should cache the Promise. | ||
| const promise = Promise.resolve(`User #${id}`); | ||
|
|
||
| return promise; | ||
| } | ||
|
|
||
| function UserDetails({ userUsable }) { | ||
| const user = use(userUsable); | ||
| return <p>Hello, {user}!</p>; | ||
| } | ||
|
|
||
| export default function App() { | ||
| const [userId, setUserId] = useState(null); | ||
| // The initial | ||
| const [userUsable, setUser] = useState(null); | ||
|
Comment on lines
+475
to
+476
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. Not critical, but I have a few suggestions on this bit:
|
||
|
|
||
| return ( | ||
| <div> | ||
| <button | ||
| onClick={() => { | ||
| setUser(preloadUser(1)); | ||
| setUserId(1); | ||
| }} | ||
| > | ||
| Load User #1 | ||
| </button> | ||
| <button | ||
| onClick={() => { | ||
| setUser(preloadUser(2)); | ||
| setUserId(2); | ||
| }} | ||
| > | ||
| Load User #2 | ||
| </button> | ||
| <Suspense key={userId} fallback={<p>Loading</p>}> | ||
| {userUsable ? ( | ||
| <UserDetails userUsable={userUsable} /> | ||
| ) : ( | ||
| <p>No user selected</p> | ||
| )} | ||
| </Suspense> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| </Sandpack> | ||
|
|
||
| <Solution /> | ||
|
|
||
| #### Passing the Promise with a `status` field {/*passing-the-promise-with-the-status-field*/} | ||
|
|
||
|
|
||
| <Sandpack> | ||
|
|
||
| ```js src/App.js active | ||
| import { Suspense, use, useState } from "react"; | ||
| import { flushSync } from "react-dom"; | ||
|
|
||
| class PromiseWithStatus extends Promise { | ||
| status = "pending"; | ||
| value = null; | ||
| reason = null; | ||
|
|
||
| constructor(executor) { | ||
| let resolve; | ||
| let reject; | ||
| super((_resolve, _reject) => { | ||
| resolve = _resolve; | ||
| reject = _reject; | ||
| }); | ||
| // Setting the `status` field allows React to | ||
| // synchronously read the value if the Promise | ||
| // is already settled by the time the Promise is | ||
| // passed to `use`. | ||
| executor( | ||
| (value) => { | ||
| this.status = "fulfilled"; | ||
| this.value = value; | ||
| resolve(value); | ||
| }, | ||
| (reason) => { | ||
| this.status = "rejected"; | ||
| this.reason = reason; | ||
| reject(reason); | ||
| } | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function preloadUser(id) { | ||
| // This is not a real implementation of getting the | ||
| // Promise for the user. A real implementation would | ||
| // probably call `fetch` or another data fetching method. | ||
| // The actual implementation should cache the Promise. | ||
| // The important part is that we are using the | ||
| // PromiseWithStatus subclass here. Check out the next | ||
| // step if you're not controlling the Promise creation | ||
| // (e.g. when `fetch` is used). | ||
| const promise = PromiseWithStatus.resolve(`User #${id}`); | ||
|
|
||
| return promise; | ||
| } | ||
|
|
||
| function UserDetails({ userUsable }) { | ||
| const user = use(userUsable); | ||
| return <p>Hello, {user}!</p>; | ||
| } | ||
|
|
||
| export default function App() { | ||
| const [userId, setUserId] = useState(null); | ||
| // The initial | ||
| const [userUsable, setUser] = useState(null); | ||
|
|
||
| return ( | ||
| <div> | ||
| <button | ||
| onClick={() => { | ||
| // flushSync is only used for illustration | ||
| // purposes. A real app would probably use | ||
| // startTransition instead. | ||
| flushSync(() => { | ||
|
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. NIT: Is If it's not necessary, it might distract from the point more than help, at least for me it made me wonder if this was a necessary condition? I do think it's good to point out this does not happen with transitions though and not sure how to do that differently. 😄
Member
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. There's actually a much easier and common way for this to happen: export default function App() {
const [userId, setUserId] = useState(null);
const [userUsable, setUser] = useState(null);
const [isPending, startTransition] = useTransition();
return (
<div>
<button
onClick={() => {
// ⚠️ this startTransition causes a sync update
// to App to update the `isPending` value
startTransition(() => {
setUser(preloadUser(1));
setUserId(1);
});
}}
>
Load User #1
</button>
{/* ... */}
<span>{isPending && 'loading...'}</span>
<Suspense key={userId} fallback={<p>Loading</p>}>
{/*
⚠️ Without proper memoization, the sync update
flows into this boundary, causing it to suspend
if the data is not available syncronously
*/}
{userUsable ? (
<UserDetails userUsable={userUsable} />
) : (
<p>No user selected</p>
)}
</Suspense>
</div>
);
}https://codesandbox.io/p/sandbox/ymm9s8?file=%2Fsrc%2FApp.js 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. I realize now that you might both trying to show practical examples of how this situation might happen in real code? I was focused on just the practical aspects of what's necessary for it to trigger. I was focused on the fact that even the first example triggers the suspend with just: <button
onClick={() => {
setUser(preloadUser(2));
setUserId(2);
}}
>
Member
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. Yeah that's also true, good point, the flushSync isn't necessary in any of the examples to trigger this. However, not using a transition here would be an anti-pattern since it would force the fallback if the data wasn't synchronously available (i.e. preloading didn't finish), so I think using a transition is still good here. https://react.dev/reference/react/Suspense#preventing-already-revealed-content-from-hiding 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.
Oh yeah, totally onboard with that.
I don't have a strong opinion on that, in my mind it all depends on what the purpose of the examples are, which I'm not a good judge of.
Personally, I already know there are a lot of situations this might happen in, so I lean towards minimal, but the broader examples might be more helpful to others!
Member
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. Though in that case the Compiler also solves it :) 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. The simplest case I can think of thats also common is likely just loading the page with prefetched data? Not sure if there is special behavior or if it makes for a good example though. 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. If we are talking cases, wouldnt a transition where a new suspense boundary reveals with the use inside also have this problem? Fallback show if status is not set, never shows if it is?
Member
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. Correct. And actually I just noticed the key so this example is good as is (with or without the transition) so I think we should just leave the transition in?
Member
Author
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. I don't think it helps showing a more involved example. This is good enough for me so that we have something to link to. Feel free to merge or include an example that helps understand the status field. |
||
| setUser(preloadUser(1)); | ||
| setUserId(1); | ||
| }); | ||
| }} | ||
| > | ||
| Load User #1 | ||
| </button> | ||
| <button | ||
| onClick={() => { | ||
| setUser(preloadUser(2)); | ||
| setUserId(2); | ||
| }} | ||
| > | ||
| Load User #2 | ||
| </button> | ||
| <Suspense key={userId} fallback={<p>Loading</p>}> | ||
| {userUsable ? ( | ||
| <UserDetails userUsable={userUsable} /> | ||
| ) : ( | ||
| <p>No user selected</p> | ||
| )} | ||
| </Suspense> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| ``` | ||
|
|
||
| </Sandpack> | ||
|
|
||
| <Solution /> | ||
|
|
||
| #### Simplified implementation setting the `status` field {/*simplified-implementation-setting-the-status-field*/} | ||
|
|
||
| <Sandpack> | ||
|
|
||
| ```js src/App.js active | ||
| import { Suspense, use, useState } from "react"; | ||
| import { flushSync } from "react-dom"; | ||
|
|
||
| function preloadUser(id) { | ||
| const value = `User #${id}`; | ||
| // This is not a real implementation of getting the | ||
| // Promise for the user. A real implementation would | ||
| // probably call `fetch` or another data fetching method. | ||
| // The actual implementation should cache the Promise. | ||
| const promise = Promise.resolve(value); | ||
|
|
||
| // We don't need to create a custom subclass. | ||
| // We can just set the necessary fields directly on the | ||
| // Promise. | ||
| promise.status = "pending"; | ||
| promise.then( | ||
| (value) => { | ||
| promise.status = "fulfilled"; | ||
| promise.value = value; | ||
| }, | ||
| (error) => { | ||
| promise.status = "rejected"; | ||
| promise.reason = error; | ||
| } | ||
| ); | ||
|
|
||
| // Setting the status in `.then` is too late if we want | ||
| // to create an already settled Promise. We only included | ||
| // setting the fields in `.then` for illustration | ||
| // purposes. Since our demo wants an already resolved | ||
| // Promise, we set the necessary fields synchronously. | ||
| promise.status = "fulfilled"; | ||
| promise.value = value; | ||
| return promise; | ||
| } | ||
|
|
||
| function UserDetails({ userUsable }) { | ||
| const user = use(userUsable); | ||
| return <p>Hello, {user}!</p>; | ||
| } | ||
|
|
||
| export default function App() { | ||
| const [userId, setUserId] = useState(null); | ||
| // The initial | ||
| const [userUsable, setUser] = useState(null); | ||
|
|
||
| return ( | ||
| <div> | ||
| <button | ||
| onClick={() => { | ||
| // flushSync is only used for illustration | ||
| // purposes. A real app would probably use | ||
| // startTransition instead. | ||
| flushSync(() => { | ||
| setUser(preloadUser(1)); | ||
| setUserId(1); | ||
| }); | ||
| }} | ||
| > | ||
| Load User #1 | ||
| </button> | ||
| <button | ||
| onClick={() => { | ||
| flushSync(() => { | ||
| setUser(preloadUser(2)); | ||
| setUserId(2); | ||
| }); | ||
| }} | ||
| > | ||
| Load User #2 | ||
| </button> | ||
| <Suspense key={userId} fallback={<p>Loading</p>}> | ||
| {userUsable ? ( | ||
| <UserDetails userUsable={userUsable} /> | ||
| ) : ( | ||
| <p>No user selected</p> | ||
| )} | ||
| </Suspense> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| </Sandpack> | ||
|
|
||
| <Solution /> | ||
|
|
||
| </Recipes> | ||
|
|
||
| ## Troubleshooting {/*troubleshooting*/} | ||
|
|
||
| ### "Suspense Exception: This is not a real error!" {/*suspense-exception-error*/} | ||
|
|
||

Uh oh!
There was an error while loading. Please reload this page.
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 is good!
Maybe "... if the update was not part of a Transition (e.g.
ReactDOM.flushSync())" is too much detail here? At least for me it made the sentence slightly harder to parse and didn't seem that important in this specific context, but not a big deal.I wondered what it would look like if the docs tried to explain the basics more and noodled on a separate approach, I'm not at all sure this is better or captures everything correctly, so read it as an exploration:
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 like this, but I also like including examples of when this can happen. But I wouldn't use flush sync for that, I would use examples like "such as when the user clicks the back button, or when they continue interacting with the page while a transition is in progress"
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 like that. The preload case might be another one that's easy to understand and helps explain things.
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.
That's not trivial to repro though. We have to make a concession between showing the observed behavior and when this actually applies. The back button wouldn't work in a docs demo. I don't know what the "when they continue interacting with the page while a transition is in progress" case is. Would you mind following up with a better example and we merge this so that we can link to something in the meantime=
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 agree! How I read this was that it was not about the examples, but about the text. Reading what Ricky wrote again I'm not sure?
I agree there is a concession to be made and that the observed behavior is most important. It's more important to know this can happen (and you can look up how in other docs) as well as how to prevent it, rather than the exact situations it does.
That said, I don't think it hurts to add a sentence with an example of when a promise might already be resolved either to just give some feel for why it's important to handle this, maybe something like:
I don't think it's terribly important though.
(Sidenote: Explaining all these intricacies also has value, but this section is short on space to do that. Some of the more involved things might be better for a "guide"-format, like on Suspenseful library integration?)