@samitouri / QOS-React-2 / commits / 5d7e8b90e2

[DevTools] Use use() instead of throwing a Promise in Caches (#34033)

Sebastian Markbåge committed Jul 29, 2025 at 03:45 UTC 5d7e8b90e29c0f3c995b52d493a0f70dd7d72f3f
19 files changed +743 -704
packages/react-devtools-shared/src/__tests__/preprocessData-test.js
+51 -90
@@ -857,7 +857,7 @@ describe('Timeline profiler', () => {
857 {
858 "batchUID": 0,
859 "depth": 0,
860 - "duration": 0.014,
860 + "duration": 0.012,
861 "lanes": "0b0000000000000000000000000000101",
862 "timestamp": 0.008,
863 "type": "render-idle",
@@ -873,25 +873,17 @@ describe('Timeline profiler', () => {
873 {
874 "batchUID": 0,
875 "depth": 0,
876 - "duration": 0.010,
876 + "duration": 0.008,
877 "lanes": "0b0000000000000000000000000000101",
878 "timestamp": 0.012,
879 "type": "commit",
880 },
881 - {
882 - "batchUID": 0,
883 - "depth": 1,
884 - "duration": 0.001,
885 - "lanes": "0b0000000000000000000000000000101",
886 - "timestamp": 0.02,
887 - "type": "layout-effects",
888 - },
881 {
882 "batchUID": 0,
883 "depth": 0,
884 "duration": 0.004,
885 "lanes": "0b0000000000000000000000000000101",
894 - "timestamp": 0.023,
886 + "timestamp": 0.021,
887 "type": "passive-effects",
888 },
889 ],
@@ -899,9 +891,9 @@ describe('Timeline profiler', () => {
891 {
892 "batchUID": 1,
893 "depth": 0,
902 - "duration": 0.014,
894 + "duration": 0.012,
895 "lanes": "0b0000000000000000000000000000101",
904 - "timestamp": 0.028,
896 + "timestamp": 0.026,
897 "type": "render-idle",
898 },
899 {
@@ -909,31 +901,23 @@ describe('Timeline profiler', () => {
901 "depth": 0,
902 "duration": 0.003,
903 "lanes": "0b0000000000000000000000000000101",
912 - "timestamp": 0.028,
904 + "timestamp": 0.026,
905 "type": "render",
906 },
907 {
908 "batchUID": 1,
909 "depth": 0,
918 - "duration": 0.010,
910 + "duration": 0.008,
911 "lanes": "0b0000000000000000000000000000101",
920 - "timestamp": 0.032,
912 + "timestamp": 0.03,
913 "type": "commit",
914 },
923 - {
924 - "batchUID": 1,
925 - "depth": 1,
926 - "duration": 0.001,
927 - "lanes": "0b0000000000000000000000000000101",
928 - "timestamp": 0.04,
929 - "type": "layout-effects",
930 - },
915 {
916 "batchUID": 1,
917 "depth": 0,
918 "duration": 0.003,
919 "lanes": "0b0000000000000000000000000000101",
936 - "timestamp": 0.043,
920 + "timestamp": 0.039,
921 "type": "passive-effects",
922 },
923 ],
@@ -949,26 +933,26 @@ describe('Timeline profiler', () => {
933 {
934 "componentName": "App",
935 "duration": 0.002,
952 - "timestamp": 0.024,
936 + "timestamp": 0.022,
937 "type": "passive-effect-mount",
938 "warning": null,
939 },
940 {
941 "componentName": "App",
942 "duration": 0.001,
959 - "timestamp": 0.029,
943 + "timestamp": 0.027,
944 "type": "render",
945 "warning": null,
946 },
947 {
948 "componentName": "App",
949 "duration": 0.001,
966 - "timestamp": 0.044,
950 + "timestamp": 0.04,
951 "type": "passive-effect-mount",
952 "warning": null,
953 },
954 ],
971 - "duration": 0.046,
955 + "duration": 0.042,
956 "flamechart": [],
957 "internalModuleSourceToRanges": Map {
958 undefined => [
@@ -1031,7 +1015,7 @@ describe('Timeline profiler', () => {
1015 {
1016 "batchUID": 0,
1017 "depth": 0,
1034 - "duration": 0.014,
1018 + "duration": 0.012,
1019 "lanes": "0b0000000000000000000000000000101",
1020 "timestamp": 0.008,
1021 "type": "render-idle",
@@ -1047,33 +1031,25 @@ describe('Timeline profiler', () => {
1031 {
1032 "batchUID": 0,
1033 "depth": 0,
1050 - "duration": 0.010,
1034 + "duration": 0.008,
1035 "lanes": "0b0000000000000000000000000000101",
1036 "timestamp": 0.012,
1037 "type": "commit",
1038 },
1055 - {
1056 - "batchUID": 0,
1057 - "depth": 1,
1058 - "duration": 0.001,
1059 - "lanes": "0b0000000000000000000000000000101",
1060 - "timestamp": 0.02,
1061 - "type": "layout-effects",
1062 - },
1039 {
1040 "batchUID": 0,
1041 "depth": 0,
1042 "duration": 0.004,
1043 "lanes": "0b0000000000000000000000000000101",
1068 - "timestamp": 0.023,
1044 + "timestamp": 0.021,
1045 "type": "passive-effects",
1046 },
1047 {
1048 "batchUID": 1,
1049 "depth": 0,
1074 - "duration": 0.014,
1050 + "duration": 0.012,
1051 "lanes": "0b0000000000000000000000000000101",
1076 - "timestamp": 0.028,
1052 + "timestamp": 0.026,
1053 "type": "render-idle",
1054 },
1055 {
@@ -1081,31 +1057,23 @@ describe('Timeline profiler', () => {
1057 "depth": 0,
1058 "duration": 0.003,
1059 "lanes": "0b0000000000000000000000000000101",
1084 - "timestamp": 0.028,
1060 + "timestamp": 0.026,
1061 "type": "render",
1062 },
1063 {
1064 "batchUID": 1,
1065 "depth": 0,
1090 - "duration": 0.010,
1066 + "duration": 0.008,
1067 "lanes": "0b0000000000000000000000000000101",
1092 - "timestamp": 0.032,
1068 + "timestamp": 0.03,
1069 "type": "commit",
1070 },
1095 - {
1096 - "batchUID": 1,
1097 - "depth": 1,
1098 - "duration": 0.001,
1099 - "lanes": "0b0000000000000000000000000000101",
1100 - "timestamp": 0.04,
1101 - "type": "layout-effects",
1102 - },
1071 {
1072 "batchUID": 1,
1073 "depth": 0,
1074 "duration": 0.003,
1075 "lanes": "0b0000000000000000000000000000101",
1108 - "timestamp": 0.043,
1076 + "timestamp": 0.039,
1077 "type": "passive-effects",
1078 },
1079 ],
@@ -1149,7 +1117,7 @@ describe('Timeline profiler', () => {
1117 {
1118 "componentName": "App",
1119 "lanes": "0b0000000000000000000000000000101",
1152 - "timestamp": 0.025,
1120 + "timestamp": 0.023,
1121 "type": "schedule-state-update",
1122 "warning": null,
1123 },
@@ -1254,6 +1222,15 @@ describe('Timeline profiler', () => {
1222 let promise = null;
1223 let resolvedValue = null;
1224 function readValue(value) {
1225 + if (React.use) {
1226 + if (promise === null) {
1227 + promise = Promise.resolve(true).then(() => {
1228 + return value;
1229 + });
1230 + promise.displayName = 'Testing displayName';
1231 + }
1232 + return React.use(promise);
1233 + }
1234 if (resolvedValue !== null) {
1235 return resolvedValue;
1236 } else if (promise === null) {
@@ -1273,7 +1250,7 @@ describe('Timeline profiler', () => {
1250 const testMarks = [creactCpuProfilerSample()];
1251
1252 const root = ReactDOMClient.createRoot(document.createElement('div'));
1276 - utils.act(() =>
1253 + await utils.actAsync(() =>
1254 root.render(
1255 <React.Suspense fallback="Loading...">
1256 <Component />
@@ -1823,6 +1800,14 @@ describe('Timeline profiler', () => {
1800 let promise = null;
1801 let resolvedValue = null;
1802 function readValue(value) {
1803 + if (React.use) {
1804 + if (promise === null) {
1805 + promise = Promise.resolve(true).then(() => {
1806 + return value;
1807 + });
1808 + }
1809 + return React.use(promise);
1810 + }
1811 if (resolvedValue !== null) {
1812 return resolvedValue;
1813 } else if (promise === null) {
@@ -1881,6 +1866,14 @@ describe('Timeline profiler', () => {
1866 let promise = null;
1867 let resolvedValue = null;
1868 function readValue(value) {
1869 + if (React.use) {
1870 + if (promise === null) {
1871 + promise = Promise.resolve(true).then(() => {
1872 + return value;
1873 + });
1874 + }
1875 + return React.use(promise);
1876 + }
1877 if (resolvedValue !== null) {
1878 return resolvedValue;
1879 } else if (promise === null) {
@@ -2192,14 +2185,6 @@ describe('Timeline profiler', () => {
2185 "timestamp": 10,
2186 "type": "commit",
2187 },
2195 - {
2196 - "batchUID": 1,
2197 - "depth": 1,
2198 - "duration": 0,
2199 - "lanes": "0b0000000000000000000000000100000",
2200 - "timestamp": 10,
2201 - "type": "layout-effects",
2202 - },
2188 {
2189 "batchUID": 1,
2190 "depth": 0,
@@ -2234,14 +2219,6 @@ describe('Timeline profiler', () => {
2219 "timestamp": 10,
2220 "type": "commit",
2221 },
2237 - {
2238 - "batchUID": 2,
2239 - "depth": 1,
2240 - "duration": 0,
2241 - "lanes": "0b0000000000000000000000000100000",
2242 - "timestamp": 10,
2243 - "type": "layout-effects",
2244 - },
2222 {
2223 "batchUID": 2,
2224 "depth": 0,
@@ -2292,8 +2269,8 @@ describe('Timeline profiler', () => {
2269 8 => "InputContinuous",
2270 16 => "DefaultHydration",
2271 32 => "Default",
2295 - 64 => "TransitionHydration",
2296 - 128 => "Transition",
2272 + 64 => undefined,
2273 + 128 => "TransitionHydration",
2274 256 => "Transition",
2275 512 => "Transition",
2276 1024 => "Transition",
@@ -2349,14 +2326,6 @@ describe('Timeline profiler', () => {
2326 "timestamp": 10,
2327 "type": "commit",
2328 },
2352 - {
2353 - "batchUID": 1,
2354 - "depth": 1,
2355 - "duration": 0,
2356 - "lanes": "0b0000000000000000000000000100000",
2357 - "timestamp": 10,
2358 - "type": "layout-effects",
2359 - },
2329 {
2330 "batchUID": 1,
2331 "depth": 0,
@@ -2389,14 +2358,6 @@ describe('Timeline profiler', () => {
2358 "timestamp": 10,
2359 "type": "commit",
2360 },
2392 - {
2393 - "batchUID": 2,
2394 - "depth": 1,
2395 - "duration": 0,
2396 - "lanes": "0b0000000000000000000000000100000",
2397 - "timestamp": 10,
2398 - "type": "layout-effects",
2399 - },
2361 {
2362 "batchUID": 2,
2363 "depth": 0,
packages/react-devtools-shared/src/__tests__/profilerStore-test.js
+5 -1
@@ -215,7 +215,11 @@ describe('ProfilerStore', () => {
215 it('should not throw while initializing context values for Fibers within a not-yet-mounted subtree', () => {
216 const promise = new Promise(resolve => {});
217 const SuspendingView = () => {
218 - throw promise;
218 + if (React.use) {
219 + React.use(promise);
220 + } else {
221 + throw promise;
222 + }
223 };
224
225 const App = () => {
packages/react-devtools-shared/src/__tests__/profilingCache-test.js
+8
@@ -682,6 +682,14 @@ describe('ProfilingCache', () => {
682 it('should calculate durations correctly for suspended views', async () => {
683 let data;
684 const getData = () => {
685 + if (React.use) {
686 + if (!data) {
687 + data = new Promise(resolve => {
688 + resolve('abc');
689 + });
690 + }
691 + return React.use(data);
692 + }
693 if (data) {
694 return data;
695 } else {
packages/react-devtools-shared/src/__tests__/store-test.js
+226 -186
@@ -44,10 +44,10 @@ describe('Store', () => {
44 const {render, unmount, createContainer} = getVersionedRenderImplementation();
45
46 // @reactVersion >= 18.0
47 - it('should not allow a root node to be collapsed', () => {
47 + it('should not allow a root node to be collapsed', async () => {
48 const Component = () => <div>Hi</div>;
49
50 - act(() => render(<Component count={4} />));
50 + await act(() => render(<Component count={4} />));
51 expect(store).toMatchInlineSnapshot(`
52 [root]
53 <Component>
@@ -63,16 +63,16 @@ describe('Store', () => {
63 });
64
65 // @reactVersion >= 18.0
66 - it('should properly handle a root with no visible nodes', () => {
66 + it('should properly handle a root with no visible nodes', async () => {
67 const Root = ({children}) => children;
68
69 - act(() => render(<Root>{null}</Root>));
69 + await act(() => render(<Root>{null}</Root>));
70 expect(store).toMatchInlineSnapshot(`
71 [root]
72 <Root>
73 `);
74
75 - act(() => render(<div />));
75 + await act(() => render(<div />));
76 expect(store).toMatchInlineSnapshot(`[root]`);
77 });
78
@@ -82,20 +82,24 @@ describe('Store', () => {
82 // I'mnot yet sure of how to reduce the GitHub reported production case to a test though.
83 // See https://github.com/facebook/react/issues/21445
84 // @reactVersion >= 18.0
85 - it('should handle when a component mounts before its owner', () => {
85 + it('should handle when a component mounts before its owner', async () => {
86 const promise = new Promise(resolve => {});
87
88 let Dynamic = null;
89 const Owner = () => {
90 Dynamic = <Child />;
91 - throw promise;
91 + if (React.use) {
92 + React.use(promise);
93 + } else {
94 + throw promise;
95 + }
96 };
97 const Parent = () => {
98 return Dynamic;
99 };
100 const Child = () => null;
101
98 - act(() =>
102 + await act(() =>
103 render(
104 <>
105 <React.Suspense fallback="Loading...">
@@ -114,11 +118,11 @@ describe('Store', () => {
118 });
119
120 // @reactVersion >= 18.0
117 - it('should handle multibyte character strings', () => {
121 + it('should handle multibyte character strings', async () => {
122 const Component = () => null;
123 Component.displayName = '🟩💜🔵';
124
121 - act(() => render(<Component />));
125 + await act(() => render(<Component />));
126 expect(store).toMatchInlineSnapshot(`
127 [root]
128 <🟩💜🔵>
@@ -126,7 +130,7 @@ describe('Store', () => {
130 });
131
132 describe('StrictMode compliance', () => {
129 - it('should mark strict root elements as strict', () => {
133 + it('should mark strict root elements as strict', async () => {
134 const App = () => <Component />;
135 const Component = () => null;
136
@@ -134,7 +138,7 @@ describe('Store', () => {
138 const root = ReactDOMClient.createRoot(container, {
139 unstable_strictMode: true,
140 });
137 - act(() => {
141 + await act(() => {
142 root.render(<App />);
143 });
144
@@ -143,13 +147,13 @@ describe('Store', () => {
147 });
148
149 // @reactVersion >= 18.0
146 - it('should mark non strict root elements as not strict', () => {
150 + it('should mark non strict root elements as not strict', async () => {
151 const App = () => <Component />;
152 const Component = () => null;
153
154 const container = document.createElement('div');
155 const root = ReactDOMClient.createRoot(container);
152 - act(() => {
156 + await act(() => {
157 root.render(<App />);
158 });
159
@@ -157,7 +161,7 @@ describe('Store', () => {
161 expect(store.getElementAtIndex(1).isStrictModeNonCompliant).toBe(true);
162 });
163
160 - it('should mark StrictMode subtree elements as strict', () => {
164 + it('should mark StrictMode subtree elements as strict', async () => {
165 const App = () => (
166 <React.StrictMode>
167 <Component />
@@ -167,7 +171,7 @@ describe('Store', () => {
171
172 const container = document.createElement('div');
173 const root = ReactDOMClient.createRoot(container);
170 - act(() => {
174 + await act(() => {
175 root.render(<App />);
176 });
177
@@ -182,7 +186,7 @@ describe('Store', () => {
186 });
187
188 // @reactVersion >= 18.0
185 - it('should support mount and update operations', () => {
189 + it('should support mount and update operations', async () => {
190 const Grandparent = ({count}) => (
191 <React.Fragment>
192 <Parent count={count} />
@@ -193,7 +197,7 @@ describe('Store', () => {
197 new Array(count).fill(true).map((_, index) => <Child key={index} />);
198 const Child = () => <div>Hi!</div>;
199
196 - act(() => render(<Grandparent count={4} />));
200 + await act(() => render(<Grandparent count={4} />));
201 expect(store).toMatchInlineSnapshot(`
202 [root]
203 ▾ <Grandparent>
@@ -209,7 +213,7 @@ describe('Store', () => {
213 <Child key="3">
214 `);
215
212 - act(() => render(<Grandparent count={2} />));
216 + await act(() => render(<Grandparent count={2} />));
217 expect(store).toMatchInlineSnapshot(`
218 [root]
219 ▾ <Grandparent>
@@ -221,14 +225,14 @@ describe('Store', () => {
225 <Child key="1">
226 `);
227
224 - act(() => unmount());
228 + await act(() => unmount());
229 expect(store).toMatchInlineSnapshot(``);
230 });
231
232 // @reactVersion >= 18.0
233 // @reactVersion < 19
234 // @gate !disableLegacyMode
231 - it('should support mount and update operations for multiple roots (legacy render)', () => {
235 + it('should support mount and update operations for multiple roots (legacy render)', async () => {
236 const Parent = ({count}) =>
237 new Array(count).fill(true).map((_, index) => <Child key={index} />);
238 const Child = () => <div>Hi!</div>;
@@ -236,7 +240,7 @@ describe('Store', () => {
240 const containerA = document.createElement('div');
241 const containerB = document.createElement('div');
242
239 - act(() => {
243 + await act(() => {
244 legacyRender(<Parent key="A" count={3} />, containerA);
245 legacyRender(<Parent key="B" count={2} />, containerB);
246 });
@@ -252,7 +256,7 @@ describe('Store', () => {
256 <Child key="1">
257 `);
258
255 - act(() => {
259 + await act(() => {
260 legacyRender(<Parent key="A" count={4} />, containerA);
261 legacyRender(<Parent key="B" count={1} />, containerB);
262 });
@@ -268,7 +272,7 @@ describe('Store', () => {
272 <Child key="0">
273 `);
274
271 - act(() => ReactDOM.unmountComponentAtNode(containerB));
275 + await act(() => ReactDOM.unmountComponentAtNode(containerB));
276 expect(store).toMatchInlineSnapshot(`
277 [root]
278 ▾ <Parent key="A">
@@ -278,12 +282,12 @@ describe('Store', () => {
282 <Child key="3">
283 `);
284
281 - act(() => ReactDOM.unmountComponentAtNode(containerA));
285 + await act(() => ReactDOM.unmountComponentAtNode(containerA));
286 expect(store).toMatchInlineSnapshot(``);
287 });
288
289 // @reactVersion >= 18.0
286 - it('should support mount and update operations for multiple roots (createRoot)', () => {
290 + it('should support mount and update operations for multiple roots (createRoot)', async () => {
291 const Parent = ({count}) =>
292 new Array(count).fill(true).map((_, index) => <Child key={index} />);
293 const Child = () => <div>Hi!</div>;
@@ -294,7 +298,7 @@ describe('Store', () => {
298 const rootA = ReactDOMClient.createRoot(containerA);
299 const rootB = ReactDOMClient.createRoot(containerB);
300
297 - act(() => {
301 + await act(() => {
302 rootA.render(<Parent key="A" count={3} />);
303 rootB.render(<Parent key="B" count={2} />);
304 });
@@ -310,7 +314,7 @@ describe('Store', () => {
314 <Child key="1">
315 `);
316
313 - act(() => {
317 + await act(() => {
318 rootA.render(<Parent key="A" count={4} />);
319 rootB.render(<Parent key="B" count={1} />);
320 });
@@ -326,7 +330,7 @@ describe('Store', () => {
330 <Child key="0">
331 `);
332
329 - act(() => rootB.unmount());
333 + await act(() => rootB.unmount());
334 expect(store).toMatchInlineSnapshot(`
335 [root]
336 ▾ <Parent key="A">
@@ -336,12 +340,12 @@ describe('Store', () => {
340 <Child key="3">
341 `);
342
339 - act(() => rootA.unmount());
343 + await act(() => rootA.unmount());
344 expect(store).toMatchInlineSnapshot(``);
345 });
346
347 // @reactVersion >= 18.0
344 - it('should filter DOM nodes from the store tree', () => {
348 + it('should filter DOM nodes from the store tree', async () => {
349 const Grandparent = () => (
350 <div>
351 <div>
@@ -357,7 +361,7 @@ describe('Store', () => {
361 );
362 const Child = () => <div>Hi!</div>;
363
360 - act(() => render(<Grandparent count={4} />));
364 + await act(() => render(<Grandparent count={4} />));
365 expect(store).toMatchInlineSnapshot(`
366 [root]
367 ▾ <Grandparent>
@@ -369,10 +373,14 @@ describe('Store', () => {
373 });
374
375 // @reactVersion >= 18.0
372 - it('should display Suspense nodes properly in various states', () => {
376 + it('should display Suspense nodes properly in various states', async () => {
377 const Loading = () => <div>Loading...</div>;
378 const SuspendingComponent = () => {
375 - throw new Promise(() => {});
379 + if (React.use) {
380 + React.use(new Promise(() => {}));
381 + } else {
382 + throw new Promise(() => {});
383 + }
384 };
385 const Component = () => {
386 return <div>Hello</div>;
@@ -390,7 +398,7 @@ describe('Store', () => {
398 </React.Fragment>
399 );
400
393 - act(() => render(<Wrapper shouldSuspense={true} />));
401 + await act(() => render(<Wrapper shouldSuspense={true} />));
402 expect(store).toMatchInlineSnapshot(`
403 [root]
404 ▾ <Wrapper>
@@ -399,7 +407,7 @@ describe('Store', () => {
407 <Loading>
408 `);
409
402 - act(() => {
410 + await act(() => {
411 render(<Wrapper shouldSuspense={false} />);
412 });
413 expect(store).toMatchInlineSnapshot(`
@@ -412,11 +420,15 @@ describe('Store', () => {
420 });
421
422 // @reactVersion >= 18.0
415 - it('should support nested Suspense nodes', () => {
423 + it('should support nested Suspense nodes', async () => {
424 const Component = () => null;
425 const Loading = () => <div>Loading...</div>;
426 const Never = () => {
419 - throw new Promise(() => {});
427 + if (React.use) {
428 + React.use(new Promise(() => {}));
429 + } else {
430 + throw new Promise(() => {});
431 + }
432 };
433
434 const Wrapper = ({
@@ -451,7 +463,7 @@ describe('Store', () => {
463 </React.Fragment>
464 );
465
454 - act(() =>
466 + await act(() =>
467 render(
468 <Wrapper
469 suspendParent={false}
@@ -474,7 +486,7 @@ describe('Store', () => {
486 <Loading key="Suspense 3 Fallback">
487 <Component key="Unrelated at End">
488 `);
477 - act(() =>
489 + await act(() =>
490 render(
491 <Wrapper
492 suspendParent={false}
@@ -497,7 +509,7 @@ describe('Store', () => {
509 <Loading key="Suspense 3 Fallback">
510 <Component key="Unrelated at End">
511 `);
500 - act(() =>
512 + await act(() =>
513 render(
514 <Wrapper
515 suspendParent={false}
@@ -520,7 +532,7 @@ describe('Store', () => {
532 <Loading key="Suspense 3 Fallback">
533 <Component key="Unrelated at End">
534 `);
523 - act(() =>
535 + await act(() =>
536 render(
537 <Wrapper
538 suspendParent={false}
@@ -543,7 +555,7 @@ describe('Store', () => {
555 <Loading key="Suspense 3 Fallback">
556 <Component key="Unrelated at End">
557 `);
546 - act(() =>
558 + await act(() =>
559 render(
560 <Wrapper
561 suspendParent={true}
@@ -559,7 +571,7 @@ describe('Store', () => {
571 ▾ <Suspense>
572 <Loading key="Parent Fallback">
573 `);
562 - act(() =>
574 + await act(() =>
575 render(
576 <Wrapper
577 suspendParent={false}
@@ -582,7 +594,7 @@ describe('Store', () => {
594 <Loading key="Suspense 3 Fallback">
595 <Component key="Unrelated at End">
596 `);
585 - act(() =>
597 + await act(() =>
598 render(
599 <Wrapper
600 suspendParent={false}
@@ -607,7 +619,7 @@ describe('Store', () => {
619 `);
620
621 const rendererID = getRendererID();
610 - act(() =>
622 + await act(() =>
623 agent.overrideSuspense({
624 id: store.getElementIDAtIndex(4),
625 rendererID,
@@ -628,7 +640,7 @@ describe('Store', () => {
640 <Loading key="Suspense 3 Fallback">
641 <Component key="Unrelated at End">
642 `);
631 - act(() =>
643 + await act(() =>
644 agent.overrideSuspense({
645 id: store.getElementIDAtIndex(2),
646 rendererID,
@@ -642,7 +654,7 @@ describe('Store', () => {
654 ▾ <Suspense>
655 <Loading key="Parent Fallback">
656 `);
645 - act(() =>
657 + await act(() =>
658 render(
659 <Wrapper
660 suspendParent={false}
@@ -658,7 +670,7 @@ describe('Store', () => {
670 ▾ <Suspense>
671 <Loading key="Parent Fallback">
672 `);
661 - act(() =>
673 + await act(() =>
674 agent.overrideSuspense({
675 id: store.getElementIDAtIndex(2),
676 rendererID,
@@ -679,7 +691,7 @@ describe('Store', () => {
691 <Loading key="Suspense 3 Fallback">
692 <Component key="Unrelated at End">
693 `);
682 - act(() =>
694 + await act(() =>
695 agent.overrideSuspense({
696 id: store.getElementIDAtIndex(4),
697 rendererID,
@@ -700,7 +712,7 @@ describe('Store', () => {
712 <Loading key="Suspense 3 Fallback">
713 <Component key="Unrelated at End">
714 `);
703 - act(() =>
715 + await act(() =>
716 render(
717 <Wrapper
718 suspendParent={false}
@@ -725,10 +737,14 @@ describe('Store', () => {
737 `);
738 });
739
728 - it('should display a partially rendered SuspenseList', () => {
740 + it('should display a partially rendered SuspenseList', async () => {
741 const Loading = () => <div>Loading...</div>;
742 const SuspendingComponent = () => {
731 - throw new Promise(() => {});
743 + if (React.use) {
744 + React.use(new Promise(() => {}));
745 + } else {
746 + throw new Promise(() => {});
747 + }
748 };
749 const Component = () => {
750 return <div>Hello</div>;
@@ -747,7 +763,7 @@ describe('Store', () => {
763
764 const container = document.createElement('div');
765 const root = ReactDOMClient.createRoot(container);
750 - act(() => {
766 + await act(() => {
767 root.render(<Wrapper shouldSuspense={true} />);
768 });
769 expect(store).toMatchInlineSnapshot(`
@@ -759,7 +775,7 @@ describe('Store', () => {
775 <Loading>
776 `);
777
762 - act(() => {
778 + await act(() => {
779 root.render(<Wrapper shouldSuspense={false} />);
780 });
781 expect(store).toMatchInlineSnapshot(`
@@ -774,7 +790,7 @@ describe('Store', () => {
790 });
791
792 // @reactVersion >= 18.0
777 - it('should support collapsing parts of the tree', () => {
793 + it('should support collapsing parts of the tree', async () => {
794 const Grandparent = ({count}) => (
795 <React.Fragment>
796 <Parent count={count} />
@@ -785,7 +801,7 @@ describe('Store', () => {
801 new Array(count).fill(true).map((_, index) => <Child key={index} />);
802 const Child = () => <div>Hi!</div>;
803
788 - act(() => render(<Grandparent count={2} />));
804 + await act(() => render(<Grandparent count={2} />));
805 expect(store).toMatchInlineSnapshot(`
806 [root]
807 ▾ <Grandparent>
@@ -801,7 +817,7 @@ describe('Store', () => {
817 const parentOneID = store.getElementIDAtIndex(1);
818 const parentTwoID = store.getElementIDAtIndex(4);
819
804 - act(() => store.toggleIsCollapsed(parentOneID, true));
820 + await act(() => store.toggleIsCollapsed(parentOneID, true));
821 expect(store).toMatchInlineSnapshot(`
822 [root]
823 ▾ <Grandparent>
@@ -811,7 +827,7 @@ describe('Store', () => {
827 <Child key="1">
828 `);
829
814 - act(() => store.toggleIsCollapsed(parentTwoID, true));
830 + await act(() => store.toggleIsCollapsed(parentTwoID, true));
831 expect(store).toMatchInlineSnapshot(`
832 [root]
833 ▾ <Grandparent>
@@ -819,7 +835,7 @@ describe('Store', () => {
835 ▸ <Parent>
836 `);
837
822 - act(() => store.toggleIsCollapsed(parentOneID, false));
838 + await act(() => store.toggleIsCollapsed(parentOneID, false));
839 expect(store).toMatchInlineSnapshot(`
840 [root]
841 ▾ <Grandparent>
@@ -829,13 +845,13 @@ describe('Store', () => {
845 ▸ <Parent>
846 `);
847
832 - act(() => store.toggleIsCollapsed(grandparentID, true));
848 + await act(() => store.toggleIsCollapsed(grandparentID, true));
849 expect(store).toMatchInlineSnapshot(`
850 [root]
851 ▸ <Grandparent>
852 `);
853
838 - act(() => store.toggleIsCollapsed(grandparentID, false));
854 + await act(() => store.toggleIsCollapsed(grandparentID, false));
855 expect(store).toMatchInlineSnapshot(`
856 [root]
857 ▾ <Grandparent>
@@ -847,7 +863,7 @@ describe('Store', () => {
863 });
864
865 // @reactVersion >= 18.0
850 - it('should support reordering of children', () => {
866 + it('should support reordering of children', async () => {
867 const Root = ({children}) => children;
868 const Component = () => null;
869
@@ -856,7 +872,7 @@ describe('Store', () => {
872 const foo = <Foo key="foo" />;
873 const bar = <Bar key="bar" />;
874
859 - act(() => render(<Root>{[foo, bar]}</Root>));
875 + await act(() => render(<Root>{[foo, bar]}</Root>));
876 expect(store).toMatchInlineSnapshot(`
877 [root]
878 ▾ <Root>
@@ -867,7 +883,7 @@ describe('Store', () => {
883 <Component key="1">
884 `);
885
870 - act(() => render(<Root>{[bar, foo]}</Root>));
886 + await act(() => render(<Root>{[bar, foo]}</Root>));
887 expect(store).toMatchInlineSnapshot(`
888 [root]
889 ▾ <Root>
@@ -878,13 +894,17 @@ describe('Store', () => {
894 <Component key="0">
895 `);
896
881 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true));
897 + await act(() =>
898 + store.toggleIsCollapsed(store.getElementIDAtIndex(0), true),
899 + );
900 expect(store).toMatchInlineSnapshot(`
901 [root]
902 ▸ <Root>
903 `);
904
887 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
905 + await act(() =>
906 + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
907 + );
908 expect(store).toMatchInlineSnapshot(`
909 [root]
910 ▾ <Root>
@@ -903,12 +923,12 @@ describe('Store', () => {
923 });
924
925 // @reactVersion >= 18.0
906 - it('should support mount and update operations', () => {
926 + it('should support mount and update operations', async () => {
927 const Parent = ({count}) =>
928 new Array(count).fill(true).map((_, index) => <Child key={index} />);
929 const Child = () => <div>Hi!</div>;
930
911 - act(() =>
931 + await act(() =>
932 render(
933 <React.Fragment>
934 <Parent count={1} />
@@ -922,7 +942,7 @@ describe('Store', () => {
942 ▸ <Parent>
943 `);
944
925 - act(() =>
945 + await act(() =>
946 render(
947 <React.Fragment>
948 <Parent count={2} />
@@ -936,14 +956,14 @@ describe('Store', () => {
956 ▸ <Parent>
957 `);
958
939 - act(() => unmount());
959 + await act(() => unmount());
960 expect(store).toMatchInlineSnapshot(``);
961 });
962
963 // @reactVersion >= 18.0
964 // @reactVersion < 19
965 // @gate !disableLegacyMode
946 - it('should support mount and update operations for multiple roots (legacy render)', () => {
966 + it('should support mount and update operations for multiple roots (legacy render)', async () => {
967 const Parent = ({count}) =>
968 new Array(count).fill(true).map((_, index) => <Child key={index} />);
969 const Child = () => <div>Hi!</div>;
@@ -951,7 +971,7 @@ describe('Store', () => {
971 const containerA = document.createElement('div');
972 const containerB = document.createElement('div');
973
954 - act(() => {
974 + await act(() => {
975 legacyRender(<Parent key="A" count={3} />, containerA);
976 legacyRender(<Parent key="B" count={2} />, containerB);
977 });
@@ -962,7 +982,7 @@ describe('Store', () => {
982 ▸ <Parent key="B">
983 `);
984
965 - act(() => {
985 + await act(() => {
986 legacyRender(<Parent key="A" count={4} />, containerA);
987 legacyRender(<Parent key="B" count={1} />, containerB);
988 });
@@ -973,18 +993,18 @@ describe('Store', () => {
993 ▸ <Parent key="B">
994 `);
995
976 - act(() => ReactDOM.unmountComponentAtNode(containerB));
996 + await act(() => ReactDOM.unmountComponentAtNode(containerB));
997 expect(store).toMatchInlineSnapshot(`
998 [root]
999 ▸ <Parent key="A">
1000 `);
1001
982 - act(() => ReactDOM.unmountComponentAtNode(containerA));
1002 + await act(() => ReactDOM.unmountComponentAtNode(containerA));
1003 expect(store).toMatchInlineSnapshot(``);
1004 });
1005
1006 // @reactVersion >= 18.0
987 - it('should support mount and update operations for multiple roots (createRoot)', () => {
1007 + it('should support mount and update operations for multiple roots (createRoot)', async () => {
1008 const Parent = ({count}) =>
1009 new Array(count).fill(true).map((_, index) => <Child key={index} />);
1010 const Child = () => <div>Hi!</div>;
@@ -995,7 +1015,7 @@ describe('Store', () => {
1015 const rootA = ReactDOMClient.createRoot(containerA);
1016 const rootB = ReactDOMClient.createRoot(containerB);
1017
998 - act(() => {
1018 + await act(() => {
1019 rootA.render(<Parent key="A" count={3} />);
1020 rootB.render(<Parent key="B" count={2} />);
1021 });
@@ -1006,7 +1026,7 @@ describe('Store', () => {
1026 ▸ <Parent key="B">
1027 `);
1028
1009 - act(() => {
1029 + await act(() => {
1030 rootA.render(<Parent key="A" count={4} />);
1031 rootB.render(<Parent key="B" count={1} />);
1032 });
@@ -1017,18 +1037,18 @@ describe('Store', () => {
1037 ▸ <Parent key="B">
1038 `);
1039
1020 - act(() => rootB.unmount());
1040 + await act(() => rootB.unmount());
1041 expect(store).toMatchInlineSnapshot(`
1042 [root]
1043 ▸ <Parent key="A">
1044 `);
1045
1026 - act(() => rootA.unmount());
1046 + await act(() => rootA.unmount());
1047 expect(store).toMatchInlineSnapshot(``);
1048 });
1049
1050 // @reactVersion >= 18.0
1031 - it('should filter DOM nodes from the store tree', () => {
1051 + it('should filter DOM nodes from the store tree', async () => {
1052 const Grandparent = () => (
1053 <div>
1054 <div>
@@ -1044,13 +1064,15 @@ describe('Store', () => {
1064 );
1065 const Child = () => <div>Hi!</div>;
1066
1047 - act(() => render(<Grandparent count={4} />));
1067 + await act(() => render(<Grandparent count={4} />));
1068 expect(store).toMatchInlineSnapshot(`
1069 [root]
1070 ▸ <Grandparent>
1071 `);
1072
1053 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
1073 + await act(() =>
1074 + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
1075 + );
1076 expect(store).toMatchInlineSnapshot(`
1077 [root]
1078 ▾ <Grandparent>
@@ -1058,7 +1080,9 @@ describe('Store', () => {
1080 ▸ <Parent>
1081 `);
1082
1061 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
1083 + await act(() =>
1084 + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false),
1085 + );
1086 expect(store).toMatchInlineSnapshot(`
1087 [root]
1088 ▾ <Grandparent>
@@ -1069,10 +1093,14 @@ describe('Store', () => {
1093 });
1094
1095 // @reactVersion >= 18.0
1072 - it('should display Suspense nodes properly in various states', () => {
1096 + it('should display Suspense nodes properly in various states', async () => {
1097 const Loading = () => <div>Loading...</div>;
1098 const SuspendingComponent = () => {
1075 - throw new Promise(() => {});
1099 + if (React.use) {
1100 + React.use(new Promise(() => {}));
1101 + } else {
1102 + throw new Promise(() => {});
1103 + }
1104 };
1105 const Component = () => {
1106 return <div>Hello</div>;
@@ -1090,15 +1118,19 @@ describe('Store', () => {
1118 </React.Fragment>
1119 );
1120
1093 - act(() => render(<Wrapper shouldSuspense={true} />));
1121 + await act(() => render(<Wrapper shouldSuspense={true} />));
1122 expect(store).toMatchInlineSnapshot(`
1123 [root]
1124 ▸ <Wrapper>
1125 `);
1126
1127 // This test isn't meaningful unless we expand the suspended tree
1100 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
1101 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(2), false));
1128 + await act(() =>
1129 + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
1130 + );
1131 + await act(() =>
1132 + store.toggleIsCollapsed(store.getElementIDAtIndex(2), false),
1133 + );
1134 expect(store).toMatchInlineSnapshot(`
1135 [root]
1136 ▾ <Wrapper>
@@ -1107,7 +1139,7 @@ describe('Store', () => {
1139 <Loading>
1140 `);
1141
1110 - act(() => {
1142 + await act(() => {
1143 render(<Wrapper shouldSuspense={false} />);
1144 });
1145 expect(store).toMatchInlineSnapshot(`
@@ -1120,7 +1152,7 @@ describe('Store', () => {
1152 });
1153
1154 // @reactVersion >= 18.0
1123 - it('should support expanding parts of the tree', () => {
1155 + it('should support expanding parts of the tree', async () => {
1156 const Grandparent = ({count}) => (
1157 <React.Fragment>
1158 <Parent count={count} />
@@ -1131,7 +1163,7 @@ describe('Store', () => {
1163 new Array(count).fill(true).map((_, index) => <Child key={index} />);
1164 const Child = () => <div>Hi!</div>;
1165
1134 - act(() => render(<Grandparent count={2} />));
1166 + await act(() => render(<Grandparent count={2} />));
1167 expect(store).toMatchInlineSnapshot(`
1168 [root]
1169 ▸ <Grandparent>
@@ -1139,7 +1171,7 @@ describe('Store', () => {
1171
1172 const grandparentID = store.getElementIDAtIndex(0);
1173
1142 - act(() => store.toggleIsCollapsed(grandparentID, false));
1174 + await act(() => store.toggleIsCollapsed(grandparentID, false));
1175 expect(store).toMatchInlineSnapshot(`
1176 [root]
1177 ▾ <Grandparent>
@@ -1150,7 +1182,7 @@ describe('Store', () => {
1182 const parentOneID = store.getElementIDAtIndex(1);
1183 const parentTwoID = store.getElementIDAtIndex(2);
1184
1153 - act(() => store.toggleIsCollapsed(parentOneID, false));
1185 + await act(() => store.toggleIsCollapsed(parentOneID, false));
1186 expect(store).toMatchInlineSnapshot(`
1187 [root]
1188 ▾ <Grandparent>
@@ -1160,7 +1192,7 @@ describe('Store', () => {
1192 ▸ <Parent>
1193 `);
1194
1163 - act(() => store.toggleIsCollapsed(parentTwoID, false));
1195 + await act(() => store.toggleIsCollapsed(parentTwoID, false));
1196 expect(store).toMatchInlineSnapshot(`
1197 [root]
1198 ▾ <Grandparent>
@@ -1172,7 +1204,7 @@ describe('Store', () => {
1204 <Child key="1">
1205 `);
1206
1175 - act(() => store.toggleIsCollapsed(parentOneID, true));
1207 + await act(() => store.toggleIsCollapsed(parentOneID, true));
1208 expect(store).toMatchInlineSnapshot(`
1209 [root]
1210 ▾ <Grandparent>
@@ -1182,7 +1214,7 @@ describe('Store', () => {
1214 <Child key="1">
1215 `);
1216
1185 - act(() => store.toggleIsCollapsed(parentTwoID, true));
1217 + await act(() => store.toggleIsCollapsed(parentTwoID, true));
1218 expect(store).toMatchInlineSnapshot(`
1219 [root]
1220 ▾ <Grandparent>
@@ -1190,7 +1222,7 @@ describe('Store', () => {
1222 ▸ <Parent>
1223 `);
1224
1193 - act(() => store.toggleIsCollapsed(grandparentID, true));
1225 + await act(() => store.toggleIsCollapsed(grandparentID, true));
1226 expect(store).toMatchInlineSnapshot(`
1227 [root]
1228 ▸ <Grandparent>
@@ -1198,7 +1230,7 @@ describe('Store', () => {
1230 });
1231
1232 // @reactVersion >= 18.0
1201 - it('should support expanding deep parts of the tree', () => {
1233 + it('should support expanding deep parts of the tree', async () => {
1234 const Wrapper = ({forwardedRef}) => (
1235 <Nested depth={3} forwardedRef={forwardedRef} />
1236 );
@@ -1211,7 +1243,7 @@ describe('Store', () => {
1243
1244 const ref = React.createRef();
1245
1214 - act(() => render(<Wrapper forwardedRef={ref} />));
1246 + await act(() => render(<Wrapper forwardedRef={ref} />));
1247 expect(store).toMatchInlineSnapshot(`
1248 [root]
1249 ▸ <Wrapper>
@@ -1219,7 +1251,7 @@ describe('Store', () => {
1251
1252 const deepestedNodeID = agent.getIDForHostInstance(ref.current);
1253
1222 - act(() => store.toggleIsCollapsed(deepestedNodeID, false));
1254 + await act(() => store.toggleIsCollapsed(deepestedNodeID, false));
1255 expect(store).toMatchInlineSnapshot(`
1256 [root]
1257 ▾ <Wrapper>
@@ -1231,13 +1263,13 @@ describe('Store', () => {
1263
1264 const rootID = store.getElementIDAtIndex(0);
1265
1234 - act(() => store.toggleIsCollapsed(rootID, true));
1266 + await act(() => store.toggleIsCollapsed(rootID, true));
1267 expect(store).toMatchInlineSnapshot(`
1268 [root]
1269 ▸ <Wrapper>
1270 `);
1271
1240 - act(() => store.toggleIsCollapsed(rootID, false));
1272 + await act(() => store.toggleIsCollapsed(rootID, false));
1273 expect(store).toMatchInlineSnapshot(`
1274 [root]
1275 ▾ <Wrapper>
@@ -1249,14 +1281,14 @@ describe('Store', () => {
1281
1282 const id = store.getElementIDAtIndex(1);
1283
1252 - act(() => store.toggleIsCollapsed(id, true));
1284 + await act(() => store.toggleIsCollapsed(id, true));
1285 expect(store).toMatchInlineSnapshot(`
1286 [root]
1287 ▾ <Wrapper>
1288 ▸ <Nested>
1289 `);
1290
1259 - act(() => store.toggleIsCollapsed(id, false));
1291 + await act(() => store.toggleIsCollapsed(id, false));
1292 expect(store).toMatchInlineSnapshot(`
1293 [root]
1294 ▾ <Wrapper>
@@ -1268,7 +1300,7 @@ describe('Store', () => {
1300 });
1301
1302 // @reactVersion >= 18.0
1271 - it('should support reordering of children', () => {
1303 + it('should support reordering of children', async () => {
1304 const Root = ({children}) => children;
1305 const Component = () => null;
1306
@@ -1277,19 +1309,21 @@ describe('Store', () => {
1309 const foo = <Foo key="foo" />;
1310 const bar = <Bar key="bar" />;
1311
1280 - act(() => render(<Root>{[foo, bar]}</Root>));
1312 + await act(() => render(<Root>{[foo, bar]}</Root>));
1313 expect(store).toMatchInlineSnapshot(`
1314 [root]
1315 ▸ <Root>
1316 `);
1317
1286 - act(() => render(<Root>{[bar, foo]}</Root>));
1318 + await act(() => render(<Root>{[bar, foo]}</Root>));
1319 expect(store).toMatchInlineSnapshot(`
1320 [root]
1321 ▸ <Root>
1322 `);
1323
1292 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
1324 + await act(() =>
1325 + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
1326 + );
1327 expect(store).toMatchInlineSnapshot(`
1328 [root]
1329 ▾ <Root>
@@ -1297,7 +1331,7 @@ describe('Store', () => {
1331 ▸ <Foo key="foo">
1332 `);
1333
1300 - act(() => {
1334 + await act(() => {
1335 store.toggleIsCollapsed(store.getElementIDAtIndex(2), false);
1336 store.toggleIsCollapsed(store.getElementIDAtIndex(1), false);
1337 });
@@ -1311,7 +1345,9 @@ describe('Store', () => {
1345 <Component key="0">
1346 `);
1347
1314 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), true));
1348 + await act(() =>
1349 + store.toggleIsCollapsed(store.getElementIDAtIndex(0), true),
1350 + );
1351 expect(store).toMatchInlineSnapshot(`
1352 [root]
1353 ▸ <Root>
@@ -1319,7 +1355,7 @@ describe('Store', () => {
1355 });
1356
1357 // @reactVersion >= 18.0
1322 - it('should not add new nodes when suspense is toggled', () => {
1358 + it('should not add new nodes when suspense is toggled', async () => {
1359 const SuspenseTree = () => {
1360 return (
1361 <React.Suspense fallback={<Fallback>Loading outer</Fallback>}>
@@ -1332,14 +1368,18 @@ describe('Store', () => {
1368 const Parent = () => <Child />;
1369 const Child = () => null;
1370
1335 - act(() => render(<SuspenseTree />));
1371 + await act(() => render(<SuspenseTree />));
1372 expect(store).toMatchInlineSnapshot(`
1373 [root]
1374 ▸ <SuspenseTree>
1375 `);
1376
1341 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
1342 - act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
1377 + await act(() =>
1378 + store.toggleIsCollapsed(store.getElementIDAtIndex(0), false),
1379 + );
1380 + await act(() =>
1381 + store.toggleIsCollapsed(store.getElementIDAtIndex(1), false),
1382 + );
1383 expect(store).toMatchInlineSnapshot(`
1384 [root]
1385 ▾ <SuspenseTree>
@@ -1350,7 +1390,7 @@ describe('Store', () => {
1390 const rendererID = getRendererID();
1391 const suspenseID = store.getElementIDAtIndex(1);
1392
1353 - act(() =>
1393 + await act(() =>
1394 agent.overrideSuspense({
1395 id: suspenseID,
1396 rendererID,
@@ -1364,7 +1404,7 @@ describe('Store', () => {
1404 <Fallback>
1405 `);
1406
1367 - act(() =>
1407 + await act(() =>
1408 agent.overrideSuspense({
1409 id: suspenseID,
1410 rendererID,
@@ -1386,7 +1426,7 @@ describe('Store', () => {
1426 });
1427
1428 // @reactVersion >= 18.0
1389 - it('should support a single root with a single child', () => {
1429 + it('should support a single root with a single child', async () => {
1430 const Grandparent = () => (
1431 <React.Fragment>
1432 <Parent />
@@ -1396,7 +1436,7 @@ describe('Store', () => {
1436 const Parent = () => <Child />;
1437 const Child = () => null;
1438
1399 - act(() => render(<Grandparent />));
1439 + await act(() => render(<Grandparent />));
1440
1441 for (let i = 0; i < store.numElements; i++) {
1442 expect(store.getIndexOfElementID(store.getElementIDAtIndex(i))).toBe(i);
@@ -1404,12 +1444,12 @@ describe('Store', () => {
1444 });
1445
1446 // @reactVersion >= 18.0
1407 - it('should support multiple roots with one children each', () => {
1447 + it('should support multiple roots with one children each', async () => {
1448 const Grandparent = () => <Parent />;
1449 const Parent = () => <Child />;
1450 const Child = () => null;
1451
1412 - act(() => {
1452 + await act(() => {
1453 render(<Grandparent />);
1454 render(<Grandparent />);
1455 });
@@ -1420,12 +1460,12 @@ describe('Store', () => {
1460 });
1461
1462 // @reactVersion >= 18.0
1423 - it('should support a single root with multiple top level children', () => {
1463 + it('should support a single root with multiple top level children', async () => {
1464 const Grandparent = () => <Parent />;
1465 const Parent = () => <Child />;
1466 const Child = () => null;
1467
1428 - act(() =>
1468 + await act(() =>
1469 render(
1470 <React.Fragment>
1471 <Grandparent />
@@ -1440,12 +1480,12 @@ describe('Store', () => {
1480 });
1481
1482 // @reactVersion >= 18.0
1443 - it('should support multiple roots with multiple top level children', () => {
1483 + it('should support multiple roots with multiple top level children', async () => {
1484 const Grandparent = () => <Parent />;
1485 const Parent = () => <Child />;
1486 const Child = () => null;
1487
1448 - act(() => {
1488 + await act(() => {
1489 render(
1490 <React.Fragment>
1491 <Grandparent />
@@ -1472,7 +1512,7 @@ describe('Store', () => {
1512 // @reactVersion >= 18.0
1513 // @reactVersion < 19
1514 // @gate !disableLegacyMode
1475 - it('detects and updates profiling support based on the attached roots (legacy render)', () => {
1515 + it('detects and updates profiling support based on the attached roots (legacy render)', async () => {
1516 const Component = () => null;
1517
1518 const containerA = document.createElement('div');
@@ -1480,19 +1520,19 @@ describe('Store', () => {
1520
1521 expect(store.rootSupportsBasicProfiling).toBe(false);
1522
1483 - act(() => legacyRender(<Component />, containerA));
1523 + await act(() => legacyRender(<Component />, containerA));
1524 expect(store.rootSupportsBasicProfiling).toBe(true);
1525
1486 - act(() => legacyRender(<Component />, containerB));
1487 - act(() => ReactDOM.unmountComponentAtNode(containerA));
1526 + await act(() => legacyRender(<Component />, containerB));
1527 + await act(() => ReactDOM.unmountComponentAtNode(containerA));
1528 expect(store.rootSupportsBasicProfiling).toBe(true);
1529
1490 - act(() => ReactDOM.unmountComponentAtNode(containerB));
1530 + await act(() => ReactDOM.unmountComponentAtNode(containerB));
1531 expect(store.rootSupportsBasicProfiling).toBe(false);
1532 });
1533
1534 // @reactVersion >= 18
1495 - it('detects and updates profiling support based on the attached roots (createRoot)', () => {
1535 + it('detects and updates profiling support based on the attached roots (createRoot)', async () => {
1536 const Component = () => null;
1537
1538 const containerA = document.createElement('div');
@@ -1503,26 +1543,26 @@ describe('Store', () => {
1543
1544 expect(store.rootSupportsBasicProfiling).toBe(false);
1545
1506 - act(() => rootA.render(<Component />));
1546 + await act(() => rootA.render(<Component />));
1547 expect(store.rootSupportsBasicProfiling).toBe(true);
1548
1509 - act(() => rootB.render(<Component />));
1510 - act(() => rootA.unmount());
1549 + await act(() => rootB.render(<Component />));
1550 + await act(() => rootA.unmount());
1551 expect(store.rootSupportsBasicProfiling).toBe(true);
1552
1513 - act(() => rootB.unmount());
1553 + await act(() => rootB.unmount());
1554 expect(store.rootSupportsBasicProfiling).toBe(false);
1555 });
1556
1557 // @reactVersion >= 18.0
1518 - it('should properly serialize non-string key values', () => {
1558 + it('should properly serialize non-string key values', async () => {
1559 const Child = () => null;
1560
1561 // Bypass React element's automatic stringifying of keys intentionally.
1562 // This is pretty hacky.
1563 const fauxElement = Object.assign({}, <Child />, {key: 123});
1564
1525 - act(() => render([fauxElement]));
1565 + await act(() => render([fauxElement]));
1566 expect(store).toMatchInlineSnapshot(`
1567 [root]
1568 <Child key="123">
@@ -1581,12 +1621,12 @@ describe('Store', () => {
1621 );
1622
1623 // Render once to start fetching the lazy component
1584 - act(() => render(<App />));
1624 + await act(() => render(<App />));
1625
1626 await Promise.resolve();
1627
1628 // Render again after it resolves
1589 - act(() => render(<App />));
1629 + await act(() => render(<App />));
1630
1631 expect(store).toMatchInlineSnapshot(`
1632 [root]
@@ -1638,7 +1678,7 @@ describe('Store', () => {
1678 const container = document.createElement('div');
1679
1680 // Render once to start fetching the lazy component
1641 - act(() => legacyRender(<App renderChildren={true} />, container));
1681 + await act(() => legacyRender(<App renderChildren={true} />, container));
1682
1683 expect(store).toMatchInlineSnapshot(`
1684 [root]
@@ -1649,7 +1689,7 @@ describe('Store', () => {
1689 await Promise.resolve();
1690
1691 // Render again after it resolves
1652 - act(() => legacyRender(<App renderChildren={true} />, container));
1692 + await act(() => legacyRender(<App renderChildren={true} />, container));
1693
1694 expect(store).toMatchInlineSnapshot(`
1695 [root]
@@ -1659,7 +1699,7 @@ describe('Store', () => {
1699 `);
1700
1701 // Render again to unmount it
1662 - act(() => legacyRender(<App renderChildren={false} />, container));
1702 + await act(() => legacyRender(<App renderChildren={false} />, container));
1703
1704 expect(store).toMatchInlineSnapshot(`
1705 [root]
@@ -1673,7 +1713,7 @@ describe('Store', () => {
1713 const root = ReactDOMClient.createRoot(container);
1714
1715 // Render once to start fetching the lazy component
1676 - act(() => root.render(<App renderChildren={true} />));
1716 + await act(() => root.render(<App renderChildren={true} />));
1717
1718 expect(store).toMatchInlineSnapshot(`
1719 [root]
@@ -1684,7 +1724,7 @@ describe('Store', () => {
1724 await Promise.resolve();
1725
1726 // Render again after it resolves
1687 - act(() => root.render(<App renderChildren={true} />));
1727 + await act(() => root.render(<App renderChildren={true} />));
1728
1729 expect(store).toMatchInlineSnapshot(`
1730 [root]
@@ -1694,7 +1734,7 @@ describe('Store', () => {
1734 `);
1735
1736 // Render again to unmount it
1697 - act(() => root.render(<App renderChildren={false} />));
1737 + await act(() => root.render(<App renderChildren={false} />));
1738
1739 expect(store).toMatchInlineSnapshot(`
1740 [root]
@@ -1709,7 +1749,7 @@ describe('Store', () => {
1749 const container = document.createElement('div');
1750
1751 // Render once to start fetching the lazy component
1712 - act(() => legacyRender(<App renderChildren={true} />, container));
1752 + await act(() => legacyRender(<App renderChildren={true} />, container));
1753
1754 expect(store).toMatchInlineSnapshot(`
1755 [root]
@@ -1718,7 +1758,7 @@ describe('Store', () => {
1758 `);
1759
1760 // Render again to unmount it before it finishes loading
1721 - act(() => legacyRender(<App renderChildren={false} />, container));
1761 + await act(() => legacyRender(<App renderChildren={false} />, container));
1762
1763 expect(store).toMatchInlineSnapshot(`
1764 [root]
@@ -1733,7 +1773,7 @@ describe('Store', () => {
1773 const root = ReactDOMClient.createRoot(container);
1774
1775 // Render once to start fetching the lazy component
1736 - act(() => root.render(<App renderChildren={true} />));
1776 + await act(() => root.render(<App renderChildren={true} />));
1777
1778 expect(store).toMatchInlineSnapshot(`
1779 [root]
@@ -1742,7 +1782,7 @@ describe('Store', () => {
1782 `);
1783
1784 // Render again to unmount it before it finishes loading
1745 - act(() => root.render(<App renderChildren={false} />));
1785 + await act(() => root.render(<App renderChildren={false} />));
1786
1787 expect(store).toMatchInlineSnapshot(`
1788 [root]
@@ -1753,15 +1793,15 @@ describe('Store', () => {
1793
1794 describe('inline errors and warnings', () => {
1795 // @reactVersion >= 18.0
1756 - it('during render are counted', () => {
1796 + it('during render are counted', async () => {
1797 function Example() {
1798 console.error('test-only: render error');
1799 console.warn('test-only: render warning');
1800 return null;
1801 }
1802
1763 - withErrorsOrWarningsIgnored(['test-only:'], () => {
1764 - act(() => render(<Example />));
1803 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
1804 + await act(() => render(<Example />));
1805 });
1806
1807 expect(store).toMatchInlineSnapshot(`
@@ -1770,8 +1810,8 @@ describe('Store', () => {
1810 <Example> ✕⚠
1811 `);
1812
1773 - withErrorsOrWarningsIgnored(['test-only:'], () => {
1774 - act(() => render(<Example rerender={1} />));
1813 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
1814 + await act(() => render(<Example rerender={1} />));
1815 });
1816
1817 expect(store).toMatchInlineSnapshot(`
@@ -1782,7 +1822,7 @@ describe('Store', () => {
1822 });
1823
1824 // @reactVersion >= 18.0
1785 - it('during layout get counted', () => {
1825 + it('during layout get counted', async () => {
1826 function Example() {
1827 React.useLayoutEffect(() => {
1828 console.error('test-only: layout error');
@@ -1791,8 +1831,8 @@ describe('Store', () => {
1831 return null;
1832 }
1833
1794 - withErrorsOrWarningsIgnored(['test-only:'], () => {
1795 - act(() => render(<Example />));
1834 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
1835 + await act(() => render(<Example />));
1836 });
1837
1838 expect(store).toMatchInlineSnapshot(`
@@ -1801,8 +1841,8 @@ describe('Store', () => {
1841 <Example> ✕⚠
1842 `);
1843
1804 - withErrorsOrWarningsIgnored(['test-only:'], () => {
1805 - act(() => render(<Example rerender={1} />));
1844 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
1845 + await act(() => render(<Example rerender={1} />));
1846 });
1847
1848 expect(store).toMatchInlineSnapshot(`
@@ -1818,7 +1858,7 @@ describe('Store', () => {
1858 }
1859
1860 // @reactVersion >= 18.0
1821 - it('are counted (after no delay)', () => {
1861 + it('are counted (after no delay)', async () => {
1862 function Example() {
1863 React.useEffect(() => {
1864 console.error('test-only: passive error');
@@ -1827,8 +1867,8 @@ describe('Store', () => {
1867 return null;
1868 }
1869
1830 - withErrorsOrWarningsIgnored(['test-only:'], () => {
1831 - act(() => {
1870 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
1871 + await act(() => {
1872 render(<Example />);
1873 }, false);
1874 });
@@ -1839,12 +1879,12 @@ describe('Store', () => {
1879 <Example> ✕⚠
1880 `);
1881
1842 - act(() => unmount());
1882 + await act(() => unmount());
1883 expect(store).toMatchInlineSnapshot(``);
1884 });
1885
1886 // @reactVersion >= 18.0
1847 - it('are flushed early when there is a new commit', () => {
1887 + it('are flushed early when there is a new commit', async () => {
1888 function Example() {
1889 React.useEffect(() => {
1890 console.error('test-only: passive error');
@@ -1890,7 +1930,7 @@ describe('Store', () => {
1930 `);
1931 });
1932
1893 - act(() => unmount());
1933 + await act(() => unmount());
1934 expect(store).toMatchInlineSnapshot(``);
1935 });
1936 });
@@ -1898,7 +1938,7 @@ describe('Store', () => {
1938 // In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088
1939 // The warning is moved to the Child instead of the Parent.
1940 // @reactVersion >= 19.0.1
1901 - it('from react get counted [React >= 19.0.1]', () => {
1941 + it('from react get counted [React >= 19.0.1]', async () => {
1942 function Example() {
1943 return [<Child />];
1944 }
@@ -1923,7 +1963,7 @@ describe('Store', () => {
1963
1964 // @reactVersion >= 18.0
1965 // @reactVersion < 19.0
1926 - it('from react get counted [React 18.x]', () => {
1966 + it('from react get counted [React 18.x]', async () => {
1967 function Example() {
1968 return [<Child />];
1969 }
@@ -1947,15 +1987,15 @@ describe('Store', () => {
1987 });
1988
1989 // @reactVersion >= 18.0
1950 - it('can be cleared for the whole app', () => {
1990 + it('can be cleared for the whole app', async () => {
1991 function Example() {
1992 console.error('test-only: render error');
1993 console.warn('test-only: render warning');
1994 return null;
1995 }
1996
1957 - withErrorsOrWarningsIgnored(['test-only:'], () => {
1958 - act(() =>
1997 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
1998 + await act(() =>
1999 render(
2000 <React.Fragment>
2001 <Example />
@@ -1988,15 +2028,15 @@ describe('Store', () => {
2028 });
2029
2030 // @reactVersion >= 18.0
1991 - it('can be cleared for particular Fiber (only warnings)', () => {
2031 + it('can be cleared for particular Fiber (only warnings)', async () => {
2032 function Example() {
2033 console.error('test-only: render error');
2034 console.warn('test-only: render warning');
2035 return null;
2036 }
2037
1998 - withErrorsOrWarningsIgnored(['test-only:'], () => {
1999 - act(() =>
2038 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
2039 + await act(() =>
2040 render(
2041 <React.Fragment>
2042 <Example />
@@ -2033,15 +2073,15 @@ describe('Store', () => {
2073 });
2074
2075 // @reactVersion >= 18.0
2036 - it('can be cleared for a particular Fiber (only errors)', () => {
2076 + it('can be cleared for a particular Fiber (only errors)', async () => {
2077 function Example() {
2078 console.error('test-only: render error');
2079 console.warn('test-only: render warning');
2080 return null;
2081 }
2082
2043 - withErrorsOrWarningsIgnored(['test-only:'], () => {
2044 - act(() =>
2083 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
2084 + await act(() =>
2085 render(
2086 <React.Fragment>
2087 <Example />
@@ -2078,7 +2118,7 @@ describe('Store', () => {
2118 });
2119
2120 // @reactVersion >= 18.0
2081 - it('are updated when fibers are removed from the tree', () => {
2121 + it('are updated when fibers are removed from the tree', async () => {
2122 function ComponentWithWarning() {
2123 console.warn('test-only: render warning');
2124 return null;
@@ -2093,8 +2133,8 @@ describe('Store', () => {
2133 return null;
2134 }
2135
2096 - withErrorsOrWarningsIgnored(['test-only:'], () => {
2097 - act(() =>
2136 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
2137 + await act(() =>
2138 render(
2139 <React.Fragment>
2140 <ComponentWithError />
@@ -2112,8 +2152,8 @@ describe('Store', () => {
2152 <ComponentWithWarningAndError> ✕⚠
2153 `);
2154
2115 - withErrorsOrWarningsIgnored(['test-only:'], () => {
2116 - act(() =>
2155 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
2156 + await act(() =>
2157 render(
2158 <React.Fragment>
2159 <ComponentWithWarning />
@@ -2129,8 +2169,8 @@ describe('Store', () => {
2169 <ComponentWithWarningAndError> ✕⚠
2170 `);
2171
2132 - withErrorsOrWarningsIgnored(['test-only:'], () => {
2133 - act(() =>
2172 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
2173 + await act(() =>
2174 render(
2175 <React.Fragment>
2176 <ComponentWithWarning />
@@ -2144,8 +2184,8 @@ describe('Store', () => {
2184 <ComponentWithWarning> ⚠
2185 `);
2186
2147 - withErrorsOrWarningsIgnored(['test-only:'], () => {
2148 - act(() => render(<React.Fragment />));
2187 + withErrorsOrWarningsIgnored(['test-only:'], async () => {
2188 + await act(() => render(<React.Fragment />));
2189 });
2190 expect(store).toMatchInlineSnapshot(`[root]`);
2191 expect(store.componentWithErrorCount).toBe(0);
packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js
+5 -1
@@ -509,7 +509,11 @@ describe('Store component filters', () => {
509
510 const Component = ({shouldSuspend}) => {
511 if (shouldSuspend) {
512 - throw promise;
512 + if (React.use) {
513 + React.use(promise);
514 + } else {
515 + throw promise;
516 + }
517 }
518 return null;
519 };
packages/react-devtools-shared/src/__tests__/storeStressSync-test.js
+10 -2
@@ -522,7 +522,11 @@ describe('StoreStress (Legacy Mode)', () => {
522 ];
523
524 const Never = () => {
525 - throw new Promise(() => {});
525 + if (React.use) {
526 + React.use(new Promise(() => {}));
527 + } else {
528 + throw new Promise(() => {});
529 + }
530 };
531
532 const Root = ({children}) => {
@@ -1144,7 +1148,11 @@ describe('StoreStress (Legacy Mode)', () => {
1148 ];
1149
1150 const Never = () => {
1147 - throw new Promise(() => {});
1151 + if (React.use) {
1152 + React.use(new Promise(() => {}));
1153 + } else {
1154 + throw new Promise(() => {});
1155 + }
1156 };
1157
1158 const MaybeSuspend = ({children, suspend}) => {
packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
+78 -70
@@ -38,7 +38,7 @@ describe('StoreStressConcurrent', () => {
38 // This is a stress test for the tree mount/update/unmount traversal.
39 // It renders different trees that should produce the same output.
40 // @reactVersion >= 18.0
41 - it('should handle a stress test with different tree operations (Concurrent Mode)', () => {
41 + it('should handle a stress test with different tree operations (Concurrent Mode)', async () => {
42 let setShowX;
43 const A = () => 'a';
44 const B = () => 'b';
@@ -151,26 +151,26 @@ describe('StoreStressConcurrent', () => {
151 root = ReactDOMClient.createRoot(container);
152
153 // Verify mounting 'abcde'.
154 - act(() => root.render(<Parent>{cases[i]}</Parent>));
154 + await act(() => root.render(<Parent>{cases[i]}</Parent>));
155 expect(container.textContent).toMatch('abcde');
156 expect(print(store)).toEqual(snapshotForABCDE);
157
158 // Verify switching to 'abxde'.
159 - act(() => {
159 + await act(() => {
160 setShowX(true);
161 });
162 expect(container.textContent).toMatch('abxde');
163 expect(print(store)).toBe(snapshotForABXDE);
164
165 // Verify switching back to 'abcde'.
166 - act(() => {
166 + await act(() => {
167 setShowX(false);
168 });
169 expect(container.textContent).toMatch('abcde');
170 expect(print(store)).toBe(snapshotForABCDE);
171
172 // Clean up.
173 - act(() => root.unmount());
173 + await act(() => root.unmount());
174 expect(print(store)).toBe('');
175 }
176
@@ -180,19 +180,19 @@ describe('StoreStressConcurrent', () => {
180 root = ReactDOMClient.createRoot(container);
181 for (let i = 0; i < cases.length; i++) {
182 // Verify mounting 'abcde'.
183 - act(() => root.render(<Parent>{cases[i]}</Parent>));
183 + await act(() => root.render(<Parent>{cases[i]}</Parent>));
184 expect(container.textContent).toMatch('abcde');
185 expect(print(store)).toEqual(snapshotForABCDE);
186
187 // Verify switching to 'abxde'.
188 - act(() => {
188 + await act(() => {
189 setShowX(true);
190 });
191 expect(container.textContent).toMatch('abxde');
192 expect(print(store)).toBe(snapshotForABXDE);
193
194 // Verify switching back to 'abcde'.
195 - act(() => {
195 + await act(() => {
196 setShowX(false);
197 });
198 expect(container.textContent).toMatch('abcde');
@@ -204,7 +204,7 @@ describe('StoreStressConcurrent', () => {
204 });
205
206 // @reactVersion >= 18.0
207 - it('should handle stress test with reordering (Concurrent Mode)', () => {
207 + it('should handle stress test with reordering (Concurrent Mode)', async () => {
208 const A = () => 'a';
209 const B = () => 'b';
210 const C = () => 'c';
@@ -245,10 +245,10 @@ describe('StoreStressConcurrent', () => {
245 let container = document.createElement('div');
246 for (let i = 0; i < steps.length; i++) {
247 const root = ReactDOMClient.createRoot(container);
248 - act(() => root.render(<Root>{steps[i]}</Root>));
248 + await act(() => root.render(<Root>{steps[i]}</Root>));
249 // We snapshot each step once so it doesn't regress.
250 snapshots.push(print(store));
251 - act(() => root.unmount());
251 + await act(() => root.unmount());
252 expect(print(store)).toBe('');
253 }
254
@@ -316,13 +316,13 @@ describe('StoreStressConcurrent', () => {
316 for (let j = 0; j < steps.length; j++) {
317 container = document.createElement('div');
318 const root = ReactDOMClient.createRoot(container);
319 - act(() => root.render(<Root>{steps[i]}</Root>));
319 + await act(() => root.render(<Root>{steps[i]}</Root>));
320 expect(print(store)).toMatch(snapshots[i]);
321 - act(() => root.render(<Root>{steps[j]}</Root>));
321 + await act(() => root.render(<Root>{steps[j]}</Root>));
322 expect(print(store)).toMatch(snapshots[j]);
323 - act(() => root.render(<Root>{steps[i]}</Root>));
323 + await act(() => root.render(<Root>{steps[i]}</Root>));
324 expect(print(store)).toMatch(snapshots[i]);
325 - act(() => root.unmount());
325 + await act(() => root.unmount());
326 expect(print(store)).toBe('');
327 }
328 }
@@ -332,7 +332,7 @@ describe('StoreStressConcurrent', () => {
332 for (let j = 0; j < steps.length; j++) {
333 container = document.createElement('div');
334 const root = ReactDOMClient.createRoot(container);
335 - act(() =>
335 + await act(() =>
336 root.render(
337 <Root>
338 <div>{steps[i]}</div>
@@ -340,7 +340,7 @@ describe('StoreStressConcurrent', () => {
340 ),
341 );
342 expect(print(store)).toMatch(snapshots[i]);
343 - act(() =>
343 + await act(() =>
344 root.render(
345 <Root>
346 <div>{steps[j]}</div>
@@ -348,7 +348,7 @@ describe('StoreStressConcurrent', () => {
348 ),
349 );
350 expect(print(store)).toMatch(snapshots[j]);
351 - act(() =>
351 + await act(() =>
352 root.render(
353 <Root>
354 <div>{steps[i]}</div>
@@ -356,7 +356,7 @@ describe('StoreStressConcurrent', () => {
356 ),
357 );
358 expect(print(store)).toMatch(snapshots[i]);
359 - act(() => root.unmount());
359 + await act(() => root.unmount());
360 expect(print(store)).toBe('');
361 }
362 }
@@ -392,7 +392,11 @@ describe('StoreStressConcurrent', () => {
392 ];
393
394 const Never = () => {
395 - throw new Promise(() => {});
395 + if (React.use) {
396 + React.use(new Promise(() => {}));
397 + } else {
398 + throw new Promise(() => {});
399 + }
400 };
401
402 const Root = ({children}) => {
@@ -405,7 +409,7 @@ describe('StoreStressConcurrent', () => {
409 let container = document.createElement('div');
410 for (let i = 0; i < steps.length; i++) {
411 const root = ReactDOMClient.createRoot(container);
408 - act(() =>
412 + await act(() =>
413 root.render(
414 <Root>
415 <X />
@@ -416,7 +420,7 @@ describe('StoreStressConcurrent', () => {
420 );
421 // We snapshot each step once so it doesn't regress.d
422 snapshots.push(print(store));
419 - act(() => root.unmount());
423 + await act(() => root.unmount());
424 expect(print(store)).toBe('');
425 }
426
@@ -507,7 +511,7 @@ describe('StoreStressConcurrent', () => {
511 // 2. Verify check Suspense can render same steps as initial fallback content.
512 for (let i = 0; i < steps.length; i++) {
513 const root = ReactDOMClient.createRoot(container);
510 - act(() =>
514 + await act(() =>
515 root.render(
516 <Root>
517 <X />
@@ -521,7 +525,7 @@ describe('StoreStressConcurrent', () => {
525 ),
526 );
527 expect(print(store)).toEqual(snapshots[i]);
524 - act(() => root.unmount());
528 + await act(() => root.unmount());
529 expect(print(store)).toBe('');
530 }
531
@@ -531,7 +535,7 @@ describe('StoreStressConcurrent', () => {
535 // Always start with a fresh container and steps[i].
536 container = document.createElement('div');
537 const root = ReactDOMClient.createRoot(container);
534 - act(() =>
538 + await act(() =>
539 root.render(
540 <Root>
541 <X />
@@ -542,7 +546,7 @@ describe('StoreStressConcurrent', () => {
546 );
547 expect(print(store)).toEqual(snapshots[i]);
548 // Re-render with steps[j].
545 - act(() =>
549 + await act(() =>
550 root.render(
551 <Root>
552 <X />
@@ -554,7 +558,7 @@ describe('StoreStressConcurrent', () => {
558 // Verify the successful transition to steps[j].
559 expect(print(store)).toEqual(snapshots[j]);
560 // Check that we can transition back again.
557 - act(() =>
561 + await act(() =>
562 root.render(
563 <Root>
564 <X />
@@ -565,7 +569,7 @@ describe('StoreStressConcurrent', () => {
569 );
570 expect(print(store)).toEqual(snapshots[i]);
571 // Clean up after every iteration.
568 - act(() => root.unmount());
572 + await act(() => root.unmount());
573 expect(print(store)).toBe('');
574 }
575 }
@@ -576,7 +580,7 @@ describe('StoreStressConcurrent', () => {
580 // Always start with a fresh container and steps[i].
581 container = document.createElement('div');
582 const root = ReactDOMClient.createRoot(container);
579 - act(() =>
583 + await act(() =>
584 root.render(
585 <Root>
586 <X />
@@ -591,7 +595,7 @@ describe('StoreStressConcurrent', () => {
595 );
596 expect(print(store)).toEqual(snapshots[i]);
597 // Re-render with steps[j].
594 - act(() =>
598 + await act(() =>
599 root.render(
600 <Root>
601 <X />
@@ -607,7 +611,7 @@ describe('StoreStressConcurrent', () => {
611 // Verify the successful transition to steps[j].
612 expect(print(store)).toEqual(snapshots[j]);
613 // Check that we can transition back again.
610 - act(() =>
614 + await act(() =>
615 root.render(
616 <Root>
617 <X />
@@ -622,7 +626,7 @@ describe('StoreStressConcurrent', () => {
626 );
627 expect(print(store)).toEqual(snapshots[i]);
628 // Clean up after every iteration.
625 - act(() => root.unmount());
629 + await act(() => root.unmount());
630 expect(print(store)).toBe('');
631 }
632 }
@@ -633,7 +637,7 @@ describe('StoreStressConcurrent', () => {
637 // Always start with a fresh container and steps[i].
638 container = document.createElement('div');
639 const root = ReactDOMClient.createRoot(container);
636 - act(() =>
640 + await act(() =>
641 root.render(
642 <Root>
643 <X />
@@ -644,7 +648,7 @@ describe('StoreStressConcurrent', () => {
648 );
649 expect(print(store)).toEqual(snapshots[i]);
650 // Re-render with steps[j].
647 - act(() =>
651 + await act(() =>
652 root.render(
653 <Root>
654 <X />
@@ -660,7 +664,7 @@ describe('StoreStressConcurrent', () => {
664 // Verify the successful transition to steps[j].
665 expect(print(store)).toEqual(snapshots[j]);
666 // Check that we can transition back again.
663 - act(() =>
667 + await act(() =>
668 root.render(
669 <Root>
670 <X />
@@ -671,7 +675,7 @@ describe('StoreStressConcurrent', () => {
675 );
676 expect(print(store)).toEqual(snapshots[i]);
677 // Clean up after every iteration.
674 - act(() => root.unmount());
678 + await act(() => root.unmount());
679 expect(print(store)).toBe('');
680 }
681 }
@@ -682,7 +686,7 @@ describe('StoreStressConcurrent', () => {
686 // Always start with a fresh container and steps[i].
687 container = document.createElement('div');
688 const root = ReactDOMClient.createRoot(container);
685 - act(() =>
689 + await act(() =>
690 root.render(
691 <Root>
692 <X />
@@ -697,7 +701,7 @@ describe('StoreStressConcurrent', () => {
701 );
702 expect(print(store)).toEqual(snapshots[i]);
703 // Re-render with steps[j].
700 - act(() =>
704 + await act(() =>
705 root.render(
706 <Root>
707 <X />
@@ -709,7 +713,7 @@ describe('StoreStressConcurrent', () => {
713 // Verify the successful transition to steps[j].
714 expect(print(store)).toEqual(snapshots[j]);
715 // Check that we can transition back again.
712 - act(() =>
716 + await act(() =>
717 root.render(
718 <Root>
719 <X />
@@ -724,7 +728,7 @@ describe('StoreStressConcurrent', () => {
728 );
729 expect(print(store)).toEqual(snapshots[i]);
730 // Clean up after every iteration.
727 - act(() => root.unmount());
731 + await act(() => root.unmount());
732 expect(print(store)).toBe('');
733 }
734 }
@@ -735,7 +739,7 @@ describe('StoreStressConcurrent', () => {
739 // Always start with a fresh container and steps[i].
740 container = document.createElement('div');
741 const root = ReactDOMClient.createRoot(container);
738 - act(() =>
742 + await act(() =>
743 root.render(
744 <Root>
745 <X />
@@ -772,7 +776,7 @@ describe('StoreStressConcurrent', () => {
776 expect(print(store)).toEqual(snapshots[i]);
777
778 // Trigger actual fallback.
775 - act(() =>
779 + await act(() =>
780 root.render(
781 <Root>
782 <X />
@@ -788,7 +792,7 @@ describe('StoreStressConcurrent', () => {
792 expect(print(store)).toEqual(snapshots[j]);
793
794 // Force fallback while we're in fallback mode.
791 - act(() => {
795 + await act(() => {
796 bridge.send('overrideSuspense', {
797 id: suspenseID,
798 rendererID: store.getRendererIDForElement(suspenseID),
@@ -799,7 +803,7 @@ describe('StoreStressConcurrent', () => {
803 expect(print(store)).toEqual(snapshots[j]);
804
805 // Switch to primary mode.
802 - act(() =>
806 + await act(() =>
807 root.render(
808 <Root>
809 <X />
@@ -859,7 +863,11 @@ describe('StoreStressConcurrent', () => {
863 ];
864
865 const Never = () => {
862 - throw new Promise(() => {});
866 + if (React.use) {
867 + React.use(new Promise(() => {}));
868 + } else {
869 + throw new Promise(() => {});
870 + }
871 };
872
873 const MaybeSuspend = ({children, suspend}) => {
@@ -890,7 +898,7 @@ describe('StoreStressConcurrent', () => {
898 let container = document.createElement('div');
899 for (let i = 0; i < steps.length; i++) {
900 const root = ReactDOMClient.createRoot(container);
893 - act(() =>
901 + await act(() =>
902 root.render(
903 <Root>
904 <X />
@@ -903,7 +911,7 @@ describe('StoreStressConcurrent', () => {
911 );
912 // We snapshot each step once so it doesn't regress.
913 snapshots.push(print(store));
906 - act(() => root.unmount());
914 + await act(() => root.unmount());
915 expect(print(store)).toBe('');
916 }
917
@@ -913,7 +921,7 @@ describe('StoreStressConcurrent', () => {
921 const fallbackSnapshots = [];
922 for (let i = 0; i < steps.length; i++) {
923 const root = ReactDOMClient.createRoot(container);
916 - act(() =>
924 + await act(() =>
925 root.render(
926 <Root>
927 <X />
@@ -928,7 +936,7 @@ describe('StoreStressConcurrent', () => {
936 );
937 // We snapshot each step once so it doesn't regress.
938 fallbackSnapshots.push(print(store));
931 - act(() => root.unmount());
939 + await act(() => root.unmount());
940 expect(print(store)).toBe('');
941 }
942
@@ -1046,7 +1054,7 @@ describe('StoreStressConcurrent', () => {
1054 // Always start with a fresh container and steps[i].
1055 container = document.createElement('div');
1056 const root = ReactDOMClient.createRoot(container);
1049 - act(() =>
1057 + await act(() =>
1058 root.render(
1059 <Root>
1060 <X />
@@ -1059,7 +1067,7 @@ describe('StoreStressConcurrent', () => {
1067 );
1068 expect(print(store)).toEqual(snapshots[i]);
1069 // Re-render with steps[j].
1062 - act(() =>
1070 + await act(() =>
1071 root.render(
1072 <Root>
1073 <X />
@@ -1073,7 +1081,7 @@ describe('StoreStressConcurrent', () => {
1081 // Verify the successful transition to steps[j].
1082 expect(print(store)).toEqual(snapshots[j]);
1083 // Check that we can transition back again.
1076 - act(() =>
1084 + await act(() =>
1085 root.render(
1086 <Root>
1087 <X />
@@ -1086,7 +1094,7 @@ describe('StoreStressConcurrent', () => {
1094 );
1095 expect(print(store)).toEqual(snapshots[i]);
1096 // Clean up after every iteration.
1089 - act(() => root.unmount());
1097 + await act(() => root.unmount());
1098 expect(print(store)).toBe('');
1099 }
1100 }
@@ -1097,7 +1105,7 @@ describe('StoreStressConcurrent', () => {
1105 // Always start with a fresh container and steps[i].
1106 container = document.createElement('div');
1107 const root = ReactDOMClient.createRoot(container);
1100 - act(() =>
1108 + await act(() =>
1109 root.render(
1110 <Root>
1111 <X />
@@ -1115,7 +1123,7 @@ describe('StoreStressConcurrent', () => {
1123 );
1124 expect(print(store)).toEqual(fallbackSnapshots[i]);
1125 // Re-render with steps[j].
1118 - act(() =>
1126 + await act(() =>
1127 root.render(
1128 <Root>
1129 <X />
@@ -1134,7 +1142,7 @@ describe('StoreStressConcurrent', () => {
1142 // Verify the successful transition to steps[j].
1143 expect(print(store)).toEqual(fallbackSnapshots[j]);
1144 // Check that we can transition back again.
1137 - act(() =>
1145 + await act(() =>
1146 root.render(
1147 <Root>
1148 <X />
@@ -1152,7 +1160,7 @@ describe('StoreStressConcurrent', () => {
1160 );
1161 expect(print(store)).toEqual(fallbackSnapshots[i]);
1162 // Clean up after every iteration.
1155 - act(() => root.unmount());
1163 + await act(() => root.unmount());
1164 expect(print(store)).toBe('');
1165 }
1166 }
@@ -1163,7 +1171,7 @@ describe('StoreStressConcurrent', () => {
1171 // Always start with a fresh container and steps[i].
1172 container = document.createElement('div');
1173 const root = ReactDOMClient.createRoot(container);
1166 - act(() =>
1174 + await act(() =>
1175 root.render(
1176 <Root>
1177 <X />
@@ -1176,7 +1184,7 @@ describe('StoreStressConcurrent', () => {
1184 );
1185 expect(print(store)).toEqual(snapshots[i]);
1186 // Re-render with steps[j].
1179 - act(() =>
1187 + await act(() =>
1188 root.render(
1189 <Root>
1190 <X />
@@ -1190,7 +1198,7 @@ describe('StoreStressConcurrent', () => {
1198 // Verify the successful transition to steps[j].
1199 expect(print(store)).toEqual(fallbackSnapshots[j]);
1200 // Check that we can transition back again.
1193 - act(() =>
1201 + await act(() =>
1202 root.render(
1203 <Root>
1204 <X />
@@ -1203,7 +1211,7 @@ describe('StoreStressConcurrent', () => {
1211 );
1212 expect(print(store)).toEqual(snapshots[i]);
1213 // Clean up after every iteration.
1206 - act(() => root.unmount());
1214 + await act(() => root.unmount());
1215 expect(print(store)).toBe('');
1216 }
1217 }
@@ -1214,7 +1222,7 @@ describe('StoreStressConcurrent', () => {
1222 // Always start with a fresh container and steps[i].
1223 container = document.createElement('div');
1224 const root = ReactDOMClient.createRoot(container);
1217 - act(() =>
1225 + await act(() =>
1226 root.render(
1227 <Root>
1228 <X />
@@ -1227,7 +1235,7 @@ describe('StoreStressConcurrent', () => {
1235 );
1236 expect(print(store)).toEqual(fallbackSnapshots[i]);
1237 // Re-render with steps[j].
1230 - act(() =>
1238 + await act(() =>
1239 root.render(
1240 <Root>
1241 <X />
@@ -1241,7 +1249,7 @@ describe('StoreStressConcurrent', () => {
1249 // Verify the successful transition to steps[j].
1250 expect(print(store)).toEqual(snapshots[j]);
1251 // Check that we can transition back again.
1244 - act(() =>
1252 + await act(() =>
1253 root.render(
1254 <Root>
1255 <X />
@@ -1254,7 +1262,7 @@ describe('StoreStressConcurrent', () => {
1262 );
1263 expect(print(store)).toEqual(fallbackSnapshots[i]);
1264 // Clean up after every iteration.
1257 - act(() => root.unmount());
1265 + await act(() => root.unmount());
1266 expect(print(store)).toBe('');
1267 }
1268 }
@@ -1265,7 +1273,7 @@ describe('StoreStressConcurrent', () => {
1273 // Always start with a fresh container and steps[i].
1274 container = document.createElement('div');
1275 const root = ReactDOMClient.createRoot(container);
1268 - act(() =>
1276 + await act(() =>
1277 root.render(
1278 <Root>
1279 <X />
@@ -1304,7 +1312,7 @@ describe('StoreStressConcurrent', () => {
1312 expect(print(store)).toEqual(snapshots[i]);
1313
1314 // Trigger actual fallback.
1307 - act(() =>
1315 + await act(() =>
1316 root.render(
1317 <Root>
1318 <X />
@@ -1318,7 +1326,7 @@ describe('StoreStressConcurrent', () => {
1326 expect(print(store)).toEqual(fallbackSnapshots[j]);
1327
1328 // Force fallback while we're in fallback mode.
1321 - act(() => {
1329 + await act(() => {
1330 bridge.send('overrideSuspense', {
1331 id: suspenseID,
1332 rendererID: store.getRendererIDForElement(suspenseID),
@@ -1329,7 +1337,7 @@ describe('StoreStressConcurrent', () => {
1337 expect(print(store)).toEqual(fallbackSnapshots[j]);
1338
1339 // Switch to primary mode.
1332 - act(() =>
1340 + await act(() =>
1341 root.render(
1342 <Root>
1343 <X />
@@ -1355,7 +1363,7 @@ describe('StoreStressConcurrent', () => {
1363 expect(print(store)).toEqual(snapshots[i]);
1364
1365 // Clean up after every iteration.
1358 - act(() => root.unmount());
1366 + await act(() => root.unmount());
1367 expect(print(store)).toBe('');
1368 }
1369 }
packages/react-devtools-shared/src/devtools/cache.js
+29 -57
@@ -7,7 +7,12 @@
7 * @flow
8 */
9
10 -import type {ReactContext, Thenable} from 'shared/ReactTypes';
10 +import type {
11 + ReactContext,
12 + Thenable,
13 + FulfilledThenable,
14 + RejectedThenable,
15 +} from 'shared/ReactTypes';
16
17 import * as React from 'react';
18 import {createContext} from 'react';
@@ -26,27 +31,6 @@ import {createContext} from 'react';
31
32 export type {Thenable};
33
29 -interface Suspender {
30 - then(resolve: () => mixed, reject: () => mixed): mixed;
31 -}
32 -
33 -type PendingResult = {
34 - status: 0,
35 - value: Suspender,
36 -};
37 -
38 -type ResolvedResult<Value> = {
39 - status: 1,
40 - value: Value,
41 -};
42 -
43 -type RejectedResult = {
44 - status: 2,
45 - value: mixed,
46 -};
47 -
48 -type Result<Value> = PendingResult | ResolvedResult<Value> | RejectedResult;
49 -
34 export type Resource<Input, Key, Value> = {
35 clear(): void,
36 invalidate(Key): void,
@@ -55,10 +39,6 @@ export type Resource<Input, Key, Value> = {
39 write(Key, Value): void,
40 };
41
58 -const Pending = 0;
59 -const Resolved = 1;
60 -const Rejected = 2;
61 -
42 let readContext;
43 if (typeof React.use === 'function') {
44 readContext = function (Context: ReactContext<null>) {
@@ -115,33 +95,25 @@ function accessResult<Input, Key, Value>(
95 fetch: Input => Thenable<Value>,
96 input: Input,
97 key: Key,
118 -): Result<Value> {
98 +): Thenable<Value> {
99 const entriesForResource = getEntriesForResource(resource);
100 const entry = entriesForResource.get(key);
101 if (entry === undefined) {
102 const thenable = fetch(input);
103 thenable.then(
104 value => {
125 - if (newResult.status === Pending) {
126 - const resolvedResult: ResolvedResult<Value> = (newResult: any);
127 - resolvedResult.status = Resolved;
128 - resolvedResult.value = value;
129 - }
105 + const fulfilledThenable: FulfilledThenable<Value> = (thenable: any);
106 + fulfilledThenable.status = 'fulfilled';
107 + fulfilledThenable.value = value;
108 },
109 error => {
132 - if (newResult.status === Pending) {
133 - const rejectedResult: RejectedResult = (newResult: any);
134 - rejectedResult.status = Rejected;
135 - rejectedResult.value = error;
136 - }
110 + const rejectedThenable: RejectedThenable<Value> = (thenable: any);
111 + rejectedThenable.status = 'rejected';
112 + rejectedThenable.reason = error;
113 },
114 );
139 - const newResult: PendingResult = {
140 - status: Pending,
141 - value: thenable,
142 - };
143 - entriesForResource.set(key, newResult);
144 - return newResult;
115 + entriesForResource.set(key, thenable);
116 + return thenable;
117 } else {
118 return entry;
119 }
@@ -167,23 +139,22 @@ export function createResource<Input, Key, Value>(
139 readContext(CacheContext);
140
141 const key = hashInput(input);
170 - const result: Result<Value> = accessResult(resource, fetch, input, key);
142 + const result: Thenable<Value> = accessResult(resource, fetch, input, key);
143 + if (typeof React.use === 'function') {
144 + return React.use(result);
145 + }
146 +
147 switch (result.status) {
172 - case Pending: {
173 - const suspender = result.value;
174 - throw suspender;
175 - }
176 - case Resolved: {
148 + case 'fulfilled': {
149 const value = result.value;
150 return value;
151 }
180 - case Rejected: {
181 - const error = result.value;
152 + case 'rejected': {
153 + const error = result.reason;
154 throw error;
155 }
156 default:
185 - // Should be unreachable
186 - return (undefined: any);
157 + throw result;
158 }
159 },
160
@@ -198,12 +169,13 @@ export function createResource<Input, Key, Value>(
169 write(key: Key, value: Value): void {
170 const entriesForResource = getEntriesForResource(resource);
171
201 - const resolvedResult: ResolvedResult<Value> = {
202 - status: Resolved,
172 + const fulfilledThenable: FulfilledThenable<Value> = (Promise.resolve(
173 value,
204 - };
174 + ): any);
175 + fulfilledThenable.status = 'fulfilled';
176 + fulfilledThenable.value = value;
177
206 - entriesForResource.set(key, resolvedResult);
178 + entriesForResource.set(key, fulfilledThenable);
179 },
180 };
181
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js
+1 -1
@@ -20,7 +20,7 @@ import {getMetaValueLabel, serializeHooksForCopy} from '../utils';
20 import Store from '../../store';
21 import styles from './InspectedElementHooksTree.css';
22 import {meta} from '../../../hydration';
23 -import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
23 +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation';
24 import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
25 import isArray from 'react-devtools-shared/src/isArray';
26
packages/react-devtools-shared/src/devtools/views/ErrorBoundary/cache.js
+61 -61
@@ -7,54 +7,46 @@
7 * @flow
8 */
9
10 -import type {Wakeable} from 'shared/ReactTypes';
10 +import type {
11 + Thenable,
12 + FulfilledThenable,
13 + RejectedThenable,
14 +} from 'shared/ReactTypes';
15 import type {GitHubIssue} from './githubAPI';
16
17 +import * as React from 'react';
18 +
19 import {unstable_getCacheForType as getCacheForType} from 'react';
20 import {searchGitHubIssues} from './githubAPI';
21
22 const API_TIMEOUT = 3000;
17 -
18 -const Pending = 0;
19 -const Resolved = 1;
20 -const Rejected = 2;
21 -
22 -type PendingRecord = {
23 - status: 0,
24 - value: Wakeable,
25 -};
26 -
27 -type ResolvedRecord<T> = {
28 - status: 1,
29 - value: T,
30 -};
31 -
32 -type RejectedRecord = {
33 - status: 2,
34 - value: null,
35 -};
36 -
37 -type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
38 -
39 -function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
40 - if (record.status === Resolved) {
41 - // This is just a type refinement.
42 - return record;
43 - } else if (record.status === Rejected) {
44 - // This is just a type refinement.
45 - return record;
23 +function readRecord<T>(record: Thenable<T>): T | null {
24 + if (typeof React.use === 'function') {
25 + try {
26 + return React.use(record);
27 + } catch (x) {
28 + if (x === null) {
29 + return null;
30 + }
31 + throw x;
32 + }
33 + }
34 + if (record.status === 'fulfilled') {
35 + return record.value;
36 + } else if (record.status === 'rejected') {
37 + return null;
38 } else {
47 - throw record.value;
39 + throw record;
40 }
41 }
42
51 -type GitHubIssueMap = Map<string, Record<GitHubIssue>>;
43 +type GitHubIssueMap = Map<string, Thenable<GitHubIssue>>;
44
45 function createMap(): GitHubIssueMap {
46 return new Map();
47 }
48
57 -function getRecordMap(): Map<string, Record<GitHubIssue>> {
49 +function getRecordMap(): Map<string, Thenable<GitHubIssue>> {
50 return getCacheForType(createMap);
51 }
52
@@ -65,10 +57,15 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
57 let record = map.get(errorMessage);
58
59 if (!record) {
68 - const callbacks = new Set<() => mixed>();
69 - const wakeable: Wakeable = {
70 - then(callback: () => mixed) {
60 + const callbacks = new Set<(value: any) => mixed>();
61 + const rejectCallbacks = new Set<(reason: mixed) => mixed>();
62 + const thenable: Thenable<GitHubIssue> = {
63 + status: 'pending',
64 + value: null,
65 + reason: null,
66 + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
67 callbacks.add(callback);
68 + rejectCallbacks.add(reject);
69 },
70
71 // Optional property used by Timeline:
@@ -76,13 +73,17 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
73 };
74 const wake = () => {
75 // This assumes they won't throw.
79 - callbacks.forEach(callback => callback());
76 + callbacks.forEach(callback => callback((thenable: any).value));
77 callbacks.clear();
78 + rejectCallbacks.clear();
79 };
82 - const newRecord: Record<GitHubIssue> = (record = {
83 - status: Pending,
84 - value: wakeable,
85 - });
80 + const wakeRejections = () => {
81 + // This assumes they won't throw.
82 + rejectCallbacks.forEach(callback => callback((thenable: any).reason));
83 + rejectCallbacks.clear();
84 + callbacks.clear();
85 + };
86 + record = thenable;
87
88 let didTimeout = false;
89
@@ -93,41 +94,40 @@ export function findGitHubIssue(errorMessage: string): GitHubIssue | null {
94 }
95
96 if (maybeItem) {
96 - const resolvedRecord =
97 - ((newRecord: any): ResolvedRecord<GitHubIssue>);
98 - resolvedRecord.status = Resolved;
99 - resolvedRecord.value = maybeItem;
97 + const fulfilledThenable: FulfilledThenable<GitHubIssue> =
98 + (thenable: any);
99 + fulfilledThenable.status = 'fulfilled';
100 + fulfilledThenable.value = maybeItem;
101 + wake();
102 } else {
101 - const notFoundRecord = ((newRecord: any): RejectedRecord);
102 - notFoundRecord.status = Rejected;
103 - notFoundRecord.value = null;
103 + const notFoundThenable: RejectedThenable<GitHubIssue> =
104 + (thenable: any);
105 + notFoundThenable.status = 'rejected';
106 + notFoundThenable.reason = null;
107 + wakeRejections();
108 }
105 -
106 - wake();
109 })
110 .catch(error => {
109 - const thrownRecord = ((newRecord: any): RejectedRecord);
110 - thrownRecord.status = Rejected;
111 - thrownRecord.value = null;
112 -
113 - wake();
111 + const rejectedThenable: RejectedThenable<GitHubIssue> = (thenable: any);
112 + rejectedThenable.status = 'rejected';
113 + rejectedThenable.reason = null;
114 + wakeRejections();
115 });
116
117 // Only wait a little while for GitHub results before showing a fallback.
118 setTimeout(() => {
119 didTimeout = true;
120
120 - const timedoutRecord = ((newRecord: any): RejectedRecord);
121 - timedoutRecord.status = Rejected;
122 - timedoutRecord.value = null;
123 -
124 - wake();
121 + const timedoutThenable: RejectedThenable<GitHubIssue> = (thenable: any);
122 + timedoutThenable.status = 'rejected';
123 + timedoutThenable.reason = null;
124 + wakeRejections();
125 }, API_TIMEOUT);
126
127 map.set(errorMessage, record);
128 }
129
130 - const response = readRecord(record).value;
130 + const response = readRecord(record);
131 return response;
132 }
133
packages/react-devtools-shared/src/devtools/views/Profiler/HookChangeSummary.js
+2 -4
@@ -21,10 +21,8 @@ import ButtonIcon from '../ButtonIcon';
21 import {InspectedElementContext} from '../Components/InspectedElementContext';
22 import {StoreContext} from '../context';
23
24 -import {
25 - getAlreadyLoadedHookNames,
26 - getHookSourceLocationKey,
27 -} from 'react-devtools-shared/src/hookNamesCache';
24 +import {getAlreadyLoadedHookNames} from 'react-devtools-shared/src/hookNamesCache';
25 +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation';
26 import Toggle from '../Toggle';
27 import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
28 import type {ChangeDescription} from './types';
packages/react-devtools-shared/src/dynamicImportCache.js
+56 -50
@@ -9,30 +9,15 @@
9
10 import {__DEBUG__} from 'react-devtools-shared/src/constants';
11
12 -import type {Thenable, Wakeable} from 'shared/ReactTypes';
12 +import type {
13 + Thenable,
14 + FulfilledThenable,
15 + RejectedThenable,
16 +} from 'shared/ReactTypes';
17
14 -const TIMEOUT = 30000;
15 -
16 -const Pending = 0;
17 -const Resolved = 1;
18 -const Rejected = 2;
19 -
20 -type PendingRecord = {
21 - status: 0,
22 - value: Wakeable,
23 -};
24 -
25 -type ResolvedRecord<T> = {
26 - status: 1,
27 - value: T,
28 -};
29 -
30 -type RejectedRecord = {
31 - status: 2,
32 - value: null,
33 -};
18 +import * as React from 'react';
19
35 -type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
20 +const TIMEOUT = 30000;
21
22 type Module = any;
23 type ModuleLoaderFunction = () => Thenable<Module>;
@@ -42,16 +27,23 @@ type ModuleLoaderFunction = () => Thenable<Module>;
27 // Modules are static anyway.
28 const moduleLoaderFunctionToModuleMap: Map<ModuleLoaderFunction, Module> =
29 new Map();
45 -
46 -function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
47 - if (record.status === Resolved) {
48 - // This is just a type refinement.
49 - return record;
50 - } else if (record.status === Rejected) {
51 - // This is just a type refinement.
52 - return record;
30 +function readRecord<T>(record: Thenable<T>): T | null {
31 + if (typeof React.use === 'function') {
32 + try {
33 + return React.use(record);
34 + } catch (x) {
35 + if (x === null) {
36 + return null;
37 + }
38 + throw x;
39 + }
40 + }
41 + if (record.status === 'fulfilled') {
42 + return record.value;
43 + } else if (record.status === 'rejected') {
44 + return null;
45 } else {
54 - throw record.value;
46 + throw record;
47 }
48 }
49
@@ -66,10 +58,15 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
58 }
59
60 if (!record) {
69 - const callbacks = new Set<() => mixed>();
70 - const wakeable: Wakeable = {
71 - then(callback: () => mixed) {
61 + const callbacks = new Set<(value: any) => mixed>();
62 + const rejectCallbacks = new Set<(reason: mixed) => mixed>();
63 + const thenable: Thenable<Module> = {
64 + status: 'pending',
65 + value: null,
66 + reason: null,
67 + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
68 callbacks.add(callback);
69 + rejectCallbacks.add(reject);
70 },
71
72 // Optional property used by Timeline:
@@ -85,12 +82,21 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
82 // This assumes they won't throw.
83 callbacks.forEach(callback => callback());
84 callbacks.clear();
85 + rejectCallbacks.clear();
86 };
87 + const wakeRejections = () => {
88 + if (timeoutID) {
89 + clearTimeout(timeoutID);
90 + timeoutID = null;
91 + }
92
90 - const newRecord: Record<Module> = (record = {
91 - status: Pending,
92 - value: wakeable,
93 - });
93 + // This assumes they won't throw.
94 + rejectCallbacks.forEach(callback => callback((thenable: any).reason));
95 + rejectCallbacks.clear();
96 + callbacks.clear();
97 + };
98 +
99 + record = thenable;
100
101 let didTimeout = false;
102
@@ -106,9 +112,9 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
112 return;
113 }
114
109 - const resolvedRecord = ((newRecord: any): ResolvedRecord<Module>);
110 - resolvedRecord.status = Resolved;
111 - resolvedRecord.value = module;
115 + const fulfilledThenable: FulfilledThenable<Module> = (thenable: any);
116 + fulfilledThenable.status = 'fulfilled';
117 + fulfilledThenable.value = module;
118
119 wake();
120 },
@@ -125,11 +131,11 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
131
132 console.log(error);
133
128 - const thrownRecord = ((newRecord: any): RejectedRecord);
129 - thrownRecord.status = Rejected;
130 - thrownRecord.value = null;
134 + const rejectedThenable: RejectedThenable<Module> = (thenable: any);
135 + rejectedThenable.status = 'rejected';
136 + rejectedThenable.reason = error;
137
132 - wake();
138 + wakeRejections();
139 },
140 );
141
@@ -145,17 +151,17 @@ export function loadModule(moduleLoaderFunction: ModuleLoaderFunction): Module {
151
152 didTimeout = true;
153
148 - const timedoutRecord = ((newRecord: any): RejectedRecord);
149 - timedoutRecord.status = Rejected;
150 - timedoutRecord.value = null;
154 + const rejectedThenable: RejectedThenable<Module> = (thenable: any);
155 + rejectedThenable.status = 'rejected';
156 + rejectedThenable.reason = null;
157
152 - wake();
158 + wakeRejections();
159 }, TIMEOUT);
160
161 moduleLoaderFunctionToModuleMap.set(moduleLoaderFunction, record);
162 }
163
164 // $FlowFixMe[underconstrained-implicit-instantiation]
159 - const response = readRecord(record).value;
165 + const response = readRecord(record);
166 return response;
167 }
packages/react-devtools-shared/src/hookNamesCache.js
+74 -76
@@ -10,49 +10,40 @@
10 import {__DEBUG__} from 'react-devtools-shared/src/constants';
11
12 import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
13 -import type {Thenable, Wakeable} from 'shared/ReactTypes';
13 +import type {
14 + Thenable,
15 + FulfilledThenable,
16 + RejectedThenable,
17 +} from 'shared/ReactTypes';
18 import type {
19 Element,
20 HookNames,
17 - HookSourceLocationKey,
21 } from 'react-devtools-shared/src/frontend/types';
19 -import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
22 import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
23 +
24 +import * as React from 'react';
25 +
26 import {withCallbackPerfMeasurements} from './PerformanceLoggingUtils';
27 import {logEvent} from './Logger';
28
29 const TIMEOUT = 30000;
25 -
26 -const Pending = 0;
27 -const Resolved = 1;
28 -const Rejected = 2;
29 -
30 -type PendingRecord = {
31 - status: 0,
32 - value: Wakeable,
33 -};
34 -
35 -type ResolvedRecord<T> = {
36 - status: 1,
37 - value: T,
38 -};
39 -
40 -type RejectedRecord = {
41 - status: 2,
42 - value: null,
43 -};
44 -
45 -type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
46 -
47 -function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
48 - if (record.status === Resolved) {
49 - // This is just a type refinement.
50 - return record;
51 - } else if (record.status === Rejected) {
52 - // This is just a type refinement.
53 - return record;
30 +function readRecord<T>(record: Thenable<T>): T | null {
31 + if (typeof React.use === 'function') {
32 + try {
33 + return React.use(record);
34 + } catch (x) {
35 + if (record.status === 'rejected') {
36 + return null;
37 + }
38 + throw x;
39 + }
40 + }
41 + if (record.status === 'fulfilled') {
42 + return record.value;
43 + } else if (record.status === 'rejected') {
44 + return null;
45 } else {
55 - throw record.value;
46 + throw record;
47 }
48 }
49
@@ -65,16 +56,16 @@ type LoadHookNamesFunction = (
56 // Otherwise, refreshing the inspected element cache would also clear this cache.
57 // TODO Rethink this if the React API constraints change.
58 // See https://github.com/reactwg/react-18/discussions/25#discussioncomment-980435
68 -let map: WeakMap<Element, Record<HookNames>> = new WeakMap();
59 +let map: WeakMap<Element, Thenable<HookNames>> = new WeakMap();
60
61 export function hasAlreadyLoadedHookNames(element: Element): boolean {
62 const record = map.get(element);
72 - return record != null && record.status === Resolved;
63 + return record != null && record.status === 'fulfilled';
64 }
65
66 export function getAlreadyLoadedHookNames(element: Element): HookNames | null {
67 const record = map.get(element);
77 - if (record != null && record.status === Resolved) {
68 + if (record != null && record.status === 'fulfilled') {
69 return record.value;
70 }
71 return null;
@@ -95,10 +86,15 @@ export function loadHookNames(
86 }
87
88 if (!record) {
98 - const callbacks = new Set<() => mixed>();
99 - const wakeable: Wakeable = {
100 - then(callback: () => mixed) {
89 + const callbacks = new Set<(value: any) => mixed>();
90 + const rejectCallbacks = new Set<(reason: mixed) => mixed>();
91 + const thenable: Thenable<HookNames> = {
92 + status: 'pending',
93 + value: null,
94 + reason: null,
95 + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
96 callbacks.add(callback);
97 + rejectCallbacks.add(reject);
98 },
99
100 // Optional property used by Timeline:
@@ -117,7 +113,18 @@ export function loadHookNames(
113 }
114
115 // This assumes they won't throw.
120 - callbacks.forEach(callback => callback());
116 + callbacks.forEach(callback => callback((thenable: any).value));
117 + callbacks.clear();
118 + rejectCallbacks.clear();
119 + };
120 + const wakeRejections = () => {
121 + if (timeoutID) {
122 + clearTimeout(timeoutID);
123 + timeoutID = null;
124 + }
125 + // This assumes they won't throw.
126 + rejectCallbacks.forEach(callback => callback((thenable: any).reason));
127 + rejectCallbacks.clear();
128 callbacks.clear();
129 };
130
@@ -132,10 +139,7 @@ export function loadHookNames(
139 });
140 };
141
135 - const newRecord: Record<HookNames> = (record = {
136 - status: Pending,
137 - value: wakeable,
138 - });
142 + record = thenable;
143
144 withCallbackPerfMeasurements(
145 'loadHookNames',
@@ -151,20 +155,24 @@ export function loadHookNames(
155 }
156
157 if (hookNames) {
154 - const resolvedRecord =
155 - ((newRecord: any): ResolvedRecord<HookNames>);
156 - resolvedRecord.status = Resolved;
157 - resolvedRecord.value = hookNames;
158 + const fulfilledThenable: FulfilledThenable<HookNames> =
159 + (thenable: any);
160 + fulfilledThenable.status = 'fulfilled';
161 + fulfilledThenable.value = hookNames;
162 + status = 'success';
163 + resolvedHookNames = hookNames;
164 + done();
165 + wake();
166 } else {
159 - const notFoundRecord = ((newRecord: any): RejectedRecord);
160 - notFoundRecord.status = Rejected;
161 - notFoundRecord.value = null;
167 + const notFoundThenable: RejectedThenable<HookNames> =
168 + (thenable: any);
169 + notFoundThenable.status = 'rejected';
170 + notFoundThenable.reason = null;
171 + status = 'error';
172 + resolvedHookNames = hookNames;
173 + done();
174 + wakeRejections();
175 }
163 -
164 - status = 'success';
165 - resolvedHookNames = hookNames;
166 - done();
167 - wake();
176 },
177 function onError(error) {
178 if (didTimeout) {
@@ -177,13 +185,14 @@ export function loadHookNames(
185
186 console.error(error);
187
180 - const thrownRecord = ((newRecord: any): RejectedRecord);
181 - thrownRecord.status = Rejected;
182 - thrownRecord.value = null;
188 + const rejectedThenable: RejectedThenable<HookNames> =
189 + (thenable: any);
190 + rejectedThenable.status = 'rejected';
191 + rejectedThenable.reason = null;
192
193 status = 'error';
194 done();
186 - wake();
195 + wakeRejections();
196 },
197 );
198
@@ -197,13 +206,13 @@ export function loadHookNames(
206
207 didTimeout = true;
208
200 - const timedoutRecord = ((newRecord: any): RejectedRecord);
201 - timedoutRecord.status = Rejected;
202 - timedoutRecord.value = null;
209 + const timedoutThenable: RejectedThenable<HookNames> = (thenable: any);
210 + timedoutThenable.status = 'rejected';
211 + timedoutThenable.reason = null;
212
213 status = 'timeout';
214 done();
206 - wake();
215 + wakeRejections();
216 }, TIMEOUT);
217 },
218 handleLoadComplete,
@@ -211,21 +220,10 @@ export function loadHookNames(
220 map.set(element, record);
221 }
222
214 - const response = readRecord(record).value;
223 + const response = readRecord(record);
224 return response;
225 }
226
218 -export function getHookSourceLocationKey({
219 - fileName,
220 - lineNumber,
221 - columnNumber,
222 -}: HookSource): HookSourceLocationKey {
223 - if (fileName == null || lineNumber == null || columnNumber == null) {
224 - throw Error('Hook source code location not found.');
225 - }
226 - return `${fileName}:${lineNumber}:${columnNumber}`;
227 -}
228 -
227 export function clearHookNamesCache(): void {
228 map = new WeakMap();
229 }
packages/react-devtools-shared/src/hookSourceLocation.js new
+22
@@ -0,0 +1,22 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {HookSourceLocationKey} from 'react-devtools-shared/src/frontend/types';
11 +import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
12 +
13 +export function getHookSourceLocationKey({
14 + fileName,
15 + lineNumber,
16 + columnNumber,
17 +}: HookSource): HookSourceLocationKey {
18 + if (fileName == null || lineNumber == null || columnNumber == null) {
19 + throw Error('Hook source code location not found.');
20 + }
21 + return `${fileName}:${lineNumber}:${columnNumber}`;
22 +}
packages/react-devtools-shared/src/hooks/parseHookNames/index.js
+2
@@ -12,6 +12,8 @@ import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
12 import type {HookNames} from 'react-devtools-shared/src/frontend/types';
13 import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
14
15 +import 'react';
16 +
17 import {withAsyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
18 import WorkerizedParseSourceAndMetadata from './parseSourceAndMetadata.worker';
19 import typeof * as ParseSourceAndMetadataModule from './parseSourceAndMetadata';
packages/react-devtools-shared/src/hooks/parseHookNames/loadSourceAndMetadata.js
+1 -1
@@ -46,7 +46,7 @@
46 // and there is no need to convert runtime code to the original source.
47
48 import {__DEBUG__} from 'react-devtools-shared/src/constants';
49 -import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
49 +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation';
50 import {sourceMapIncludesSource} from '../SourceMapUtils';
51 import {
52 withAsyncPerfMeasurements,
packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js
+1 -1
@@ -15,7 +15,7 @@ import LRU from 'lru-cache';
15 import {getHookName} from '../astUtils';
16 import {areSourceMapsAppliedToErrors} from '../ErrorTester';
17 import {__DEBUG__} from 'react-devtools-shared/src/constants';
18 -import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
18 +import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookSourceLocation';
19 import {SourceMapMetadataConsumer} from '../SourceMapMetadataConsumer';
20 import {
21 withAsyncPerfMeasurements,
packages/react-devtools-shared/src/inspectedElementCache.js
+55 -53
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import * as React from 'react';
11 +
12 import {
13 unstable_getCacheForType as getCacheForType,
14 startTransition,
@@ -16,7 +18,11 @@ import {inspectElement as inspectElementMutableSource} from 'react-devtools-shar
18 import ElementPollingCancellationError from 'react-devtools-shared/src//errors/ElementPollingCancellationError';
19
20 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
19 -import type {Wakeable} from 'shared/ReactTypes';
21 +import type {
22 + Thenable,
23 + FulfilledThenable,
24 + RejectedThenable,
25 +} from 'shared/ReactTypes';
26 import type {
27 Element,
28 InspectedElement as InspectedElementFrontend,
@@ -24,44 +30,27 @@ import type {
30 InspectedElementPath,
31 } from 'react-devtools-shared/src/frontend/types';
32
27 -const Pending = 0;
28 -const Resolved = 1;
29 -const Rejected = 2;
30 -
31 -type PendingRecord = {
32 - status: 0,
33 - value: Wakeable,
34 -};
35 -
36 -type ResolvedRecord<T> = {
37 - status: 1,
38 - value: T,
39 -};
40 -
41 -type RejectedRecord = {
42 - status: 2,
43 - value: Error | string,
44 -};
45 -
46 -type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
47 -
48 -function readRecord<T>(record: Record<T>): ResolvedRecord<T> {
49 - if (record.status === Resolved) {
50 - // This is just a type refinement.
51 - return record;
33 +function readRecord<T>(record: Thenable<T>): T {
34 + if (typeof React.use === 'function') {
35 + return React.use(record);
36 + }
37 + if (record.status === 'fulfilled') {
38 + return record.value;
39 + } else if (record.status === 'rejected') {
40 + throw record.reason;
41 } else {
53 - throw record.value;
42 + throw record;
43 }
44 }
45
57 -type InspectedElementMap = WeakMap<Element, Record<InspectedElementFrontend>>;
46 +type InspectedElementMap = WeakMap<Element, Thenable<InspectedElementFrontend>>;
47 type CacheSeedKey = () => InspectedElementMap;
48
49 function createMap(): InspectedElementMap {
50 return new WeakMap();
51 }
52
64 -function getRecordMap(): WeakMap<Element, Record<InspectedElementFrontend>> {
53 +function getRecordMap(): WeakMap<Element, Thenable<InspectedElementFrontend>> {
54 return getCacheForType(createMap);
55 }
56
@@ -69,12 +58,15 @@ function createCacheSeed(
58 element: Element,
59 inspectedElement: InspectedElementFrontend,
60 ): [CacheSeedKey, InspectedElementMap] {
72 - const newRecord: Record<InspectedElementFrontend> = {
73 - status: Resolved,
61 + const thenable: FulfilledThenable<InspectedElementFrontend> = {
62 + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
63 + callback(thenable.value);
64 + },
65 + status: 'fulfilled',
66 value: inspectedElement,
67 };
68 const map = createMap();
77 - map.set(element, newRecord);
69 + map.set(element, thenable);
70 return [createMap, map];
71 }
72
@@ -91,10 +83,15 @@ export function inspectElement(
83 const map = getRecordMap();
84 let record = map.get(element);
85 if (!record) {
94 - const callbacks = new Set<() => mixed>();
95 - const wakeable: Wakeable = {
96 - then(callback: () => mixed) {
86 + const callbacks = new Set<(value: any) => mixed>();
87 + const rejectCallbacks = new Set<(reason: mixed) => mixed>();
88 + const thenable: Thenable<InspectedElementFrontend> = {
89 + status: 'pending',
90 + value: null,
91 + reason: null,
92 + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
93 callbacks.add(callback);
94 + rejectCallbacks.add(reject);
95 },
96
97 // Optional property used by Timeline:
@@ -103,19 +100,24 @@ export function inspectElement(
100
101 const wake = () => {
102 // This assumes they won't throw.
106 - callbacks.forEach(callback => callback());
103 + callbacks.forEach(callback => callback((thenable: any).value));
104 callbacks.clear();
105 + rejectCallbacks.clear();
106 };
109 - const newRecord: Record<InspectedElementFrontend> = (record = {
110 - status: Pending,
111 - value: wakeable,
112 - });
107 + const wakeRejections = () => {
108 + // This assumes they won't throw.
109 + rejectCallbacks.forEach(callback => callback((thenable: any).reason));
110 + rejectCallbacks.clear();
111 + callbacks.clear();
112 + };
113 + record = thenable;
114
115 const rendererID = store.getRendererIDForElement(element.id);
116 if (rendererID == null) {
116 - const rejectedRecord = ((newRecord: any): RejectedRecord);
117 - rejectedRecord.status = Rejected;
118 - rejectedRecord.value = new Error(
117 + const rejectedThenable: RejectedThenable<InspectedElementFrontend> =
118 + (thenable: any);
119 + rejectedThenable.status = 'rejected';
120 + rejectedThenable.reason = new Error(
121 `Could not inspect element with id "${element.id}". No renderer found.`,
122 );
123
@@ -129,29 +131,29 @@ export function inspectElement(
131 InspectedElementFrontend,
132 InspectedElementResponseType,
133 ]) => {
132 - const resolvedRecord =
133 - ((newRecord: any): ResolvedRecord<InspectedElementFrontend>);
134 - resolvedRecord.status = Resolved;
135 - resolvedRecord.value = inspectedElement;
136 -
134 + const fulfilledThenable: FulfilledThenable<InspectedElementFrontend> =
135 + (thenable: any);
136 + fulfilledThenable.status = 'fulfilled';
137 + fulfilledThenable.value = inspectedElement;
138 wake();
139 },
140
141 error => {
142 console.error(error);
143
143 - const rejectedRecord = ((newRecord: any): RejectedRecord);
144 - rejectedRecord.status = Rejected;
145 - rejectedRecord.value = error;
144 + const rejectedThenable: RejectedThenable<InspectedElementFrontend> =
145 + (thenable: any);
146 + rejectedThenable.status = 'rejected';
147 + rejectedThenable.reason = error;
148
147 - wake();
149 + wakeRejections();
150 },
151 );
152
153 map.set(element, record);
154 }
155
154 - const response = readRecord(record).value;
156 + const response = readRecord(record);
157 return response;
158 }
159
packages/react-devtools-timeline/src/timelineCache.js
+56 -50
@@ -7,46 +7,42 @@
7 * @flow
8 */
9
10 -import type {Wakeable} from 'shared/ReactTypes';
10 +import type {
11 + Thenable,
12 + FulfilledThenable,
13 + RejectedThenable,
14 +} from 'shared/ReactTypes';
15 import type {TimelineData} from './types';
16
13 -import {importFile as importFileWorker} from './import-worker';
14 -
15 -const Pending = 0;
16 -const Resolved = 1;
17 -const Rejected = 2;
18 -
19 -type PendingRecord = {
20 - status: 0,
21 - value: Wakeable,
22 -};
23 -
24 -type ResolvedRecord<T> = {
25 - status: 1,
26 - value: T,
27 -};
17 +import * as React from 'react';
18
29 -type RejectedRecord = {
30 - status: 2,
31 - value: Error,
32 -};
33 -
34 -type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
19 +import {importFile as importFileWorker} from './import-worker';
20
21 // This is intentionally a module-level Map, rather than a React-managed one.
22 // Otherwise, refreshing the inspected element cache would also clear this cache.
23 // Profiler file contents are static anyway.
39 -const fileNameToProfilerDataMap: Map<string, Record<TimelineData>> = new Map();
40 -
41 -function readRecord<T>(record: Record<T>): ResolvedRecord<T> | RejectedRecord {
42 - if (record.status === Resolved) {
43 - // This is just a type refinement.
44 - return record;
45 - } else if (record.status === Rejected) {
46 - // This is just a type refinement.
47 - return record;
24 +const fileNameToProfilerDataMap: Map<
25 + string,
26 + Thenable<TimelineData>,
27 +> = new Map();
28 +
29 +function readRecord<T>(record: Thenable<T>): T | Error {
30 + if (typeof React.use === 'function') {
31 + try {
32 + return React.use(record);
33 + } catch (x) {
34 + if (record.status === 'rejected') {
35 + return (record.reason: any);
36 + }
37 + throw x;
38 + }
39 + }
40 + if (record.status === 'fulfilled') {
41 + return record.value;
42 + } else if (record.status === 'rejected') {
43 + return (record.reason: any);
44 } else {
49 - throw record.value;
45 + throw record;
46 }
47 }
48
@@ -55,10 +51,15 @@ export function importFile(file: File): TimelineData | Error {
51 let record = fileNameToProfilerDataMap.get(fileName);
52
53 if (!record) {
58 - const callbacks = new Set<() => mixed>();
59 - const wakeable: Wakeable = {
60 - then(callback: () => mixed) {
54 + const callbacks = new Set<(value: any) => mixed>();
55 + const rejectCallbacks = new Set<(reason: mixed) => mixed>();
56 + const thenable: Thenable<TimelineData> = {
57 + status: 'pending',
58 + value: null,
59 + reason: null,
60 + then(callback: (value: any) => mixed, reject: (error: mixed) => mixed) {
61 callbacks.add(callback);
62 + rejectCallbacks.add(reject);
63 },
64
65 // Optional property used by Timeline:
@@ -67,37 +68,42 @@ export function importFile(file: File): TimelineData | Error {
68
69 const wake = () => {
70 // This assumes they won't throw.
70 - callbacks.forEach(callback => callback());
71 + callbacks.forEach(callback => callback((thenable: any).value));
72 + callbacks.clear();
73 + rejectCallbacks.clear();
74 + };
75 + const wakeRejections = () => {
76 + // This assumes they won't throw.
77 + rejectCallbacks.forEach(callback => callback((thenable: any).reason));
78 + rejectCallbacks.clear();
79 callbacks.clear();
80 };
81
74 - const newRecord: Record<TimelineData> = (record = {
75 - status: Pending,
76 - value: wakeable,
77 - });
82 + record = thenable;
83
84 importFileWorker(file).then(data => {
85 switch (data.status) {
86 case 'SUCCESS':
82 - const resolvedRecord =
83 - ((newRecord: any): ResolvedRecord<TimelineData>);
84 - resolvedRecord.status = Resolved;
85 - resolvedRecord.value = data.processedData;
87 + const fulfilledThenable: FulfilledThenable<TimelineData> =
88 + (thenable: any);
89 + fulfilledThenable.status = 'fulfilled';
90 + fulfilledThenable.value = data.processedData;
91 + wake();
92 break;
93 case 'INVALID_PROFILE_ERROR':
94 case 'UNEXPECTED_ERROR':
89 - const thrownRecord = ((newRecord: any): RejectedRecord);
90 - thrownRecord.status = Rejected;
91 - thrownRecord.value = data.error;
95 + const rejectedThenable: RejectedThenable<TimelineData> =
96 + (thenable: any);
97 + rejectedThenable.status = 'rejected';
98 + rejectedThenable.reason = data.error;
99 + wakeRejections();
100 break;
101 }
94 -
95 - wake();
102 });
103
104 fileNameToProfilerDataMap.set(fileName, record);
105 }
106
101 - const response = readRecord(record).value;
107 + const response = readRecord(record);
108 return response;
109 }