Skip to content

Commit 7268dac

Browse files
authored
Devtools: Ensure component control flow is consistent with commit when using `useDeferredValue (#28508)
1 parent a540f53 commit 7268dac

File tree

2 files changed

+261
-2
lines changed

2 files changed

+261
-2
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -444,14 +444,15 @@ function useTransition(): [
444444

445445
function useDeferredValue<T>(value: T, initialValue?: T): T {
446446
const hook = nextHook();
447+
const prevValue = hook !== null ? hook.memoizedState : value;
447448
hookLog.push({
448449
displayName: null,
449450
primitive: 'DeferredValue',
450451
stackError: new Error(),
451-
value: hook !== null ? hook.memoizedState : value,
452+
value: prevValue,
452453
debugInfo: null,
453454
});
454-
return value;
455+
return prevValue;
455456
}
456457

457458
function useId(): string {

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

+258
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,264 @@ describe('ReactHooksInspectionIntegration', () => {
12061206
`);
12071207
});
12081208

1209+
it('should return the deferred value', async () => {
1210+
let unsuspend;
1211+
function Lazy() {
1212+
return 'Lazy';
1213+
}
1214+
const Suspender = React.lazy(
1215+
() =>
1216+
new Promise(resolve => {
1217+
unsuspend = () => resolve({default: Lazy});
1218+
}),
1219+
);
1220+
const Context = React.createContext('default');
1221+
let setShow;
1222+
function Foo(props) {
1223+
const [show, _setShow] = React.useState(false);
1224+
const deferredShow = React.useDeferredValue(show);
1225+
const isPending = show !== deferredShow;
1226+
const contextDisplay = isPending ? React.use(Context) : '<none>';
1227+
React.useMemo(() => 'hello', []);
1228+
React.useMemo(() => 'not used', []);
1229+
1230+
// Otherwise we capture the version from the react-debug-tools dispatcher.
1231+
if (setShow === undefined) {
1232+
setShow = _setShow;
1233+
}
1234+
1235+
return (
1236+
<React.Suspense fallback="Loading">
1237+
Context: {contextDisplay}, {isPending ? 'Pending' : 'Nothing Pending'}
1238+
{deferredShow ? [', ', <Suspender key="suspender" />] : null}
1239+
</React.Suspense>
1240+
);
1241+
}
1242+
const renderer = await act(() => {
1243+
return ReactTestRenderer.create(
1244+
<Context.Provider value="provided">
1245+
<Foo />
1246+
</Context.Provider>,
1247+
{isConcurrent: true},
1248+
);
1249+
});
1250+
let childFiber = renderer.root.findByType(Foo)._currentFiber();
1251+
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
1252+
expect(renderer).toMatchRenderedOutput('Context: <none>, Nothing Pending');
1253+
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
1254+
[
1255+
{
1256+
"debugInfo": null,
1257+
"hookSource": {
1258+
"columnNumber": 0,
1259+
"fileName": "**",
1260+
"functionName": "Foo",
1261+
"lineNumber": 0,
1262+
},
1263+
"id": 0,
1264+
"isStateEditable": true,
1265+
"name": "State",
1266+
"subHooks": [],
1267+
"value": false,
1268+
},
1269+
{
1270+
"debugInfo": null,
1271+
"hookSource": {
1272+
"columnNumber": 0,
1273+
"fileName": "**",
1274+
"functionName": "Foo",
1275+
"lineNumber": 0,
1276+
},
1277+
"id": 1,
1278+
"isStateEditable": false,
1279+
"name": "DeferredValue",
1280+
"subHooks": [],
1281+
"value": false,
1282+
},
1283+
{
1284+
"debugInfo": null,
1285+
"hookSource": {
1286+
"columnNumber": 0,
1287+
"fileName": "**",
1288+
"functionName": "Foo",
1289+
"lineNumber": 0,
1290+
},
1291+
"id": 2,
1292+
"isStateEditable": false,
1293+
"name": "Memo",
1294+
"subHooks": [],
1295+
"value": "hello",
1296+
},
1297+
{
1298+
"debugInfo": null,
1299+
"hookSource": {
1300+
"columnNumber": 0,
1301+
"fileName": "**",
1302+
"functionName": "Foo",
1303+
"lineNumber": 0,
1304+
},
1305+
"id": 3,
1306+
"isStateEditable": false,
1307+
"name": "Memo",
1308+
"subHooks": [],
1309+
"value": "not used",
1310+
},
1311+
]
1312+
`);
1313+
1314+
await act(() => {
1315+
setShow(true);
1316+
});
1317+
1318+
expect(renderer).toMatchRenderedOutput('Context: provided, Pending');
1319+
childFiber = renderer.root.findByType(Foo)._currentFiber();
1320+
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
1321+
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
1322+
[
1323+
{
1324+
"debugInfo": null,
1325+
"hookSource": {
1326+
"columnNumber": 0,
1327+
"fileName": "**",
1328+
"functionName": "Foo",
1329+
"lineNumber": 0,
1330+
},
1331+
"id": 0,
1332+
"isStateEditable": true,
1333+
"name": "State",
1334+
"subHooks": [],
1335+
"value": true,
1336+
},
1337+
{
1338+
"debugInfo": null,
1339+
"hookSource": {
1340+
"columnNumber": 0,
1341+
"fileName": "**",
1342+
"functionName": "Foo",
1343+
"lineNumber": 0,
1344+
},
1345+
"id": 1,
1346+
"isStateEditable": false,
1347+
"name": "DeferredValue",
1348+
"subHooks": [],
1349+
"value": false,
1350+
},
1351+
{
1352+
"debugInfo": null,
1353+
"hookSource": {
1354+
"columnNumber": 0,
1355+
"fileName": "**",
1356+
"functionName": "Foo",
1357+
"lineNumber": 0,
1358+
},
1359+
"id": null,
1360+
"isStateEditable": false,
1361+
"name": "Context",
1362+
"subHooks": [],
1363+
"value": "provided",
1364+
},
1365+
{
1366+
"debugInfo": null,
1367+
"hookSource": {
1368+
"columnNumber": 0,
1369+
"fileName": "**",
1370+
"functionName": "Foo",
1371+
"lineNumber": 0,
1372+
},
1373+
"id": 2,
1374+
"isStateEditable": false,
1375+
"name": "Memo",
1376+
"subHooks": [],
1377+
"value": "hello",
1378+
},
1379+
{
1380+
"debugInfo": null,
1381+
"hookSource": {
1382+
"columnNumber": 0,
1383+
"fileName": "**",
1384+
"functionName": "Foo",
1385+
"lineNumber": 0,
1386+
},
1387+
"id": 3,
1388+
"isStateEditable": false,
1389+
"name": "Memo",
1390+
"subHooks": [],
1391+
"value": "not used",
1392+
},
1393+
]
1394+
`);
1395+
1396+
await act(() => {
1397+
unsuspend();
1398+
});
1399+
1400+
expect(renderer).toMatchRenderedOutput(
1401+
'Context: <none>, Nothing Pending, Lazy',
1402+
);
1403+
childFiber = renderer.root.findByType(Foo)._currentFiber();
1404+
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
1405+
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
1406+
[
1407+
{
1408+
"debugInfo": null,
1409+
"hookSource": {
1410+
"columnNumber": 0,
1411+
"fileName": "**",
1412+
"functionName": "Foo",
1413+
"lineNumber": 0,
1414+
},
1415+
"id": 0,
1416+
"isStateEditable": true,
1417+
"name": "State",
1418+
"subHooks": [],
1419+
"value": true,
1420+
},
1421+
{
1422+
"debugInfo": null,
1423+
"hookSource": {
1424+
"columnNumber": 0,
1425+
"fileName": "**",
1426+
"functionName": "Foo",
1427+
"lineNumber": 0,
1428+
},
1429+
"id": 1,
1430+
"isStateEditable": false,
1431+
"name": "DeferredValue",
1432+
"subHooks": [],
1433+
"value": true,
1434+
},
1435+
{
1436+
"debugInfo": null,
1437+
"hookSource": {
1438+
"columnNumber": 0,
1439+
"fileName": "**",
1440+
"functionName": "Foo",
1441+
"lineNumber": 0,
1442+
},
1443+
"id": 2,
1444+
"isStateEditable": false,
1445+
"name": "Memo",
1446+
"subHooks": [],
1447+
"value": "hello",
1448+
},
1449+
{
1450+
"debugInfo": null,
1451+
"hookSource": {
1452+
"columnNumber": 0,
1453+
"fileName": "**",
1454+
"functionName": "Foo",
1455+
"lineNumber": 0,
1456+
},
1457+
"id": 3,
1458+
"isStateEditable": false,
1459+
"name": "Memo",
1460+
"subHooks": [],
1461+
"value": "not used",
1462+
},
1463+
]
1464+
`);
1465+
});
1466+
12091467
it('should support useId hook', () => {
12101468
function Foo(props) {
12111469
const id = React.useId();

0 commit comments

Comments
 (0)