Skip to content

Commit be228eb

Browse files
Brian Vaughnzhengjitf
authored andcommitted
DevTools: Reset cached indices in Store after elements reordered (facebook#22147)
1 parent b39d0ce commit be228eb

File tree

2 files changed

+139
-36
lines changed

2 files changed

+139
-36
lines changed

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2177,6 +2177,111 @@ describe('TreeListContext', () => {
21772177
`);
21782178
});
21792179

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+
21802285
it('should update select and auto-expand parts components within hidden parts of the tree', () => {
21812286
const Wrapper = ({children}) => children;
21822287

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

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
5757
const LOCAL_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =
5858
'React::DevTools::recordChangeDescriptions';
5959

60+
type ErrorAndWarningTuples = Array<{|id: number, index: number|}>;
61+
6062
type Config = {|
6163
checkBridgeProtocolCompatibility?: boolean,
6264
isProfiling?: boolean,
@@ -94,7 +96,7 @@ export default class Store extends EventEmitter<{|
9496
// Computed whenever _errorsAndWarnings Map changes.
9597
_cachedErrorCount: number = 0;
9698
_cachedWarningCount: number = 0;
97-
_cachedErrorAndWarningTuples: Array<{|id: number, index: number|}> = [];
99+
_cachedErrorAndWarningTuples: ErrorAndWarningTuples | null = null;
98100

99101
// Should new nodes be collapsed by default when added to the tree?
100102
_collapseNodesByDefault: boolean = true;
@@ -510,7 +512,34 @@ export default class Store extends EventEmitter<{|
510512

511513
// Returns a tuple of [id, index]
512514
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+
}
514543
}
515544

516545
getErrorAndWarningCountForElementID(
@@ -1124,6 +1153,9 @@ export default class Store extends EventEmitter<{|
11241153

11251154
this._revision++;
11261155

1156+
// Any time the tree changes (e.g. elements added, removed, or reordered) cached inidices may be invalid.
1157+
this._cachedErrorAndWarningTuples = null;
1158+
11271159
if (haveErrorsOrWarningsChanged) {
11281160
let errorCount = 0;
11291161
let warningCount = 0;
@@ -1135,28 +1167,6 @@ export default class Store extends EventEmitter<{|
11351167

11361168
this._cachedErrorCount = errorCount;
11371169
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;
11601170
}
11611171

11621172
if (haveRootsChanged) {
@@ -1187,18 +1197,6 @@ export default class Store extends EventEmitter<{|
11871197
console.groupEnd();
11881198
}
11891199

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-
12021200
this.emit('mutated', [addedElementIDs, removedElementIDs]);
12031201
};
12041202

0 commit comments

Comments
 (0)