2
let ReactNoop;
3
let Scheduler;
4
let act;
5
+let use;
6
let useState;
7
let useContext;
8
let Suspense;
20
ReactNoop = require('react-noop-renderer');
21
Scheduler = require('scheduler');
22
act = require('internal-test-utils').act;
23
+ use = React.use;
24
useState = React.useState;
25
useContext = React.useContext;
26
Suspense = React.Suspense;
939
assertLog(['B', 'B']);
940
expect(root).toMatchRenderedOutput('BB');
941
});
942
+
943
+ it('regression: context change triggers retry of suspended Suspense boundary on initial mount', async () => {
944
+ // Regression test for a bug where a context change above a suspended
945
+ // Suspense boundary would fail to trigger a retry. When a Suspense
946
+ // boundary suspends during initial mount, the primary children's fibers
947
+ // are discarded because there is no current tree to preserve them. If
948
+ // the suspended promise never resolves, the only way to retry is
949
+ // something external — like a context change. Context propagation must
950
+ // mark suspended Suspense boundaries for retry even though the consumer
951
+ // fibers no longer exist in the tree.
952
+ //
953
+ // The Provider component owns the state update. The children are
954
+ // passed in from above, so they are not re-created when the Provider
955
+ // re-renders — this means the Suspense boundary bails out, exercising
956
+ // the lazy context propagation path where the bug manifests.
957
+ const Context = React.createContext(null);
958
+ const neverResolvingPromise = new Promise(() => {});
959
+ const resolvedThenable = {status: 'fulfilled', value: 'Result', then() {}};
960
+
961
+ function Consumer() {
962
+ return <Text text={use(use(Context))} />;
963
+ }
964
+
965
+ let setPromise;
966
+ function Provider({children}) {
967
+ const [promise, _setPromise] = useState(neverResolvingPromise);
968
+ setPromise = _setPromise;
969
+ return <Context.Provider value={promise}>{children}</Context.Provider>;
970
+ }
971
+
972
+ const root = ReactNoop.createRoot();
973
+ await act(() => {
974
+ root.render(
975
+ <Provider>
976
+ <Suspense fallback={<Text text="Loading" />}>
977
+ <Consumer />
978
+ </Suspense>
979
+ </Provider>,
980
+ );
981
+ });
982
+ assertLog(['Loading']);
983
+ expect(root).toMatchRenderedOutput('Loading');
984
+
985
+ await act(() => {
986
+ setPromise(resolvedThenable);
987
+ });
988
+ assertLog(['Result']);
989
+ expect(root).toMatchRenderedOutput('Result');
990
+ });
991
+
992
+ it('regression: context change triggers retry of suspended Suspense boundary on initial mount (nested)', async () => {
993
+ // Same as above, but with an additional indirection component between
994
+ // the provider and the Suspense boundary. This exercises the
995
+ // propagateContextChanges walker path rather than the
996
+ // propagateParentContextChanges path.
997
+ const Context = React.createContext(null);
998
+ const neverResolvingPromise = new Promise(() => {});
999
+ const resolvedThenable = {status: 'fulfilled', value: 'Result', then() {}};
1000
+
1001
+ function Consumer() {
1002
+ return <Text text={use(use(Context))} />;
1003
+ }
1004
+
1005
+ function Indirection({children}) {
1006
+ Scheduler.log('Indirection');
1007
+ return children;
1008
+ }
1009
+
1010
+ let setPromise;
1011
+ function Provider({children}) {
1012
+ const [promise, _setPromise] = useState(neverResolvingPromise);
1013
+ setPromise = _setPromise;
1014
+ return <Context.Provider value={promise}>{children}</Context.Provider>;
1015
+ }
1016
+
1017
+ const root = ReactNoop.createRoot();
1018
+ await act(() => {
1019
+ root.render(
1020
+ <Provider>
1021
+ <Indirection>
1022
+ <Suspense fallback={<Text text="Loading" />}>
1023
+ <Consumer />
1024
+ </Suspense>
1025
+ </Indirection>
1026
+ </Provider>,
1027
+ );
1028
+ });
1029
+ assertLog(['Indirection', 'Loading']);
1030
+ expect(root).toMatchRenderedOutput('Loading');
1031
+
1032
+ // Indirection should not re-render — only the Suspense boundary
1033
+ // should be retried.
1034
+ await act(() => {
1035
+ setPromise(resolvedThenable);
1036
+ });
1037
+ assertLog(['Result']);
1038
+ expect(root).toMatchRenderedOutput('Result');
1039
+ });
1040
});