@samitouri / QOS-React-2 / commits / 75a3e9fa40

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