Skip to content

Commit 877037f

Browse files
author
Brian Vaughn
committed
Cleaned up renderer code and added additional unit test
1 parent 8716606 commit 877037f

File tree

2 files changed

+137
-78
lines changed

2 files changed

+137
-78
lines changed

packages/react-devtools-shared/src/__tests__/store-test.js

+99-32
Original file line numberDiff line numberDiff line change
@@ -1075,43 +1075,110 @@ describe('Store', () => {
10751075
`);
10761076
});
10771077

1078-
it('during passive get counted (after a delay)', () => {
1079-
function Example() {
1080-
React.useEffect(() => {
1081-
console.error('test-only: passive error');
1082-
console.warn('test-only: passive warning');
1083-
});
1084-
return null;
1078+
describe('during passive effects', () => {
1079+
function flushPendingBridgeOperations() {
1080+
jest.runOnlyPendingTimers();
10851081
}
1086-
const container = document.createElement('div');
1087-
1088-
withErrorsOrWarningsIgnored(['test-only:'], () => {
1089-
act(() => {
1090-
ReactDOM.render(<Example />, container);
1091-
// flush bridge operations
1092-
jest.runOnlyPendingTimers();
1093-
}, false);
1094-
});
10951082

1096-
expect(store).toMatchInlineSnapshot(`
1097-
[root]
1098-
<Example>
1099-
`);
1100-
1101-
// flush count after delay
1102-
act(() => {
1083+
// Gross abstraction around pending passive warning/error delay.
1084+
function flushPendingPassiveErrorAndWarningCounts() {
11031085
jest.advanceTimersByTime(1000);
1104-
}, false);
1086+
}
11051087

1106-
// After a delay, passive effects should be committed as well
1107-
expect(store).toMatchInlineSnapshot(`
1108-
✕ 1, ⚠ 1
1109-
[root]
1110-
<Example> ✕⚠
1111-
`);
1088+
it('are counted (after a delay)', () => {
1089+
function Example() {
1090+
React.useEffect(() => {
1091+
console.error('test-only: passive error');
1092+
console.warn('test-only: passive warning');
1093+
});
1094+
return null;
1095+
}
1096+
const container = document.createElement('div');
1097+
1098+
withErrorsOrWarningsIgnored(['test-only:'], () => {
1099+
act(() => {
1100+
ReactDOM.render(<Example />, container);
1101+
}, false);
1102+
});
1103+
flushPendingBridgeOperations();
1104+
expect(store).toMatchInlineSnapshot(`
1105+
[root]
1106+
<Example>
1107+
`);
1108+
1109+
// After a delay, passive effects should be committed as well
1110+
act(flushPendingPassiveErrorAndWarningCounts, false);
1111+
expect(store).toMatchInlineSnapshot(`
1112+
✕ 1, ⚠ 1
1113+
[root]
1114+
<Example> ✕⚠
1115+
`);
1116+
1117+
act(() => ReactDOM.unmountComponentAtNode(container));
1118+
expect(store).toMatchInlineSnapshot(``);
1119+
});
11121120

1113-
act(() => ReactDOM.unmountComponentAtNode(container));
1114-
expect(store).toMatchInlineSnapshot(``);
1121+
it('are flushed early when there is a new commit', () => {
1122+
function Example() {
1123+
React.useEffect(() => {
1124+
console.error('test-only: passive error');
1125+
console.warn('test-only: passive warning');
1126+
});
1127+
return null;
1128+
}
1129+
1130+
function Noop() {
1131+
return null;
1132+
}
1133+
1134+
const container = document.createElement('div');
1135+
1136+
withErrorsOrWarningsIgnored(['test-only:'], () => {
1137+
act(() => {
1138+
ReactDOM.render(
1139+
<>
1140+
<Example />
1141+
</>,
1142+
container,
1143+
);
1144+
}, false);
1145+
flushPendingBridgeOperations();
1146+
expect(store).toMatchInlineSnapshot(`
1147+
[root]
1148+
<Example>
1149+
`);
1150+
1151+
// Before warnings and errors have flushed, flush another commit.
1152+
act(() => {
1153+
ReactDOM.render(
1154+
<>
1155+
<Example />
1156+
<Noop />
1157+
</>,
1158+
container,
1159+
);
1160+
}, false);
1161+
flushPendingBridgeOperations();
1162+
expect(store).toMatchInlineSnapshot(`
1163+
✕ 1, ⚠ 1
1164+
[root]
1165+
<Example> ✕⚠
1166+
<Noop>
1167+
`);
1168+
});
1169+
1170+
// After a delay, passive effects should be committed as well
1171+
act(flushPendingPassiveErrorAndWarningCounts, false);
1172+
expect(store).toMatchInlineSnapshot(`
1173+
✕ 2, ⚠ 2
1174+
[root]
1175+
<Example> ✕⚠
1176+
<Noop>
1177+
`);
1178+
1179+
act(() => ReactDOM.unmountComponentAtNode(container));
1180+
expect(store).toMatchInlineSnapshot(``);
1181+
});
11151182
});
11161183

11171184
it('from react get counted', () => {

packages/react-devtools-shared/src/backend/renderer.js

+38-46
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,8 @@ export function attach(
631631
// In this case, we should wait until the rest of the passive effects have run,
632632
// but we shouldn't wait until the next commit because that might be a long time.
633633
// This would also cause "tearing" between an inspected Component and the tree view.
634-
// Then again we don't want to flush too soon because rendering might stil be going on.
634+
// Then again we don't want to flush too soon because this could be an error during async rendering.
635+
// Use a debounce technique to ensure that we'll eventually flush.
635636
flushPendingErrorsAndWarningsAfterDelay();
636637
}
637638

@@ -1205,10 +1206,12 @@ export function attach(
12051206
}
12061207
}
12071208

1208-
const pendingOperations: Array<number> = [];
1209+
type OperationsArray = Array<number>;
1210+
1211+
const pendingOperations: OperationsArray = [];
12091212
const pendingRealUnmountedIDs: Array<number> = [];
12101213
const pendingSimulatedUnmountedIDs: Array<number> = [];
1211-
let pendingOperationsQueue: Array<Array<number>> | null = [];
1214+
let pendingOperationsQueue: Array<OperationsArray> | null = [];
12121215
const pendingStringTable: Map<string, number> = new Map();
12131216
let pendingStringTableLength: number = 0;
12141217
let pendingUnmountedRootID: number | null = null;
@@ -1225,55 +1228,56 @@ export function attach(
12251228
pendingOperations.push(op);
12261229
}
12271230

1231+
function flushOrQueueOperations(operations: OperationsArray): void {
1232+
if (pendingOperationsQueue !== null) {
1233+
pendingOperationsQueue.push(operations);
1234+
} else {
1235+
hook.emit('operations', operations);
1236+
}
1237+
}
1238+
12281239
let flushPendingErrorsAndWarningsAfterDelayTimeoutID = null;
1229-
function flushPendingErrorsAndWarningsAfterDelay() {
1240+
1241+
function clearPendingErrorsAndWarningsAfterDelay() {
12301242
if (flushPendingErrorsAndWarningsAfterDelayTimeoutID !== null) {
12311243
clearTimeout(flushPendingErrorsAndWarningsAfterDelayTimeoutID);
1244+
flushPendingErrorsAndWarningsAfterDelayTimeoutID = null;
12321245
}
1246+
}
1247+
1248+
function flushPendingErrorsAndWarningsAfterDelay() {
1249+
clearPendingErrorsAndWarningsAfterDelay();
12331250

12341251
flushPendingErrorsAndWarningsAfterDelayTimeoutID = setTimeout(() => {
12351252
flushPendingErrorsAndWarningsAfterDelayTimeoutID = null;
12361253

12371254
if (pendingOperations.length > 0) {
1238-
// On the off chance that somethign else has pushed pending operations,
1255+
// On the off chance that something else has pushed pending operations,
12391256
// we should bail on warnings; it's probably not safe to push midway.
12401257
return;
12411258
}
12421259

12431260
recordPendingErrorsAndWarnings();
12441261

12451262
if (pendingOperations.length === 0) {
1246-
// No warnings or errors to flush.
1263+
// No warnings or errors to flush; we can bail out early here too.
12471264
return;
12481265
}
12491266

1250-
const operations = new Array(2 + 1 + pendingOperations.length);
1251-
1252-
// Identify which renderer this update is coming from.
1253-
// This enables roots to be mapped to renderers,
1254-
// Which in turn enables fiber props, states, and hooks to be inspected.
1255-
let i = 0;
1256-
operations[i++] = rendererID;
1257-
operations[i++] = currentRootID; // Use this ID in case the root was unmounted!
1258-
operations[i++] = 0; // String table size
1259-
1260-
// Fill in the rest of the operations.
1267+
// We can create a smaller operations array than flushPendingEvents()
1268+
// because we only need to flush warning and error counts.
1269+
// Only a few pieces of fixed information are required up front.
1270+
const operations: OperationsArray = new Array(
1271+
3 + pendingOperations.length,
1272+
);
1273+
operations[0] = rendererID;
1274+
operations[1] = currentRootID;
1275+
operations[2] = 0; // String table size
12611276
for (let j = 0; j < pendingOperations.length; j++) {
1262-
operations[i + j] = pendingOperations[j];
1277+
operations[3 + j] = pendingOperations[j];
12631278
}
12641279

1265-
// Let the frontend know about tree operations.
1266-
// The first value in this array will identify which root it corresponds to,
1267-
// so we do no longer need to dispatch a separate root-committed event.
1268-
if (pendingOperationsQueue !== null) {
1269-
// Until the frontend has been connected, store the tree operations.
1270-
// This will let us avoid walking the tree later when the frontend connects,
1271-
// and it enables the Profiler's reload-and-profile functionality to work as well.
1272-
pendingOperationsQueue.push(operations);
1273-
} else {
1274-
// If we've already connected to the frontend, just pass the operations through.
1275-
hook.emit('operations', operations);
1276-
}
1280+
flushOrQueueOperations(operations);
12771281

12781282
pendingOperations.length = 0;
12791283
}, 1000);
@@ -1291,10 +1295,7 @@ export function attach(
12911295
}
12921296

12931297
function recordPendingErrorsAndWarnings() {
1294-
if (flushPendingErrorsAndWarningsAfterDelayTimeoutID !== null) {
1295-
clearTimeout(flushPendingErrorsAndWarningsAfterDelayTimeoutID);
1296-
flushPendingErrorsAndWarningsAfterDelayTimeoutID = null;
1297-
}
1298+
clearPendingErrorsAndWarningsAfterDelay();
12981299

12991300
fibersWithChangedErrorOrWarningCounts.forEach(fiberID => {
13001301
const fiber = idToFiberMap.get(fiberID);
@@ -1385,7 +1386,7 @@ export function attach(
13851386
// Which in turn enables fiber props, states, and hooks to be inspected.
13861387
let i = 0;
13871388
operations[i++] = rendererID;
1388-
operations[i++] = currentRootID; // Use this ID in case the root was unmounted!
1389+
operations[i++] = currentRootID;
13891390

13901391
// Now fill in the string table.
13911392
// [stringTableLength, str1Length, ...str1, str2Length, ...str2, ...]
@@ -1432,18 +1433,9 @@ export function attach(
14321433
i += pendingOperations.length;
14331434

14341435
// Let the frontend know about tree operations.
1435-
// The first value in this array will identify which root it corresponds to,
1436-
// so we do no longer need to dispatch a separate root-committed event.
1437-
if (pendingOperationsQueue !== null) {
1438-
// Until the frontend has been connected, store the tree operations.
1439-
// This will let us avoid walking the tree later when the frontend connects,
1440-
// and it enables the Profiler's reload-and-profile functionality to work as well.
1441-
pendingOperationsQueue.push(operations);
1442-
} else {
1443-
// If we've already connected to the frontend, just pass the operations through.
1444-
hook.emit('operations', operations);
1445-
}
1436+
flushOrQueueOperations(operations);
14461437

1438+
// Reset all of the pending state now that we've told the frontend about it.
14471439
pendingOperations.length = 0;
14481440
pendingRealUnmountedIDs.length = 0;
14491441
pendingSimulatedUnmountedIDs.length = 0;

0 commit comments

Comments
 (0)