Last active
April 28, 2025 21:13
-
-
Save tarunsahnan/93418e81882f2e343e09894bc6de6f35 to your computer and use it in GitHub Desktop.
This is a fix for "Jest does not support rendering nested async components." For more details, refer to my comment: https://github.com/testing-library/react-testing-library/issues/1209#issuecomment-2692563090
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { render } from "@testing-library/react"; | |
import React, { | |
Children, | |
cloneElement, | |
isValidElement, | |
ReactElement, | |
ReactNode, | |
} from "react"; | |
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( | |
nodeOrPromise: ReactNode | Promise<ReactNode>, | |
) { | |
// @ts-expect-error | |
const node = await nodeOrPromise; | |
if (isValidElement(node)) { | |
const evaluatedNode = await evaluateServerComponentAndChildren(node); | |
return render(evaluatedNode); | |
} | |
return render(node); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment