Overview of TanStack RSC

With the release of React Server Component's for TanStack Start, pretty much all of the major React meta-frameworks now support RSCs. While the release for TanStack has generally been met with an overwhelming positive reaction, there was some criticism of the decision to intentionally not support the use server directive. They have a few reasons for this:

  1. 1.

    TanStack Start already has it's own flavor of server functions with a lot of advanced features like middleware and cross-framework support for Solid in addition to React.

  2. 2.

    The Start server functions also are a more explicit about them being just RPC's, so they have first class support for validators like Zod, defining which HTTP method to use (POST, GET, etc) and being able to return Fetch Request objects.

  3. 3.

    The recent spate of vulnerabilities found in RSC's essentially involve hacking React's Flight payload format which is used to both send RSC's down the wired to the client but also accept arguments to use server Server Functions. TanStack Start Server Functions already use a different serialisation format that's been a bit more battle tested (a warning to the reader that security is never finished)

Jack Herrington, who has done a lot of work with Start over the past year, summed up the argument in more detail in this tweet1:

Jack Herrington on Twitter / X
1. You are free to decide if it's "RSCs" for yourself of course. But it's not a protocol. Protocols like the File Transfer Protocol (FTP) or HTTP, have documented standards, grammar, compliance tests, RFCs, and are demonstrated as language neutral across all the popular…— Jack Herrington (@jherr) April 14, 2026
https://x.com/jherr/status/2044046125368521117

For most people building apps, this is two ways of boiling a fish. If you're building an app in Next.js or React Router, you use use server, If you're building an app in TanStack Start, you'll use the createServerFn function instead, they'll do mostly the same thing. Or your can just use tRPC/oRPC, REST with OpenAPI2, or GraphQL if that's what the shape of your system demands.

But one question that popped up during the discussions of the split between the use server directive in React and TanStack start server functions was that it would bascailly make having Use server directive functions exported from libraries impossible in TanStart. This question intrigued me a bit so I decided to investigate it.

Now, directly exporting a function that just uses the use server directive doesn't seem that useful. Simply exporting a normal node function for the user to wrap in a use server/createServerFn on the application side is simple enough, and you probably don't want the extra "magic" of use server hoisting the function onto an implicit RPC endpoint to be encapsulated on the npm package side. But one thing RSC's can do with use server is pass down a Server Function into a child component, or render a form that uses the Server Action as a Form Action. These seem to be the most useful cases so I decided to focus on those.

The experiment

My experiment resulted in this monorepo. It consists of one RSC package and four applications (Next, Waku, React Router v7 Framework Mode, TanStack React Start). It exports a write function that doubles an number and persists to a file, along with components that when combined together form a card that reads from the file with RSC and also displays a form that uses the write function as an action. There are two ways this comes together in the apps:

  1. 1.

    One single RSC that included the value component and form, with the write action passed to the form via use server as an internal implementation detail of the component.

  2. 2.

    A decomposed variant where the shell, value, form component and write function are exported from the package and recombined on the app side. in this case the write function is wrapped with use server/createServerFn on the app side.

Reading the code, you can see while the Next, Waku, and React Router apps use the RSC's in the same way, Start is a bit more verbose as it uses it's RSC helpers to return components from Server Functions, where it can be loaded into the app with either TanStack Router loader's or, in the case of this app, use TanStack Query to cache the RSC's.

The results

Here's what we got:

  1. 1.

    Submitting on the internal action form in Next, Waku, and the React Router apps will not crash the application. TanStack Start will crash with an error, and this is down to Start not using the use server directive.

  2. 2.

    However, with the exception of React Router, the "Persisted Number" count doesn't update after clicking on the submit button. You have to either refresh the page or click on the "Force Revalidate" button in each app.

  3. 3.

    Meanwhile, the decomposed versions all submit properly without errors and will update the count after submission.

So what's going on here?

Remember that use server will end up being translated to a POST3 call to the server, and mutates the file with the number times 2. After the mutation, you need to update the web page, and that's where the interesting differences begin to emerge. So, this is what the decomposed versions all do differently from the internal use server action:

  1. 1.

    With Next after you mutate with a serverless action, you will have to revalidate Next's cache. (in this case with revalidatePath

  2. 2.

    Waku has an unstable_rerenderRoute function that were using for the same purposes. This was, unsuprisinly givent the name, undocumented and was only found after asking an AI to "look at the Waku repo and figure this out". Open to a better solution for this.

  3. 3.

    React Router - It Just Works. Whatever way they have it set up, It seems to just revalidate the whole route whenever a Server Function attached is triggered. Pretty slick honestly, but not too customisable.

  4. 4.

    TanStack Start has to use createServerFn both for the action and to return the RSC's. In the decomposed version, the form section of the card, unlike the other three is not an RSC, as our cache invalidation strategy release on calling invalidateQueries in the onSuccess of a useMutation. Since we're getting the mutation function wrapped in a hook, that has to be a Client Component4.

It turns out if you make Server Functions without reference to your frameworks caching strategy, your results are not gonna be great, and that means that having an RSC encapsulate a use server function from an external package that expects to be framework agnostic probably isn't that useful. That's even leaving out the question of whether application devs want there NPM packages setting up hidden POST endpoints that are a bit to implicit for comfort.

Closing thoughts

So, given all of this, my personal guidelines if I was looking to publish an RSC package on NPM today:

  1. 1.

    Publishing an RSC that directly does some server work is OK so long as that work is inline with what is to be expected from a GET request - read only. Mutations are forbidden here.

  2. 2.

    Client Components marked with use client also work fine across all the frameworks. You might want to decompose them if you want to invert control to the user and allow TanStack users to use Composite Components instead, but encapsulated inside another component is fine.

  3. 3.

    If there are server mutations that I'm shipping as well, export those separately, do not mark them with use server. Let the user intergrate them with there framework however they see best.

This might change down the line, and I'm open to corrections on the points above if I missed or misunderstood something, but that's where I am right now.

RSC adoption has been pretty slow to my mind, even in the NextJS space, and I'd be of the opinion that they have been overhyped a bit. TanStack Router and Start's flexibility and no-compromises attitude to end-to-end type-safety, alongside Query's excellent handling of async state, have always seemed to be more useful to me then RSC's over the last 4 years.

To conclude then, RSC's exported from packages will have some restrictions vs ones defined in application, but I doubt they'll be a major issue for the React ecosystem. If your interested to read more on this topic, I'd like to shout out the
list of links that Ryan posted5, especially "Are Server Components Actually That Composable?" for a lot of good thinking on the benefits and limitations of RSC and how that's ultimately affected where TanStack landed.

Happy rendering your components where you deem most appropriate.