-
Notifications
You must be signed in to change notification settings - Fork 18
This proposal has been opened as a public RFC. Please leave additional feedback in the main discussion: reactjs/rfcs#229
Previous iterations of the proposal are available in the edit history of this description.
All reactions
-
❤️ 31 -
👀 11
Replies: 4 comments 14 replies
I always love reading through these. Great high-level context and detailed-enough descriptions!
// This is a non-blocking fetch. We initiated the request but we haven't
// yet unwrapped the result.
const promise = fetchInfo();
What happens in the case the promise returned by fetchInfo rejects? At first glance it seems like we'll get an uncaught promise rejection which is going to be quite annoying especially in testing environments.
If fetchInfo is expected to handle its promise rejection by not throwing, wouldn't this disable error boundaries? Which means we're back to handling the rejected state at the component level and not by boundaries?
All reactions
If fetchInfo is expected to handle its promise rejection by not throwing...
Nah it's supposed to just reject, so that if you pass it to use it can be caught by error boundaries.
But yeah that's a really annoying error. One "solution" is to attach a listener that reports preloading errors, perhaps with a helper (logIfPreloadFails(promise)), which is maybe a good practice but is quite annoying to mandate for every use case.
Worth nothing this isn't React-specific, it affects regular async/await, too. It really should be addressed by the JavaScript runtimes.
Like IMO you should be able to write code that looks like this:
async function getAsyncData() { // Loads in parallel because nothing has been awaited yet const aPromise = getA(); const bPromise = getB(); return { a: await aPromise, b: await aPromise, }; }
but you can't because if both aPromise and bPromise rejects, bPromise will be considered unhandled. So you have to do Promise.all instead. Clowny.
Maybe there's some clever way we can intercept these warnings in the React runtime but I don't have any ideas right now.
All reactions
-
👀 1
I suppose we could preventDefault inside a global unhandledrejection event handler for the entire render phase. The rationale being that any async operation during rendering must be idempotent, so unhandled rejections can be safely ignored. If you do unwrap with use, the error will be surfaced like a regular error (i.e. trigger the nearest error boundary). https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
EDIT: Probably won't work as-is because the promise will usually reject well after the render phase has completed. But something along these lines would be nice.
All reactions
-
👀 1
Async Server Components cannot contain Hooks
Just to confirm: This does mean we can't use useId in async server components? I'm not sure we'll be able to work around this limitation with non-async server components all the time. It'll definitely feel annoying to abstract sub-trees just so that you can link two or more components by ID.
All reactions
Yeah that's correct. If it ends up being too annoying in practice, one thing we could do is provide a non-hook version of that API, because the current Server Components implementation of that hook is just a per-request counter. However, the downside there is that it won't work as a Shared Component anymore.
In general what we've found is that most use cases for hooks in Server Components can be replaced with a request-local version of the same API. For example, instead of a useRequestHeaders hook, frameworks can provide a getRequestHeaders function that reads from AsyncLocalStorage. It doesn't need to be contextual per tree.
All reactions
Note that you can always just create a "Shared Component" that you only render on the Server. It's mainly semantics.
All reactions
The RFC draft explains the pitfalls preventing client components from also being async/await, but doesn't really address why (despite making references to yield and generators) both can't (or shouldn't) be unified under a pattern like yield instead, where the values that can be yielded are the same as Usable types:
// Server function* Note({id, isEditing}) { // On the server, yielding a Promise could be // equivalent to `await` const note = yield db.posts.get(id); return ( <div> <h1>{note.title}</h1> <section>{note.body}</section> {isEditing ? <NoteEditor note={note} /> : null} </div> ); }
// Client function* Note({id}) { // On the client, we can still yield a promise. // Unlike await, React can potentially resolve this synchronously. const note = yield fetchNote(id); return ( <div> <h1>{note.title}</h1> <section>{note.body}</section> </div> ); }
Here too, React's "replay" behavior becomes not the behavior of a magical function (use) but rather a more reasonably expected one: that by yielding, you are intentionally giving up control with the explicit assumption that you may not get it back. Likewise, there may be fewer gotchas, where e.g. hooks that can "suspend" are clearly documented as such (since they would require yield*) and not simply a surprise.
Furthermore, it would not require the potentially confusing change to the rules of hooks: you can use the yield keyword conditionally, but a use*() call can never be conditional.
Finally, even if the native generator runtime has untenable performance characteristics, I'm not sure there would be anything preventing a specialized React plugin from generating a more optimal representation if necessary.
Note too that there is a lot of existing examples of generators being used for this sort of cooperative multitasking.
I'm not (formally) suggesting this option at the moment, but I can see it being a relatively common question, so perhaps worth addressing as part of the RFC.
All reactions
I think the direction we're headed eventually is that hooks will have custom syntax and we'll compile those to a generator-like form. But this is a longer term idea since it requires custom syntax that would need to be integrated into everyone's toolchains.
All reactions
-
👍 1
Yeah, I think if a compiler is already potentially in scope for other performance reasons, the runtime overhead doesn't seem too relevant in the grand scheme of things. I figure you could probably do a transform from e.g.
const foo = yield bar();
to:
let $$yeeted; // ... const foo = (React.yeet($$yeeted, bar()) ? yield $$yeeted.value : $$yeeted.value);
to avoid most of the additional overhead when no suspense is needed.
The hook abstraction problem is real, though my hunch is that people would probably prefer to know when a hook might suspend, and for such changes to be clearly documented and breaking, though I can appreciate the difference in opinions.
All reactions
Note that on the client, it's always possible for new props to flow in from above while still loading - causing the first generator to be invalid. When that happens, you're still back to square one. So it does help optimizing a bit for this case, but doesn't fully solve the issue and makes it worse for updates.
All reactions
Since that problem doesn't exist on the Server (except setState in render which will be deprecated) it's much more viable there.
All reactions
What about usecases where you want to progressively render data from a chunked http call or generator? Like @stream in GrapQL?
All reactions
An alternative would be to use a WeakMap, which offers similar benefits. The advantage of using a property instead of a WeakMap is that other frameworks besides React can access these fields, too. For example, a data framework can set the status and value fields on a promise preemptively, before passing to React, so that React can unwrap it without waiting a microtask.
So, it's expected that other libraries/frameworks to set those fields to a promise object, before React handles it. Any caveats around it? (I'll follow this convention in one of my libs. Currently, it uses a symbol property.)
All reactions
Comment from @phryneas (Redux Toolkit / RTK Query maintainer):
Suggestion from my side: put these additional properties on symbols (and export the symbols so other libs can choose to access them).
In RTKQ we already add the propertiesarg, requestId, abort, unwrap, unsubscribe, resetandcancelon the promise. We can do that because our library creates those promises - we own them. There is a good chance that another library already has astatusproperty on there - especially if it is not using native promises, but some kind of promise polyfill. I actually think I almost added one myself.
Now, React doesn't "own" those promises. It's a consumer. And it should not pollute the string-properties of those because it could cause conflicts with existing properties. It potentially could affect non-react code.
All reactions
-
👍 1
Yea, it's expected that other libraries/frameworks set these up front.
Note that React doesn't override it if a string already is set but React does expect it to become the string "fulfilled" later. It allows for other statuses to exist.
In fact, the React Server Components client runtime does both of these.
The type accepted by use(...) only accept Promises that either don't have this property or has a status corresponding to React. So it accepts either a "React Promise" or a "Regular Promise". For other kinds of Promises, it's an invalid type but you can pass a wrapper using Promise.resolve(otherPromise).
All reactions
Now, React doesn't "own" those promises. It's a consumer. And it should not pollute the string-properties of those because it could cause conflicts with existing properties. It potentially could affect non-react code.
The owner/consumer distinction is fair enough, but I think it's important to note that React isn't intercepting random promises willy nilly. These properties are only added to promises that are passed explicitly to React. And it's part of the contract of use that React will add these properties. If there's a conflict, you can wrap it another layer, or rename the fields. Or use a different API that better meets your needs.
Using Symbols instead is a reasonable suggestion, but part of our motivation here is to try to create some grassroots alignment with other libraries and frameworks. It's in everyone's best interest if we all choose the same convention.
One can argue it's slightly provocative — honestly, it's intentional. We hope if it gains enough traction it'll convince the standards bodies to address this themselves.
All reactions
I think it's important to note that React isn't intercepting random promises willy nilly
Though we do have another proposal in the works that will probably do something like this :D
All reactions
-
👀 1