DevTools: Reset cached indices in Store after elements reordered (#22147)
Brian Vaughn committed
Aug 20, 2021 at 12:53 UTC
75a3e9fa4074b4b18ee95398ceabc361a6c9f82e
2 files changed
+139
-36
packages/react-devtools-shared/src/__tests__/treeContext-test.js
+105
@@ -2177,6 +2177,111 @@ describe('TreeListContext', () => {
2177
`);
2178
});
2179
2180
+ it('should update correctly when elements are re-ordered', () => {
2181
+ const container = document.createElement('div');
2182
+ function ErrorOnce() {
2183
+ const didErroRef = React.useRef(false);
2184
+ if (!didErroRef.current) {
2185
+ didErroRef.current = true;
2186
+ console.error('test-only:one-time-error');
2187
+ }
2188
+ return null;
2189
+ }
2190
+ withErrorsOrWarningsIgnored(['test-only:'], () =>
2191
+ utils.act(() =>
2192
+ legacyRender(
2193
+ <React.Fragment>
2194
+ <Child key="A" />
2195
+ <ErrorOnce key="B" />
2196
+ <Child key="C" />
2197
+ <ErrorOnce key="D" />
2198
+ </React.Fragment>,
2199
+ container,
2200
+ ),
2201
+ ),
2202
+ );
2203
+
2204
+ let renderer;
2205
+ utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
2206
+ expect(state).toMatchInlineSnapshot(`
2207
+ ✕ 2, ⚠ 0
2208
+ [root]
2209
+ <Child key="A">
2210
+ <ErrorOnce key="B"> ✕
2211
+ <Child key="C">
2212
+ <ErrorOnce key="D"> ✕
2213
+ `);
2214
+
2215
+ // Select a child
2216
+ selectNextErrorOrWarning();
2217
+ utils.act(() => renderer.update(<Contexts />));
2218
+ expect(state).toMatchInlineSnapshot(`
2219
+ ✕ 2, ⚠ 0
2220
+ [root]
2221
+ <Child key="A">
2222
+ → <ErrorOnce key="B"> ✕
2223
+ <Child key="C">
2224
+ <ErrorOnce key="D"> ✕
2225
+ `);
2226
+
2227
+ // Re-order the tree and ensure indices are updated.
2228
+ withErrorsOrWarningsIgnored(['test-only:'], () =>
2229
+ utils.act(() =>
2230
+ legacyRender(
2231
+ <React.Fragment>
2232
+ <ErrorOnce key="B" />
2233
+ <Child key="A" />
2234
+ <ErrorOnce key="D" />
2235
+ <Child key="C" />
2236
+ </React.Fragment>,
2237
+ container,
2238
+ ),
2239
+ ),
2240
+ );
2241
+ expect(state).toMatchInlineSnapshot(`
2242
+ ✕ 2, ⚠ 0
2243
+ [root]
2244
+ → <ErrorOnce key="B"> ✕
2245
+ <Child key="A">
2246
+ <ErrorOnce key="D"> ✕
2247
+ <Child key="C">
2248
+ `);
2249
+
2250
+ // Select the next child and ensure the index doesn't break.
2251
+ selectNextErrorOrWarning();
2252
+ expect(state).toMatchInlineSnapshot(`
2253
+ ✕ 2, ⚠ 0
2254
+ [root]
2255
+ <ErrorOnce key="B"> ✕
2256
+ <Child key="A">
2257
+ → <ErrorOnce key="D"> ✕
2258
+ <Child key="C">
2259
+ `);
2260
+
2261
+ // Re-order the tree and ensure indices are updated.
2262
+ withErrorsOrWarningsIgnored(['test-only:'], () =>
2263
+ utils.act(() =>
2264
+ legacyRender(
2265
+ <React.Fragment>
2266
+ <ErrorOnce key="D" />
2267
+ <ErrorOnce key="B" />
2268
+ <Child key="A" />
2269
+ <Child key="C" />
2270
+ </React.Fragment>,
2271
+ container,
2272
+ ),
2273
+ ),
2274
+ );
2275
+ expect(state).toMatchInlineSnapshot(`
2276
+ ✕ 2, ⚠ 0
2277
+ [root]
2278
+ → <ErrorOnce key="D"> ✕
2279
+ <ErrorOnce key="B"> ✕
2280
+ <Child key="A">
2281
+ <Child key="C">
2282
+ `);
2283
+ });
2284
+
2285
it('should update select and auto-expand parts components within hidden parts of the tree', () => {
2286
const Wrapper = ({children}) => children;
2287
packages/react-devtools-shared/src/devtools/store.js
+34
-36
@@ -57,6 +57,8 @@ const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
57
const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
58
'React::DevTools::recordChangeDescriptions';
59
60
+type ErrorAndWarningTuples = Array<{|id: number, index: number|}>;
61
+
62
type Config = {|
63
checkBridgeProtocolCompatibility?: boolean,
64
isProfiling?: boolean,
@@ -94,7 +96,7 @@ export default class Store extends EventEmitter<{|
96
// Computed whenever _errorsAndWarnings Map changes.
97
_cachedErrorCount: number = 0;
98
_cachedWarningCount: number = 0;
97
- _cachedErrorAndWarningTuples: Array<{|id: number, index: number|}> = [];
99
+ _cachedErrorAndWarningTuples: ErrorAndWarningTuples | null = null;
100
101
// Should new nodes be collapsed by default when added to the tree?
102
_collapseNodesByDefault: boolean = true;
@@ -510,7 +512,34 @@ export default class Store extends EventEmitter<{|
512
513
// Returns a tuple of [id, index]
514
getElementsWithErrorsAndWarnings(): Array<{|id: number, index: number|}> {
513
- return this._cachedErrorAndWarningTuples;
515
+ if (this._cachedErrorAndWarningTuples !== null) {
516
+ return this._cachedErrorAndWarningTuples;
517
+ } else {
518
+ const errorAndWarningTuples: ErrorAndWarningTuples = [];
519
+
520
+ this._errorsAndWarnings.forEach((_, id) => {
521
+ const index = this.getIndexOfElementID(id);
522
+ if (index !== null) {
523
+ let low = 0;
524
+ let high = errorAndWarningTuples.length;
525
+ while (low < high) {
526
+ const mid = (low + high) >> 1;
527
+ if (errorAndWarningTuples[mid].index > index) {
528
+ high = mid;
529
+ } else {
530
+ low = mid + 1;
531
+ }
532
+ }
533
+
534
+ errorAndWarningTuples.splice(low, 0, {id, index});
535
+ }
536
+ });
537
+
538
+ // Cache for later (at least until the tree changes again).
539
+ this._cachedErrorAndWarningTuples = errorAndWarningTuples;
540
+
541
+ return errorAndWarningTuples;
542
+ }
543
}
544
545
getErrorAndWarningCountForElementID(
@@ -1124,6 +1153,9 @@ export default class Store extends EventEmitter<{|
1153
1154
this._revision++;
1155
1156
+ // Any time the tree changes (e.g. elements added, removed, or reordered) cached inidices may be invalid.
1157
+ this._cachedErrorAndWarningTuples = null;
1158
+
1159
if (haveErrorsOrWarningsChanged) {
1160
let errorCount = 0;
1161
let warningCount = 0;
@@ -1135,28 +1167,6 @@ export default class Store extends EventEmitter<{|
1167
1168
this._cachedErrorCount = errorCount;
1169
this._cachedWarningCount = warningCount;
1138
-
1139
- const errorAndWarningTuples: Array<{|id: number, index: number|}> = [];
1140
-
1141
- this._errorsAndWarnings.forEach((_, id) => {
1142
- const index = this.getIndexOfElementID(id);
1143
- if (index !== null) {
1144
- let low = 0;
1145
- let high = errorAndWarningTuples.length;
1146
- while (low < high) {
1147
- const mid = (low + high) >> 1;
1148
- if (errorAndWarningTuples[mid].index > index) {
1149
- high = mid;
1150
- } else {
1151
- low = mid + 1;
1152
- }
1153
- }
1154
-
1155
- errorAndWarningTuples.splice(low, 0, {id, index});
1156
- }
1157
- });
1158
-
1159
- this._cachedErrorAndWarningTuples = errorAndWarningTuples;
1170
}
1171
1172
if (haveRootsChanged) {
@@ -1187,18 +1197,6 @@ export default class Store extends EventEmitter<{|
1197
console.groupEnd();
1198
}
1199
1190
- const indicesOfCachedErrorsOrWarningsAreStale =
1191
- !haveErrorsOrWarningsChanged &&
1192
- (addedElementIDs.length > 0 || removedElementIDs.size > 0);
1193
- if (indicesOfCachedErrorsOrWarningsAreStale) {
1194
- this._cachedErrorAndWarningTuples.forEach(entry => {
1195
- const index = this.getIndexOfElementID(entry.id);
1196
- if (index !== null) {
1197
- entry.index = index;
1198
- }
1199
- });
1200
- }
1201
-
1200
this.emit('mutated', [addedElementIDs, removedElementIDs]);
1201
};
1202