Fix state leaking when a function component throws on server render (#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
Phil MacCart committed
Jul 7, 2020 at 19:10 UTC
b85b47630be57c7031b0a9ab741cf858dc0ca215
3 files changed
+51
-13
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+38
@@ -867,6 +867,44 @@ describe('ReactDOMServerHooks', () => {
867
});
868
});
869
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
+
908
if (__EXPERIMENTAL__) {
909
describe('useOpaqueIdentifier', () => {
910
it('generates unique ids for server string render', async () => {
packages/react-dom/src/server/ReactPartialRenderer.js
+2
@@ -60,6 +60,7 @@ import escapeTextForBrowser from './escapeTextForBrowser';
60
import {
61
prepareToUseHooks,
62
finishHooks,
63
+ resetHooksState,
64
Dispatcher,
65
currentPartialRenderer,
66
setCurrentPartialRenderer,
@@ -955,6 +956,7 @@ class ReactDOMServerRenderer {
956
} finally {
957
ReactCurrentDispatcher.current = prevDispatcher;
958
setCurrentPartialRenderer(prevPartialRenderer);
959
+ resetHooksState();
960
}
961
}
962
packages/react-dom/src/server/ReactPartialRendererHooks.js
+11
-13
@@ -202,24 +202,22 @@ export function finishHooks(
202
203
children = Component(props, refOrContext);
204
}
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
+
215
currentlyRenderingComponent = null;
216
+ didScheduleRenderPhaseUpdate = false;
217
firstWorkInProgressHook = null;
218
numberOfReRenders = 0;
219
renderPhaseUpdates = null;
220
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;
221
}
222
223
function readContext<T>(