diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
index 7dfcb4fc77a76..813c0fe1e7ad1 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
@@ -867,6 +867,44 @@ describe('ReactDOMServerHooks', () => {
});
});
+ it('renders successfully after a component using hooks throws an error', () => {
+ function ThrowingComponent() {
+ const [value, dispatch] = useReducer((state, action) => {
+ return state + 1;
+ }, 0);
+
+ // throw an error if the count gets too high during the re-render phase
+ if (value >= 3) {
+ throw new Error('Error from ThrowingComponent');
+ } else {
+ // dispatch to trigger a re-render of the component
+ dispatch();
+ }
+
+ return
{value}
;
+ }
+
+ function NonThrowingComponent() {
+ const [count] = useState(0);
+ return {count}
;
+ }
+
+ // First, render a component that will throw an error during a re-render triggered
+ // by a dispatch call.
+ expect(() => ReactDOMServer.renderToString()).toThrow(
+ 'Error from ThrowingComponent',
+ );
+
+ // Next, assert that we can render a function component using hooks immediately
+ // after an error occurred, which indictates the internal hooks state has been
+ // reset.
+ const container = document.createElement('div');
+ container.innerHTML = ReactDOMServer.renderToString(
+ ,
+ );
+ expect(container.children[0].textContent).toEqual('0');
+ });
+
if (__EXPERIMENTAL__) {
describe('useOpaqueIdentifier', () => {
it('generates unique ids for server string render', async () => {
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index 63693ed1b5930..8e05eba864c81 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -60,6 +60,7 @@ import escapeTextForBrowser from './escapeTextForBrowser';
import {
prepareToUseHooks,
finishHooks,
+ resetHooksState,
Dispatcher,
currentPartialRenderer,
setCurrentPartialRenderer,
@@ -955,6 +956,7 @@ class ReactDOMServerRenderer {
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
setCurrentPartialRenderer(prevPartialRenderer);
+ resetHooksState();
}
}
diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js
index e2180a47542a1..a6050d91c63e8 100644
--- a/packages/react-dom/src/server/ReactPartialRendererHooks.js
+++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js
@@ -202,24 +202,22 @@ export function finishHooks(
children = Component(props, refOrContext);
}
+ resetHooksState();
+ return children;
+}
+
+// Reset the internal hooks state if an error occurs while rendering a component
+export function resetHooksState(): void {
+ if (__DEV__) {
+ isInHookUserCodeInDev = false;
+ }
+
currentlyRenderingComponent = null;
+ didScheduleRenderPhaseUpdate = false;
firstWorkInProgressHook = null;
numberOfReRenders = 0;
renderPhaseUpdates = null;
workInProgressHook = null;
- if (__DEV__) {
- isInHookUserCodeInDev = false;
- }
-
- // These were reset above
- // currentlyRenderingComponent = null;
- // didScheduleRenderPhaseUpdate = false;
- // firstWorkInProgressHook = null;
- // numberOfReRenders = 0;
- // renderPhaseUpdates = null;
- // workInProgressHook = null;
-
- return children;
}
function readContext(