@samitouri / QOS-React-2 / commits / 47177247f8

DevTools: Fixed potential cache miss when insepcting elements (#22472)

Brian Vaughn committed Sep 30, 2021 at 12:48 UTC 47177247f8c792fbb9a33328c913e246ff139a2d
8 files changed +89 -8
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+61
@@ -384,6 +384,67 @@ describe('InspectedElement', () => {
384 `);
385 });
386
387 + // See github.com/facebook/react/issues/22241#issuecomment-931299972
388 + it('should properly recover from a cache miss on the frontend', async () => {
389 + let targetRenderCount = 0;
390 +
391 + const Wrapper = ({children}) => children;
392 + const Target = React.memo(props => {
393 + targetRenderCount++;
394 + // Even though his hook isn't referenced, it's used to observe backend rendering.
395 + React.useState(0);
396 + return null;
397 + });
398 +
399 + const container = document.createElement('div');
400 + await utils.actAsync(() =>
401 + legacyRender(
402 + <Wrapper>
403 + <Target a={1} b="abc" />
404 + </Wrapper>,
405 + container,
406 + ),
407 + );
408 +
409 + targetRenderCount = 0;
410 +
411 + let inspectedElement = await inspectElementAtIndex(1);
412 + expect(targetRenderCount).toBe(1);
413 + expect(inspectedElement.props).toMatchInlineSnapshot(`
414 + Object {
415 + "a": 1,
416 + "b": "abc",
417 + }
418 + `);
419 +
420 + const prevInspectedElement = inspectedElement;
421 +
422 + // This test causes an intermediate error to be logged but we can ignore it.
423 + console.error = () => {};
424 +
425 + // Wait for our check-for-updates poll to get the new data.
426 + jest.runOnlyPendingTimers();
427 + await Promise.resolve();
428 +
429 + // Clear the frontend cache to simulate DevTools being closed and re-opened.
430 + // The backend still thinks the most recently-inspected element is still cached,
431 + // so the frontend needs to tell it to resend a full value.
432 + // We can verify this by asserting that the component is re-rendered again.
433 + testRendererInstance = TestRenderer.create(null, {
434 + unstable_isConcurrent: true,
435 + });
436 +
437 + const {
438 + clearCacheForTests,
439 + } = require('react-devtools-shared/src/inspectedElementMutableSource');
440 + clearCacheForTests();
441 +
442 + targetRenderCount = 0;
443 + inspectedElement = await inspectElementAtIndex(1);
444 + expect(targetRenderCount).toBe(1);
445 + expect(inspectedElement).toEqual(prevInspectedElement);
446 + });
447 +
448 it('should temporarily disable console logging when re-running a component to inspect its hooks', async () => {
449 let targetRenderCount = 0;
450
packages/react-devtools-shared/src/backend/agent.js
+3 -1
@@ -72,6 +72,7 @@ type CopyElementParams = {|
72 |};
73
74 type InspectElementParams = {|
75 + forceFullData: boolean,
76 id: number,
77 path: Array<string | number> | null,
78 rendererID: number,
@@ -346,6 +347,7 @@ export default class Agent extends EventEmitter<{|
347 };
348
349 inspectElement = ({
350 + forceFullData,
351 id,
352 path,
353 rendererID,
@@ -357,7 +359,7 @@ export default class Agent extends EventEmitter<{|
359 } else {
360 this._bridge.send(
361 'inspectedElement',
360 - renderer.inspectElement(requestID, id, path),
362 + renderer.inspectElement(requestID, id, path, forceFullData),
363 );
364
365 // When user selects an element, stop trying to restore the selection,
packages/react-devtools-shared/src/backend/legacy/renderer.js
+2 -1
@@ -694,8 +694,9 @@ export function attach(
694 requestID: number,
695 id: number,
696 path: Array<string | number> | null,
697 + forceFullData: boolean,
698 ): InspectedElementPayload {
698 - if (currentlyInspectedElementID !== id) {
699 + if (forceFullData || currentlyInspectedElementID !== id) {
700 currentlyInspectedElementID = id;
701 currentlyInspectedPaths = {};
702 }
packages/react-devtools-shared/src/backend/renderer.js
+2 -1
@@ -3439,12 +3439,13 @@ export function attach(
3439 requestID: number,
3440 id: number,
3441 path: Array<string | number> | null,
3442 + forceFullData: boolean,
3443 ): InspectedElementPayload {
3444 if (path !== null) {
3445 mergeInspectedPaths(path);
3446 }
3447
3447 - if (isMostRecentlyInspectedElement(id)) {
3448 + if (isMostRecentlyInspectedElement(id) && !forceFullData) {
3449 if (!hasElementUpdatedSinceLastInspected) {
3450 if (path !== null) {
3451 let secondaryCategory = null;
packages/react-devtools-shared/src/backend/types.js
+1
@@ -339,6 +339,7 @@ export type RendererInterface = {
339 requestID: number,
340 id: number,
341 inspectedPaths: Object,
342 + forceFullData: boolean,
343 ) => InspectedElementPayload,
344 logElementToConsole: (id: number) => void,
345 overrideError: (id: number, forceError: boolean) => void,
packages/react-devtools-shared/src/backendAPI.js
+3
@@ -86,11 +86,13 @@ export function copyInspectedElementPath({
86
87 export function inspectElement({
88 bridge,
89 + forceFullData,
90 id,
91 path,
92 rendererID,
93 }: {|
94 bridge: FrontendBridge,
95 + forceFullData: boolean,
96 id: number,
97 path: Array<string | number> | null,
98 rendererID: number,
@@ -103,6 +105,7 @@ export function inspectElement({
105 );
106
107 bridge.send('inspectElement', {
108 + forceFullData,
109 id,
110 path,
111 rendererID,
packages/react-devtools-shared/src/bridge.js
+1
@@ -138,6 +138,7 @@ type ViewAttributeSourceParams = {|
138
139 type InspectElementParams = {|
140 ...ElementAndRendererID,
141 + forceFullData: boolean,
142 path: Array<number | string> | null,
143 requestID: number,
144 |};
packages/react-devtools-shared/src/inspectedElementMutableSource.js
+16 -5
@@ -62,8 +62,15 @@ export function inspectElement({
62 rendererID: number,
63 |}): Promise<InspectElementReturnType> {
64 const {id} = element;
65 +
66 + // This could indicate that the DevTools UI has been closed and reopened.
67 + // The in-memory cache will be clear but the backend still thinks we have cached data.
68 + // In this case, we need to tell it to resend the full data.
69 + const forceFullData = !inspectedElementCache.has(id);
70 +
71 return inspectElementAPI({
72 bridge,
73 + forceFullData,
74 id,
75 path,
76 rendererID,
@@ -74,7 +81,7 @@ export function inspectElement({
81 switch (type) {
82 case 'no-change':
83 // This is a no-op for the purposes of our cache.
77 - inspectedElement = inspectedElementCache.get(element.id);
84 + inspectedElement = inspectedElementCache.get(id);
85 if (inspectedElement != null) {
86 return [inspectedElement, type];
87 }
@@ -85,7 +92,7 @@ export function inspectElement({
92 case 'not-found':
93 // This is effectively a no-op.
94 // If the Element is still in the Store, we can eagerly remove it from the Map.
88 - inspectedElementCache.remove(element.id);
95 + inspectedElementCache.remove(id);
96
97 throw Error(`Element "${id}" not found`);
98
@@ -98,7 +105,7 @@ export function inspectElement({
105 fullData.value,
106 );
107
101 - inspectedElementCache.set(element.id, inspectedElement);
108 + inspectedElementCache.set(id, inspectedElement);
109
110 return [inspectedElement, type];
111
@@ -108,7 +115,7 @@ export function inspectElement({
115
116 // A path has been hydrated.
117 // Merge it with the latest copy we have locally and resolve with the merged value.
111 - inspectedElement = inspectedElementCache.get(element.id) || null;
118 + inspectedElement = inspectedElementCache.get(id) || null;
119 if (inspectedElement !== null) {
120 // Clone element
121 inspectedElement = {...inspectedElement};
@@ -121,7 +128,7 @@ export function inspectElement({
128 hydrateHelper(value, ((path: any): Path)),
129 );
130
124 - inspectedElementCache.set(element.id, inspectedElement);
131 + inspectedElementCache.set(id, inspectedElement);
132
133 return [inspectedElement, type];
134 }
@@ -140,3 +147,7 @@ export function inspectElement({
147 throw Error(`Unable to inspect element with id "${id}"`);
148 });
149 }
150 +
151 +export function clearCacheForTests(): void {
152 + inspectedElementCache.reset();
153 +}