Skip to content

Conversation

@eps1lon
Copy link
Member

@eps1lon eps1lon commented Nov 4, 2025

Turning posts into docs

I.e. how React leverages promise.status. Targeted at Suspense-enabled libraries.

Preview

@eps1lon eps1lon force-pushed the sebbie/11-04-docs_for_promise_subclasses branch from e29f977 to 40ace05 Compare November 4, 2025 14:08
@github-actions
Copy link

github-actions bot commented Nov 4, 2025

Size changes

📦 Next.js Bundle Analysis for react-dev

This analysis was generated by the Next.js Bundle Analysis action. 🤖

This PR introduced no changes to the JavaScript bundle! 🙌

### Avoiding fallbacks by passing Promise subclasses {/*avoiding-fallbacks-by-passing-promise-subclasses*/}
React will read the `status` field on Promise subclasses to synchronously read the value without waiting 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)).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some code here for a high-level view of what this looks like?

const dataPromise = {
  then: () => {},
  status: 'fulfilled',
  value: `some data'
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to show how to create a custom thenable.

Copy link
Contributor

@sebmarkbage sebmarkbage Nov 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea while you can use a custom Thenable, it's probably recommended to just use a native Promise for this.

You can make it a sub-class like such:

class FulfilledPromise extends Promise {
  status = 'fulfilled';
  value = null;
  constructor(data) {
     super(resolve => resolve(data));
     this.value = data;
  }
}
const promise = new FulfilledPromise(data);

This makes it more palatable for sticklers that thinks adding expandos to native objects is bad. It shows that the pattern is encouraged by the platform. If it wasn't, then why can I sub-class natives? If can't add fields, what am I supposed to do with those sub-classes?

But in practice when you're not adding any methods to the prototype this is really just the same thing as:

const promise = Promise.resolve(data);
promise.status = 'fulfilled';
promise.value = data;

Which is smaller code and faster, so why not do that? It also doesn't risk getting compiled which would break native subclassing.

So maybe document one stickler version and one faster version (and hint that thenables might be even faster).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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.
<Sandpack>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do something like this instead:

Screenshot 2025-11-04 at 9 31 49 AM

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 subclass

function getUser(id) {
  return {
    status: 'fulfilled',
    value: `User #${id}`,
    then: () => {}
  }
}


function UserDetails() {
  const user = use(getUser());
  return <p>Hello, {user}!</p>;
}

Without a Promise subclass

function getUser(id) {
  return Promise.resolve(`User #${id}`);
}


function UserDetails() {
  const user = use(getUser());
  return <p>Hello, {user}!</p>;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 439 to 443
### Avoiding fallbacks by passing Promise subclasses {/*avoiding-fallbacks-by-passing-promise-subclasses*/}
React will read the `status` field on Promise subclasses to synchronously read the value without waiting 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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is great content and very helpful!

It's a pretty advanced topic though so maybe point out the intended audience early? Maybe lead with:

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.

I think "subclass" and "microtask" is likely language that will trip people up, but if it's clear this is an advanced topic I think that's fine.

Maybe be a bit more explicit about the rejected/reason case as well?

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.

I think this should be its own paragraph as it's a separate point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants