@samitouri / QOS-React / commits / 9cd52b27fe

Restore context after an error happens (#21341)

Typically we don't need to restore the context here because we assume that we'll terminate the rest of the subtree so we don't need the correct context since we're not rendering any siblings. However, after a nested suspense boundary we need to restore the context. The boundary could do this but since we're already doing this in the suspense branch of renderNode, we might as well do it in the error case which isn't very perf sensitive anyway.

Sebastian Markbåge committed Apr 23, 2021 at 15:24 UTC 9cd52b27fe7be3c320d1e94a016fc416e66569b1
2 files changed +74
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+67
@@ -796,6 +796,73 @@ describe('ReactDOMFizzServer', () => {
796 );
797 });
798
799 + // @gate experimental
800 + it('should recover the outer context when an error happens inside a provider', async () => {
801 + const ContextA = React.createContext('A0');
802 + const ContextB = React.createContext('B0');
803 +
804 + function PrintA() {
805 + return (
806 + <ContextA.Consumer>{value => <Text text={value} />}</ContextA.Consumer>
807 + );
808 + }
809 +
810 + class PrintB extends React.Component {
811 + static contextType = ContextB;
812 + render() {
813 + return <Text text={this.context} />;
814 + }
815 + }
816 +
817 + function Throws() {
818 + const value = React.useContext(ContextA);
819 + throw new Error(value);
820 + }
821 +
822 + const loggedErrors = [];
823 + await act(async () => {
824 + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
825 + <div>
826 + <PrintA />
827 + <div>
828 + <ContextA.Provider value="A0.1">
829 + <Suspense
830 + fallback={
831 + <b>
832 + <Text text="Loading..." />
833 + </b>
834 + }>
835 + <ContextA.Provider value="A0.1.1">
836 + <Throws />
837 + </ContextA.Provider>
838 + </Suspense>
839 + <PrintB />
840 + </ContextA.Provider>
841 + </div>
842 + <PrintA />
843 + </div>,
844 + writable,
845 + {
846 + onError(x) {
847 + loggedErrors.push(x);
848 + },
849 + },
850 + );
851 + startWriting();
852 + });
853 + expect(loggedErrors.length).toBe(1);
854 + expect(loggedErrors[0].message).toEqual('A0.1.1');
855 + expect(getVisibleChildren(container)).toEqual(
856 + <div>
857 + A0
858 + <div>
859 + <b>Loading...</b>B0
860 + </div>
861 + A0
862 + </div>,
863 + );
864 + });
865 +
866 // @gate experimental
867 it('client renders a boundary if it errors before finishing the fallback', async () => {
868 function App({isClient}) {
packages/react-server/src/ReactFizzServer.js
+7
@@ -1174,6 +1174,13 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
1174 // Restore all active ReactContexts to what they were before.
1175 switchContext(previousContext);
1176 } else {
1177 + // Restore the context. We assume that this will be restored by the inner
1178 + // functions in case nothing throws so we don't use "finally" here.
1179 + task.blockedSegment.formatContext = previousFormatContext;
1180 + task.legacyContext = previousLegacyContext;
1181 + task.context = previousContext;
1182 + // Restore all active ReactContexts to what they were before.
1183 + switchContext(previousContext);
1184 // We assume that we don't need the correct context.
1185 // Let's terminate the rest of the tree and don't render any siblings.
1186 throw x;