Skip to content

Commit dcb6032

Browse files
pmaccarttrueadm
authored andcommitted
Fix state leaking when a function component throws on server render (facebook#19212)
* add unit test asserting internal hooks state is reset * Reset internal hooks state before rendering * reset hooks state on error * Use expect...toThrow instead of try/catch in test * reset dev-only hooks state inside resetHooksState * reset currentlyRenderingComponent to null
1 parent c9e3494 commit dcb6032

File tree

3 files changed

+51
-13
lines changed

3 files changed

+51
-13
lines changed

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,44 @@ describe('ReactDOMServerHooks', () => {
867867
});
868868
});
869869

870+
it('renders successfully after a component using hooks throws an error', () => {
871+
function ThrowingComponent() {
872+
const [value, dispatch] = useReducer((state, action) => {
873+
return state + 1;
874+
}, 0);
875+
876+
// throw an error if the count gets too high during the re-render phase
877+
if (value >= 3) {
878+
throw new Error('Error from ThrowingComponent');
879+
} else {
880+
// dispatch to trigger a re-render of the component
881+
dispatch();
882+
}
883+
884+
return <div>{value}</div>;
885+
}
886+
887+
function NonThrowingComponent() {
888+
const [count] = useState(0);
889+
return <div>{count}</div>;
890+
}
891+
892+
// First, render a component that will throw an error during a re-render triggered
893+
// by a dispatch call.
894+
expect(() => ReactDOMServer.renderToString(<ThrowingComponent />)).toThrow(
895+
'Error from ThrowingComponent',
896+
);
897+
898+
// Next, assert that we can render a function component using hooks immediately
899+
// after an error occurred, which indictates the internal hooks state has been
900+
// reset.
901+
const container = document.createElement('div');
902+
container.innerHTML = ReactDOMServer.renderToString(
903+
<NonThrowingComponent />,
904+
);
905+
expect(container.children[0].textContent).toEqual('0');
906+
});
907+
870908
if (__EXPERIMENTAL__) {
871909
describe('useOpaqueIdentifier', () => {
872910
it('generates unique ids for server string render', async () => {

packages/react-dom/src/server/ReactPartialRenderer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import escapeTextForBrowser from './escapeTextForBrowser';
6060
import {
6161
prepareToUseHooks,
6262
finishHooks,
63+
resetHooksState,
6364
Dispatcher,
6465
currentPartialRenderer,
6566
setCurrentPartialRenderer,
@@ -955,6 +956,7 @@ class ReactDOMServerRenderer {
955956
} finally {
956957
ReactCurrentDispatcher.current = prevDispatcher;
957958
setCurrentPartialRenderer(prevPartialRenderer);
959+
resetHooksState();
958960
}
959961
}
960962

packages/react-dom/src/server/ReactPartialRendererHooks.js

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -202,24 +202,22 @@ export function finishHooks(
202202

203203
children = Component(props, refOrContext);
204204
}
205+
resetHooksState();
206+
return children;
207+
}
208+
209+
// Reset the internal hooks state if an error occurs while rendering a component
210+
export function resetHooksState(): void {
211+
if (__DEV__) {
212+
isInHookUserCodeInDev = false;
213+
}
214+
205215
currentlyRenderingComponent = null;
216+
didScheduleRenderPhaseUpdate = false;
206217
firstWorkInProgressHook = null;
207218
numberOfReRenders = 0;
208219
renderPhaseUpdates = null;
209220
workInProgressHook = null;
210-
if (__DEV__) {
211-
isInHookUserCodeInDev = false;
212-
}
213-
214-
// These were reset above
215-
// currentlyRenderingComponent = null;
216-
// didScheduleRenderPhaseUpdate = false;
217-
// firstWorkInProgressHook = null;
218-
// numberOfReRenders = 0;
219-
// renderPhaseUpdates = null;
220-
// workInProgressHook = null;
221-
222-
return children;
223221
}
224222

225223
function readContext<T>(

0 commit comments

Comments
 (0)