There has been a lot of frustration in tech Twitter over the last few weeks.
Some people are frustrated with React Server Components, and some are frustrated with the way the React team communicates.
The React team is frustrated with that “pile-on” they are facing.
And everyone is right on their own terms, but we all communicate it in the most unproductive ways.

Twitter is notorious for this kind of discussion because nuance is lost due to its 280-character limit. But I still would like to explain my take on all of this.
So I’ll try to write a blog post instead.

Disclaimer: I’m explaining how things feel or felt to me. A lot of this is highly subjective. Other people might feel differently about the same things. I might just not have noticed things happening, which could have saved me a ton of frustration. This is my personal story, how I experienced it.

I love React Server Components

I love React Server Components. They are a super cool feature. It’s an amazing step forward.
But I am also super frustrated by the place I find myself in right now, wanting to support them.

I want you to understand my frustration

I’m a library maintainer. I am one of the maintainers of Apollo Client, Redux Toolkit and RTK Query. My job (in the case of Apollo) and hobby (in the case of the other two) is to make these libraries as easy & productive to use for their consumers as possible. I love to do that.

Helping users has gotten harder

But it has suddenly gotten a lot harder. People come to me with strange problems, and I can’t solve all of those for them.

  • People on StackOverflow are facing error messages they don’t understand. Turns out they created a new React project with the first recommendation on the React homepage and then just applied default settings.
    They don’t know what a Server Component is, but they are writing Server Components now.
    None of the three libraries works in Server Components as it did before. Apollo needs an extra library; the Redux-based libraries don’t really have RSC support at this point in time. I’ll get to the “why” on that later.
    I can’t give them a 3-hour lesson on StackOverflow, and honestly, I also don’t really know where to point them.
    There is not a lot of documentation that really explains why what they are doing doesn’t work, or how to do it better.
    So I just tell them, “please use problematic-library only in Client Components”.
    I don’t really feel like I’m helping people with that.
  • People come to us with bug reproductions that I don’t understand. Right now, I’m finding myself in the weird situation where I have to assume that this bug that people come to me with might not be in my library code, or in their code, but a React bug.
    I recently spent two hours debugging an infinite loop. I was already at the point where I mocked out useSyncExternalStore when I remembered that I had seen someone else mention the issue on Twitter.
    Turns out their code had an async component. Yes, their bug, but in the past, React would just have errored out with "Objects are not valid as a React child (found: [object Promise])." - but this is Next.js, shipping a canary version of React that allows Client Components to be async.
    And that actually worked in an older canary.
    But not in the newest one. And since this feature is there, but not documented as stable yet, the warning message has been removed and instead, we see this infinite loop.

Understanding things has gotten harder

Apart from that, of course, I am working actively on making our libraries better for our users - in all three environments that currently seem relevant: in React Server Components, in Client Components rendering on the server and streaming to the browser, and in Client Components on the browser.
Honestly, when I started this, I’ve never felt as lost before.
There was no public guidance on anything beyond basic RSC usage, and the Next.js docs - while they were already very good - were definitely not aimed at someone who would be writing a library.
I had to gather most of my information from Twitter discussions. At one point, Dan Abramov sat down with me on a video call and did a lot of explaining - and then later, he spent hours going over my RFC where I tried to write down what I had learned and corrected my misunderstandings and errors.
I am incredibly grateful for that, but at the same time, I look at all of the other React users out there, and I’m frustrated again.

Not everyone out there has a Dan sitting down with them and guiding them through this.
How frustrating an experience must this be for everyone?

Writing and maintaining a widely used library has gotten harder

The bundler is frustrating me to no end. Files imported from React Server Components are statically analyzed. If they contain any reference to hooks like useState, the bundler errors out.

The solution is that libraries now should ship one more entry point, aimed at React Server Components, that doesn’t contain any APIs referencing those hooks. This is already a big moment of frustration. You don’t just “add a new entry point”. Changing anything about a library’s bundler setup is like open heart surgery. One wrong move, and one of your other artifacts changes and breaks in a bunch of bundlers that worked before. It’s nerve-wracking and, frankly, something I’d want to avoid whenever I can. But okay, let’s assume that the ecosystem needs to go through all of this churn. Let’s also assume that I’m willing to essentially duplicate my whole library with RSC-friendly alternatives to all the exports I had in place before.

Now, this new entry point has to be added to the exports field in your package.json. If you already have an exports field, that’s not a problem. If you don’t have it, you’re back at the “breaking bundlers” stage. You can effectively only do that in a major version. This is a big no-go for all libraries that weren’t already planning a new major. (The Redux ecosystem is already at the stage where we were planning for a major anyways, so I’d say here we are lucky.)
If you can’t add that exports field, there is a workaround: If you don’t import the hooks from React as named imports, but as namespace imports - import * as React from 'react', current bundlers won’t pick up on that.

Let’s just hope we’ll have that workaround going into the future. I really don’t want to release a new major of Apollo Client just to ship a new entrypoint for RSCs, even if our users don’t use any of the Client-Component specific features.
Every major version we release will leave users behind (people are still migrating from Apollo Client v2 to v3, and v3 released in 2019!), and releasing a new version of a multi-platform library just to cater to a change in React feels wrong in that regard.

There’s also another pain point here that would be solved by shipping essentially a second RSC-friendly library as an alternative entrypoint, but that, in the meantime, has cost me enough frustration on it’s own, so I want to mention it: createContext is not available in RSC. So the ReactReduxContext that was a top-level export from react-redux could not be created and importing react-redux in a RSC environment was always a crash. This workaround is horrifying.

It feels like the browser is being ignored, and there is no feedback loop

When React 18 released, it shipped with Concurrent Mode, and Suspense was declared “stable” - with one little exception: Suspense for data fetching in the browser was not. So we were good ecosystem citizens and waited, and from time to time, someone dropped into our issues and asked why the library still had no support, and we pointed them to the React 18 release notes and shrugged.
At some point, the use RFC was released, and things started getting more tangible.
So, at the beginning of 2023, I started poking the React team, and the maintainers of Apollo Client, TanStack React Query, and Redux got a meeting with the React team. The message was essentially “yeah , you can try to write an implementation now”. So we did.
A few weeks later, I tried out suspense hooks in the NextJs app router, because our users had asked for that, and everything broke. We had almost released something that would not work with the future of React, because we had been good ecosystem citizens and had written out code against the latest stable release of React. Nobody had told us that things might get hairy in Server Components. We now have a helper library in place, and Apollo Client works nicely with suspense and React Server Components, so there’s not too much harm done here. But it shows a clear disconnect in communication: we only knew that Suspense for data fetching on the client was delayed because the React team was “working on something” and didn’t want their hands bound by things released in the meantime that would lock them into one direction or the other. But we didn’t know it that “something” was Server Components, the React Forget compiler, or something else entirely.

The use RFC also signals something else to me: The React team is trying to create a “symmetry” between Server and Client where it might not actually exist: The server renders once, and the client lives for a long time. A single promise might not be what you use on the client, because your data source might update repeatedly (in the case of a cache, think about updates from other components, requests, and optimistic updates). We need something more.

What frustrates me most here, though, is that we could have given our userbase some kind of suspense implementation years ago, and made their lives much better - if the React team had communicated more openly what they are working on, and had given us guardrails to stay in-between. Even without use, we could already have designed some interesting APIs. It would just not have been their “final form” yet.

This was “far into the future”, and suddenly became very urgent

I am not joking when I say that one week before the App Router (and with that, React Server Components) were declared “stable”, I sincerely believed that we would have at least six more months, probably more like a year, until RSC would get the “stable” blessing from the React team.
At that point, I was already very familiar with React Server Components, and had implemented most of the Apollo Client RSC glue package - so it was not “under my radar”.
But in the past, these “big changes” like this were announced as “this is coming in about X months” by the React team, and instead of that, we were all playing with some kind of “unstable React build” without any indication that this would become stable anytime soon. Not an alpha, beta, or release candidate.
Then, within two days, Canary releases were declared stable, and the App Router was declared stable as well.
The part of the ecosystem that had been waiting for a bigger “starting sign” that something was getting ready was completely overwhelmed by these announcements. I was only prepared for it by sheer luck - because a bunch of users had asked for it.

I still love React Server Components

Despite all these complaints, I want to repeat this paragraph. I am extemely frustrated. But I also see a ton of promise in React Server Components.
There is just a ton of work to be done for all of us.

I also have some suggestions

And that brings us to the part where I stop ranting and start making suggestions. And before I get to the suggestions: If I suggest a new API here, I would be happy to try and implement one or more of these features if the React of Next.js team communicate that they are interested in actually adopting them in the end!

Hide experimental things in Canaries

This is the most obvious win right now: I know that Canaries contain React code with experimental features. The problem is that these Canaries are shipped as part of “stable” Frameworks. As a result, users get exposed to these experimental features, might accidentally start using them, and have things breaking in the future. This is extra sneaky in the case of async functions: in the past, this would have provoked an error from React, and now in some canaries it just works, and in others you see an infinite loop.
I know it might not always be feasible, but can we get feature flags to hide these features in Canary builds here? Throwing errors if a user accidentally uses an unstable feature would really help a lot!

(Much has been said about documentation what actually is in these Canaries, and I believe it already has been heard loudly at this point, so I’ll skip that suggestion here.)

Trying to get the client and server user experience in symmetry

Right now, a lot of concepts don’t translate well between RSC and CC (Client Components), and as a result, we have to give our users very different APIs for both of these environments.
I don’t think that’s necessary. With a few more primitives/conventions, we can create APIs that (apart from initialization) are used the exact same way between RSC and CC.
I believe this would help both adoption, and users switching between projects that are RSC-capable and those that are not.

Of course, I’m speaking from the point of a “data fetching library” here, but I think a lot of these points also apply to other libraries.

Familiar client patterns on the server

Expected usage on Client and Server:

function MyComponent() {
  const result = useQueryWithSuspense(query);

  return <h2>{result.data.title}</h2>;
}

Theoretically, it should be possible to offer this API in both environments. It would be a nice “symmetrical” API, moving a CC pattern into RSC.

The missing pattern

Unfortunately, there is a missing link for this: in a RSC, useQueryWithSuspense doesn’t have access to Context, and cannot get a user-defined client. This could almost be made to work with something like

let getClient;
function registerClient(makeClient) {
  getClient = React.cache(makeClient);
}

but there is no good way right now to ensure that the user code that calls registerClient is actually called, apart from littering it into all layouts and pages. There is no central entry point file in Server Components. It would be great if frameworks could offer a “bootstrap” entrypoint that would be executed every time before a server component renders. This is probably only a little tweak to Next.js & co, but would allow us to write this kind of symmetrical API.

“Server patterns” on the client.

This is more of a “server pattern” that would be great on the client:

function MyComponent({ maxPage }){
  const { readQuery } = useEndpoint()
  const pages = Array(maxPage).fill(undefined).map((_, pageNr) => readQuery(pageNr))

  return <>
    {pages.map(pagePromise => {
      const page = use(pagePromise)
      // ...
    })}
  <>;
}

This one is possible on the server, and it is kinda possible in the browser, with a lot of workarounds, but one gaping problem: In the browser, our component lives for a long time, and a data source might update externally, at a later point in time. Now, it could be solved as an implementation detail of useEndpoint to, in that case, swap out a promise in local state against a resolved one, and return that instead - but that would always force a rerender of MyComponent. In this example, that’s okay, but these pagePromise instances could also have been passed down to child components and only be used there. Currently, this would always cause an unneccessary rerender of the parent component, because we have to swap out those promises during a rerender.

The missing API

If we could use(observable) instead, we would not have this problem. So this is (not for the first time) my plea to give us this API. The server doesn’t need this - RSCs render once, and then everything is thrown away. But in the browser, components live for a long time, and they won’t need data from a promise only once. There will be updates.

Shipping features that are a pain (or impossible) to solve from a library, or even framework perspective

(in RSC/Streaming SSR) useStream

I’m just going to point at the RFC for useStream here. We really need this, as the current workarounds will differ from framework to framework and, at least with Next.js, also have very unfortunate timing. Only being able to inject data into the stream when a suspense boundary finishes can be far too late and lead to very problematic race conditions between Server and Browser.
This API is most vital in streaming SSR and less important in RSCs, but it might be a good idea to offer it there, too.

(in RSC/Streaming SSR) registerCleanupHandler

This goes hand-in-hand with useStream.
It would be great to register a handler to be called after the last component has finished rendering, shortly before the stream is closed, to execute some cleanup logic and inject one last bunch of data into the stream.
Not having this means that on the server, connections could stay open (e.g. when receiving streamed data), receiving data that will not be accessible on the client anymore, and the client might not be aware that the server already stopped streaming, and still wait for that data instead of making a request on it’s own. This API would be equally useful in RSCs and streaming SSR.

(in Client Components) useComponentIdentifier(teardownFn?)

There was recently a great talk by Robert Balicki (slides video) that shows how much is necessary to get Suspense for data fetching working on the client. Components render, they suspend, they render again without knowing that they rendered before. As a result, there is an absurd dance that every library has to do in userspace to ensure that a (hopefully stable) promise is returned, and that data that is not needed anymore (because a component didn’t render in the end, or changed props in the meantime and requested different data) is disposed of correctly. We are talking reference counting, timers, all that jazz. React knows what the last promise for a component was when it restarts a component render, so it has some concept of component identity, even if a component didn’t commit yet.
It would save a ton of userspace workarounds (and, again, there is no good way to do this at all) if React would allow a component to ask for some kind of unique identifier. That could even just be an empty object to be used as a key in a Map or WeakMap. Bonus points if we could get some kind of “final teardown” callback to do a nice cleanup once a component is discarded.

And now?

Honestly, I don’t know. I could open RFCs for these features that I think are still missing, but I already wrote under the useStream RFC that we really need something like this and even offered to implement it. There was no feedback to it, so I am not too hopeful that opening more RFCs or Issues in the Next.js GitHub Issues (that currently seem to be drowning in confused users and/or bugs) will lead to anything.
So instead, I’m writing this post. And I hope the right person sees it because the Twitter or Reddit algorithm sweeps it their way.
I’m open to talk about these ideas and want to help if I can. Please talk to me!