Testing async React RSC components

September 27, 2025
#testing#react#rsc#nextjs#vitest#typescript#frontend

Testing React Server Components (RSCs) is quite difficult.

This is because of the async behaviour of the components.

It is so hard in fact, that the normal recommended approach to testing is to use e2e tests (like Playwright or Cypress) to test RSC components.

Here is my guide to testing RSC components with React Testing Library's render(...) functionality as best as I can figure out.

I am only going to cover 'regular' tests (unit tests, with React Testing Library) with Jest or Vitest and React Testing Library.

Want to skip to my recommended way? See the section on use() below.

What I am trying to test

I've made some very simple RSC components which do some data fetching. As I am testing with Vitest, I am mocking window.fetch with vitest-fetch-mock:

import createFetchMock from 'vitest-fetch-mock';
import { vi } from 'vitest';
createFetchMock(vi).enableMocks();

The mocked data for the fetch isn't important. The important thing is that the await inside the RSC component is actually awaited, we get the mock data, and render the JSX showing the count of rows (5 in the mock data)

const dummyData = {
  results: [
    { name: 'bulbasaur' },
    { name: 'ivysaur' },
    { name: 'venusaur' },
    { name: 'charmander' },
    { name: 'mew' },
  ],
};

And here are 3 components - an outer container (with suspense), and a couple of RSC components which do some data fetching.

(One of the difficulties is loading nested async RSC components, which is why this has the mix)

export const ContainerWithSuspenseAndLoading =
  () => {
    return (
      <div>
        <Suspense
          fallback={<b>loading 1</b>}
        >
          <FetchData />
        </Suspense>
      </div>
    );
  };

// Async RSC component, with another nested RSC component inside of it:
export const FetchAndChildComponentFetches =
  async () => {
    const data = await fetch(
      '/data'
    ).then(res => res.json());

    return (
      <div>
        <h1>
          FetchAndChildComponentFetches
        </h1>
        <h2>
          Another Fetch rows:
          {data.results.length}
        </h2>

        <ContainerWithSuspenseAndLoading />
      </div>
    );
  };

// Async RSC component (no nested child this time)
async function FetchData() {
  const data = await fetch(
    '/data'
  ).then(res => res.json());

  return (
    <div>
      <h1>Fetch Results</h1>
      <h2>
        Num rows: {data.results.length}
      </h2>
    </div>
  );
}

How to test it?

Using these versions of react and react-dom:

{
  "react": "19.1.0",
  "react-dom": "19.1.0"
}

Let's start with the easiest.

Testing a RSC component without any nested RSC components or Suspense

This is the most simple RSC component, as we have no nested child async components, and no <Suspense> to deal with.

However, even in such a simple setup, the following will not work:

it('renders FetchData', async () => {
  render(<FetchData />);

  // ❌ this will fail!
  expect(
    await screen.findByText(
      'Num rows:',
      { exact: false }
    ) // ❌ fails!
  ).toHaveTextContent('Num rows: 5');
});

It fails as it still sees the rendered document as just <body></body>.

You can even check that window.fetch was called, and it is called! So it is running our RSC component, but not rendering it all into the DOM for React Testing Library to find.

it('renders FetchData - await syntax', async () => {
  render(<FetchData />);

  // this part passes ok
  await waitFor(() => {
    expect(
      window.fetch
    ).toBeCalledTimes(1);
  }); // ✅ passes

  // ❌ fails here
  expect(
    await screen.findByText(
      'Num rows:',
      { exact: false }
    ) // ❌ fails!
  ).toHaveTextContent('Num rows: 5');
});

But there is a simple work around - use await FetchData().

it('renders FetchData - await syntax', async () => {
  // awaiting the component inside render(),
  render(await FetchData());

  expect(window.fetch).toBeCalledTimes(
    1
  ); // ✅ passes

  // no need to await findBy... here:
  expect(
    screen.getByText('Num rows:', {
      exact: false,
    })
  ).toHaveTextContent('Num rows: 5'); // ✅ passes
});

Note: because of the await inside render(await FetchData()), we don't need to await screen.findByText(...).

If you need to pass props, you can just do render(await SomeComponent({someProp: true}))

But using this method only works when the top level component is the only async RSC component!

In this next test, the component has <Suspense> with its child being the <FetchData /> component from the previous tests. But because of that Suspense, it fails to render anything other than the fallback (loading).

it('renders ContainerWithSuspenseAndLoading - await syntax', async () => {
  // component has a <Suspense> with a fallback of <b>loading 1</b>
  render(
    await ContainerWithSuspenseAndLoading()
  );

  expect(
    screen.getByText('loading 1')
  ).toBeInTheDocument(); // ✅ passes

  // ❌ this part fails:
  expect(
    await screen.findByText(
      'Num rows:',
      { exact: false }
    ) // ❌ fails!
  ).toHaveTextContent('Num rows: 5');
});

It will never render anything past the initial Suspense fallback, even with await screen.findBy...

Sometimes though, using the render(await SomeComponent()) is good enough, if all of your data loading is done in that component. You can assume any parent component (with <Suspense>) is going to call it correctly.

But it is still not ideal.

Use a helper function

After lots of research and digging through tons of Github issue comments, I found a couple of work around render functions.

Check it out here: renderServerComponent

This helper function will render RSC components and child async RSC components correctly.

it('using renderServerComponent, renders <Container />', async () => {
  // no await here:
  renderServerComponent(
    <ContainerWithSuspenseAndLoading />
  );
  // starts with the default empty body:
  expect(
    document.body
  ).toMatchInlineSnapshot(`<body />`); // ✅ passes

  // until we await findBy...:
  expect(
    await screen.findByText(
      'Num rows:',
      { exact: false }
    )
  ).toHaveTextContent('Num rows: 5'); // ✅ passes
});

Getting the initial Suspense fallback component (the loading one) wasn't possible. (Even adding a setTimeout - I thought at first my mock data was being immediately returned).

It figures out if the component it is rendering is a RSC component, and fully awaits for it to return (recursively awaiting the entire tree where needed). Then when it has evaluated the entire tree, it uses that to pass into render()

Use a specific version of React

There is a specific version of React 19 on NPM which can be used that seems to work really well for testing RSC components.

This specific version will work:

{
  "react": "19.0.0-rc-380f5d67-20241113",
  "react-dom": "19.0.0-rc-380f5d67-20241113"
}

If you use this, then writing tests with RSC components is easy - you can test Suspense, you can just use regular render(<SomeRSCComponent />).

But as far as I can tell (I tried many other rc and proper release versions) this is the only version that works.

If you use Next (please correct me if I am wrong here - I am not too sure about this part) it bundles its own version of React.

So I've had success by using Next 15 (which bundles its own React 19 version), but in my package.json (which Vitest/Jest will use) I use the rc versions above. The actual Next app works correctly, and the tests(which use the rc version) are easily testable). I am very sure this hacky work around (not nice to be testing on different versions of React) will cause problems on some repos though. I am not sure I would recommend it. However it does work.

Vitest plugin

There is a plugin for Vitest (sorry Jest users) called vitest-plugin-rsc

Note, the current version is 0.0.0-experimental-64e35ee-20250807. It was last updated 2 months ago.

However, it is only for Vitest's browser mode, which isn't what I am after.

Using act(render(...)) with use()

Inspired by this reddit post another work around is to avoid await-ing things, but just use use()

import {
  act,
  render,
  screen,
} from '@testing-library/react';
import { use, Suspense } from 'react';

const ComponentUsingUse = ({
  params,
}: {
  params: Promise<{ lang: string }>;
}) => {
  const { lang } = use(params);
  return <div>hello from {lang}</div>;
};

const ComponentUsingUseAndSuspense = ({
  params,
}: {
  params: Promise<{ lang: string }>;
}) => {
  return (
    <div>
      <Suspense
        fallback={<b>loading</b>}
      >
        <ComponentUsingUse
          params={params}
        />
      </Suspense>
    </div>
  );
};

it('renders content from use, without suspense', async () => {
  await act(() =>
    render(
      <ComponentUsingUse
        params={Promise.resolve({
          lang: 'en',
        })}
      ></ComponentUsingUse>
    )
  );
  expect(
    screen.getByText('hello from en')
  ).toBeInTheDocument(); // ✅ passes
});

it('renders with suspense', async () => {
  await act(() =>
    render(
      <ComponentUsingUseAndSuspense
        params={Promise.resolve({
          lang: 'en',
        })}
      ></ComponentUsingUseAndSuspense>
    )
  );
  expect(
    screen.getByText('hello from en')
  ).toBeInTheDocument(); // ✅ passes
});

// ❌ this will fail (doesn't wrap render in act())
it.skip('renders content from use, without suspense, without act()', async () => {
  // won't work, without wrapping render in await act()
  render(
    <ComponentUsingUse
      params={Promise.resolve({
        lang: 'en',
      })}
    ></ComponentUsingUse>
  );

  expect(
    await screen.findByText(
      'hello from en'
    )
  ).toBeInTheDocument(); // ❌ fails!
});

Here is an example showing data fetching, using use(), with a working test:

// note: this should NOT be an async component! (or use() won't work)
const Component = () => {
  // note: you will probably want to keep this promise stable!
  // e.g. wrap it in useMemo
  const fetchPromise = fetch(
    '/data'
  ).then(res => res.json());
  const data = use(fetchPromise);

  return (
    <div>
      <h1>Loaded data</h1>
      <h2>
        Rows: {data?.results?.length}
      </h2>
    </div>
  );
};

it('renders fetch using use', async () => {
  fetchMock.mockResponse(
    JSON.stringify(dummyData)
  );

  await act(() =>
    render(
      <Suspense>
        <Component />
      </Suspense>
    )
  );

  expect(
    screen.getByText('Loaded data')
  ).toBeInTheDocument(); // ✅ passes
  expect(
    screen.getByText('Rows: 5')
  ).toBeInTheDocument(); // ✅ passes
});

Note: using this approach of await act(() => render(...) with the other previous components (which await-ed in the component) will not work.

If you are starting a new app (where you don't have lots of await inside your existing components) I think this might be the best approach so far. It isn't hacky (ignoring the act() wrapped around the render call) and feels like it should work fine with future versions of React.

In summary

  • If possible, the easiest way for you to test your async RSC components is remove the async part and wrap your promises in use().
  • If you are using next, and are happy to test with a different version of react, you can use a specific (slightly older) version of React. I don't recommend this for big production apps.
  • If you have nested async RSC components, you might find it easier to test each async component individually. Using standard react versions, with nested async RSC components is a pain to do.
  • For most confidence that your components are running as expected, doing E2E (for example, Cypress or Playwright) is the best route.

Resources

Do you know of other tips/tricks/resources/urls for testing React Server components in your unit tests?

Please contact me and I will try it out & update this.

Found this useful? Share this article

TwitterLinkedIn Facebook

Want to become a pro at testing your React apps?

I've got a huge range of interactive lessons from beginner to expert level.

Get better at testing every week:

Join developers, engineers and testers who receive my free newsletter with React testing tips

I've been writing tests for years, but as I research content for this blog, I am always discovering new things in the tools we use every day!

Get free React/testing tips 👇