Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save JasonMore/99db4c19cdee596c5d8ffa6849a50aa3 to your computer and use it in GitHub Desktop.
Save JasonMore/99db4c19cdee596c5d8ffa6849a50aa3 to your computer and use it in GitHub Desktop.
This is a fix for "Jest does not support rendering nested async components." with testing library render options. Issue https://github.com/testing-library/react-testing-library/issues/1209#issuecomment-2692563090
// Added ability to pass in options for testing-library/react
//
// Lifted from https://gist.github.com/tarunsahnan/93418e81882f2e343e09894bc6de6f35 on 2025-04-25
// Thread: https://github.com/testing-library/react-testing-library/issues/1209#issuecomment-2692563090
//
// ORIGINAL BELOW
//
// While using [this solution](https://github.com/testing-library/react-testing-library/issues/1209#issuecomment-2400054404),
// I encountered a timeout issue caused by API calls in my components. Each component was waiting for its
// API call to complete before rendering, which led to delays and affected the flow to subsequent components.
// To resolve this, I refactored the logic so that the API calls are initiated in parallel.
// Then, the function waits for all components to finish rendering.
import { Queries, queries } from '@testing-library/dom';
import { RenderOptions, render } from '@testing-library/react';
import React, {
Children,
ReactElement,
ReactNode,
cloneElement,
isValidElement,
} from 'react';
import * as ReactDOMClient from 'react-dom/client';
type RendererableContainer = ReactDOMClient.Container;
type HydrateableContainer = Parameters<
(typeof ReactDOMClient)['hydrateRoot']
>[0];
function setFakeReactDispatcher<T>(action: () => T): T {
/**
* We use some internals from React to avoid a lot of warnings in our tests when faking
* to render server components. If the structure of React changes, this function should still work,
* but the tests will again print warnings.
*
* If this is the case, this function can also simply be removed and all tests should still function.
*/
if (!('__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' in React)) {
return action();
}
const secret = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
if (
!secret ||
typeof secret !== 'object' ||
!('ReactCurrentDispatcher' in secret)
) {
return action();
}
const currentDispatcher = secret.ReactCurrentDispatcher;
if (
!currentDispatcher ||
typeof currentDispatcher !== 'object' ||
!('current' in currentDispatcher)
) {
return action();
}
const previousDispatcher = currentDispatcher.current;
try {
currentDispatcher.current = new Proxy(
{},
{
get() {
throw new Error('This is a client component');
},
},
);
} catch {
return action();
}
const result = action();
currentDispatcher.current = previousDispatcher;
return result;
}
async function evaluateServerComponent(
node: ReactElement,
): Promise<ReactElement> {
if (node && node.type?.constructor?.name === 'AsyncFunction') {
// Handle async server nodes by calling await.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const evaluatedNode: ReactElement = await node.type({ ...node.props });
return evaluateServerComponent(evaluatedNode);
}
if (node && node.type?.constructor?.name === 'Function') {
try {
return setFakeReactDispatcher(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const evaluatedNode: ReactElement = node.type({ ...node.props });
return evaluateServerComponent(evaluatedNode);
});
} catch {
// If evaluating fails with a function node, it might be because of using client side hooks.
// In that case, simply return the node, it will be handled by the react testing library render function.
return node;
}
}
return node;
}
async function evaluateServerComponentAndChildren(node: ReactElement) {
const evaluatedNode = await evaluateServerComponent(node);
if (!evaluatedNode?.props.children) {
return evaluatedNode;
}
const children = Children.toArray(evaluatedNode.props.children);
const promises = [];
for (let i = 0; i < children.length; i += 1) {
const child = children[i];
if (!isValidElement(child)) {
continue;
}
promises.push({
index: i,
promise: new Promise(async (resolve) => {
const value = await evaluateServerComponentAndChildren(child);
resolve({ index: i, value });
}),
});
}
const values = (await Promise.all(promises.map((c) => c.promise))) as {
index: number;
value: React.ReactElement;
}[];
values.forEach((v) => {
children[v.index] = v.value;
});
return cloneElement(evaluatedNode, {}, ...children);
}
// Follow <https://github.com/testing-library/react-testing-library/issues/1209>
// for the latest updates on React Testing Library support for React Server
// Components (RSC)
export async function renderServerComponent<
Q extends Queries = typeof queries,
Container extends RendererableContainer | HydrateableContainer = HTMLElement,
BaseElement extends RendererableContainer | HydrateableContainer = Container,
>(
nodeOrPromise: ReactNode | Promise<ReactNode>,
options: RenderOptions<Q, Container, BaseElement>,
) {
const node = await nodeOrPromise;
if (isValidElement(node)) {
const evaluatedNode = await evaluateServerComponentAndChildren(node);
return render(evaluatedNode, options);
}
return render(node, options);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment