- author::
- link:: rfcs/0000-first-class-support-for-promises.md at first-class-promises · acdlite/rfcs (github.com)
- extra tags:: #react #server #islands-architecture
React server components (initial proposal) was a so-so DX that solved an important problem: I want to render a React component on the server only, with APIs to easily fetch data for rendering.
- Pulls on islands architecture concepts (hi Astro.build!)
- Improves full-stack ergonomics
Proposal streamlines underpinnings of React Query, NextJS, and Remix, but does not replace them.
- server components will just be async components. No cognitive overhead, just
async / await
:
async function Note({id, isEditing}) {
const note = await db.posts.get(id);
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
{isEditing ? <NoteEditor note={note} /> : null}
</div>
);
}
- why this was missing: didn't want server / client components to feel different
- why this was added: writing React adapters for every server-side API (a majority built around async / await) isn't worth the cost
- Makes server components easy to identify. If it says
async function
, it's intended for the server- ❓ Kinda weird assumption if async is supported on the client at some point (see last section)
- ❓ Async != server in most people's minds
We've since changed our mind. We now believe that the benefits of async/await in Server Components outweigh the downsides of having a different API (
use
) on the client.
React is not bringing async / await
to client components due to technical footguns (see last section). Instead, you can "unwrap" promises and block rendering with use
:
// `use` inside of a React component or Hook...
const data = use(promise);
// ...roughly equates to `await` in an async function
const data = await promise;
Adapts rules of promises to hooks:
use
will only work inside components and hook functions (like traditional hooks)use
will work inside conditionals and loops (like await, unlike traditional hooks)use
will block rendering until promise is resolved. Will bubble up to the suspense boundary during loading to render any fallbacks, and will unblock any UI when ready- Can use for conditional fetching too (again, unlike traditional hooks like
useQuery
)
function Note({id, shouldIncludeAuthor}) {
const note = use(fetchNote(id));
let byline = null;
if (shouldIncludeAuthor) {
// only make expensive fetch when author is needed
const author = use(fetchNoteAuthor(note.authorId));
byline = <h2>{author.displayName}</h2>;
}
return (
<div>
<h1>{note.title}</h1>
{byline}
<section>{note.body}</section>
</div>
);
}
Without a cache, use
would fetch + resolve on every render (at least w/o a useEffect
presumably). We need a useQuery
-like caching strat
Missing from present proposal, but will add a cache
helper to wrap fetch calls 👀
const fetchNote = cache(async (id) => {
const response = await fetch(`/api/notes/${id}`);
return await response.json();
} /* unknown: caching options */);
function Note({id}) {
// The `fetchNote` call returns the same promise every time until a new id
// is passed, or until the cache is refreshed.
const note = use(fetchNote(id));
...
- if components were async, it would be too easy to invalidate a component prop and re-render; performance
-
It's not so much that it introduces new performance caveats, but it makes all the performance caveats described above much more likely
-
- flagging with
use
could pave way for auto-memoizing compiler - still
async/ await
is technically possible on the client - will warn async components with dev server error, but will not fail to build (huh??)
- confirmed in PR comments
- React wants NextJS to play with this idea internally before they start throwing compiler errors. Sounds like they're unsure of best practices right now