Devtools: Ensure component control flow is consistent with commit when using `useDeferredValue (#28508)
Sebastian Silbermann committed
Mar 14, 2024 at 15:41 UTC
7268dacf70cd6a7f5705a19053aad46b5289ab72
2 files changed
+261
-2
packages/react-debug-tools/src/ReactDebugHooks.js
+3
-2
@@ -444,14 +444,15 @@ function useTransition(): [
444
445
function useDeferredValue<T>(value: T, initialValue?: T): T {
446
const hook = nextHook();
447
+ const prevValue = hook !== null ? hook.memoizedState : value;
448
hookLog.push({
449
displayName: null,
450
primitive: 'DeferredValue',
451
stackError: new Error(),
451
- value: hook !== null ? hook.memoizedState : value,
452
+ value: prevValue,
453
debugInfo: null,
454
});
454
- return value;
455
+ return prevValue;
456
}
457
458
function useId(): string {
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+258
@@ -1206,6 +1206,264 @@ describe('ReactHooksInspectionIntegration', () => {
1206
`);
1207
});
1208
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
+
1467
it('should support useId hook', () => {
1468
function Foo(props) {
1469
const id = React.useId();