@samitouri / QOS-React-2 / commits / b85b47630b

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>(