Cache inspected element data until it is updated
Brian Vaughn committed
May 24, 2019 at 07:37 UTC
07db37f0305e06f6d87caf1adff44bbbb5e4156a
3 files changed
+187
-9
src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+52
@@ -51,6 +51,58 @@ exports[`InspectedElementContext should inspect the currently selected element:
51
}
52
`;
53
54
+exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: mount 1`] = `
55
+[root]
56
+ ▾ <Wrapper>
57
+ <Anonymous>
58
+`;
59
+
60
+exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: initial render 1`] = `
61
+{
62
+ "id": 3,
63
+ "owners": null,
64
+ "context": null,
65
+ "events": null,
66
+ "hooks": [
67
+ {
68
+ "id": 0,
69
+ "isStateEditable": true,
70
+ "name": "State",
71
+ "value": 0,
72
+ "subHooks": []
73
+ }
74
+ ],
75
+ "props": {
76
+ "foo": 1,
77
+ "bar": "abc"
78
+ },
79
+ "state": null
80
+}
81
+`;
82
+
83
+exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 3: updated state 1`] = `
84
+{
85
+ "id": 3,
86
+ "owners": null,
87
+ "context": null,
88
+ "events": null,
89
+ "hooks": [
90
+ {
91
+ "id": 0,
92
+ "isStateEditable": true,
93
+ "name": "State",
94
+ "value": 0,
95
+ "subHooks": []
96
+ }
97
+ ],
98
+ "props": {
99
+ "foo": 2,
100
+ "bar": "def"
101
+ },
102
+ "state": null
103
+}
104
+`;
105
+
106
exports[`InspectedElementContext should poll for updates for the currently selected element: 1: mount 1`] = `
107
[root]
108
<Example>
src/__tests__/inspectedElementContext-test.js
+90
@@ -159,4 +159,94 @@ describe('InspectedElementContext', () => {
159
160
done();
161
});
162
+
163
+ it('should not re-render a function with hooks if it did not update since it was last inspected', async done => {
164
+ let targetRenderCount = 0;
165
+
166
+ const Wrapper = ({ children }) => children;
167
+ const Target = React.memo(props => {
168
+ targetRenderCount++;
169
+ React.useState(0);
170
+ return null;
171
+ });
172
+
173
+ const container = document.createElement('div');
174
+ utils.act(() =>
175
+ ReactDOM.render(
176
+ <Wrapper>
177
+ <Target foo={1} bar="abc" />
178
+ </Wrapper>,
179
+ container
180
+ )
181
+ );
182
+ expect(store).toMatchSnapshot('1: mount');
183
+
184
+ const id = ((store.getElementIDAtIndex(1): any): number);
185
+
186
+ let inspectedElement = null;
187
+
188
+ function Suspender({ target }) {
189
+ const { read } = React.useContext(InspectedElementContext);
190
+ inspectedElement = read(target);
191
+ return null;
192
+ }
193
+
194
+ targetRenderCount = 0;
195
+
196
+ let renderer;
197
+ await utils.actAsync(
198
+ () =>
199
+ (renderer = TestRenderer.create(
200
+ <Contexts
201
+ defaultSelectedElementID={id}
202
+ defaultSelectedElementIndex={1}
203
+ >
204
+ <React.Suspense fallback={null}>
205
+ <Suspender target={id} />
206
+ </React.Suspense>
207
+ </Contexts>
208
+ )),
209
+ 3
210
+ );
211
+ expect(targetRenderCount).toBe(1);
212
+ expect(inspectedElement).toMatchSnapshot('2: initial render');
213
+
214
+ const initialInspectedElement = inspectedElement;
215
+
216
+ targetRenderCount = 0;
217
+ inspectedElement = null;
218
+ await utils.actAsync(
219
+ () =>
220
+ renderer.update(
221
+ <Contexts
222
+ defaultSelectedElementID={id}
223
+ defaultSelectedElementIndex={1}
224
+ >
225
+ <React.Suspense fallback={null}>
226
+ <Suspender target={id} />
227
+ </React.Suspense>
228
+ </Contexts>
229
+ ),
230
+ 1
231
+ );
232
+ expect(targetRenderCount).toBe(0);
233
+ expect(inspectedElement).toEqual(initialInspectedElement);
234
+
235
+ targetRenderCount = 0;
236
+
237
+ await utils.actAsync(() =>
238
+ ReactDOM.render(
239
+ <Wrapper>
240
+ <Target foo={2} bar="def" />
241
+ </Wrapper>,
242
+ container
243
+ )
244
+ );
245
+
246
+ // Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
247
+ expect(targetRenderCount).toBe(2);
248
+ expect(inspectedElement).toMatchSnapshot('3: updated state');
249
+
250
+ done();
251
+ });
252
});
src/backend/renderer.js
+45
-9
@@ -1102,6 +1102,19 @@ export function attach(
1102
if (__DEBUG__) {
1103
debug('updateFiberRecursively()', nextFiber, parentFiber);
1104
}
1105
+
1106
+ if (
1107
+ mostRecentlyInspectedElement !== null &&
1108
+ mostRecentlyInspectedElementID ===
1109
+ getFiberID(getPrimaryFiber(nextFiber)) &&
1110
+ hasDataChanged(prevFiber, nextFiber)
1111
+ ) {
1112
+ // If this Fiber has updated, clear cached inspected data.
1113
+ // If it is inspected again, it may need to be re-run to obtain updated hooks values.
1114
+ mostRecentlyInspectedElement = null;
1115
+ mostRecentlyInspectedElementID = null;
1116
+ }
1117
+
1118
const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
1119
const isSuspense = nextFiber.tag === SuspenseComponent;
1120
let shouldResetChildren = false;
@@ -1890,18 +1903,41 @@ export function attach(
1903
};
1904
}
1905
1906
+ let mostRecentlyInspectedElementID: number | null = null;
1907
+ let mostRecentlyInspectedElement: InspectedElement | null = null;
1908
+
1909
function inspectElement(id: number): InspectedElement | null {
1894
- let result = inspectElementRaw(id);
1895
- if (result === null) {
1910
+ // If this element has not been updated since it was last inspected, reuse the last value.
1911
+ // This avoids re-invoking a function component with hooks.
1912
+ // TODO We could send a special signal (e.g. true) to avoid serialization too.
1913
+ if (
1914
+ mostRecentlyInspectedElement !== null &&
1915
+ mostRecentlyInspectedElementID === id
1916
+ ) {
1917
+ return mostRecentlyInspectedElement;
1918
+ }
1919
+
1920
+ mostRecentlyInspectedElementID = id;
1921
+ mostRecentlyInspectedElement = inspectElementRaw(id);
1922
+ if (mostRecentlyInspectedElement === null) {
1923
return null;
1924
}
1898
- // TODO Review sanitization approach for the below inspectable values.
1899
- result.context = cleanForBridge(result.context);
1900
- result.events = cleanForBridge(result.events);
1901
- result.hooks = cleanForBridge(result.hooks);
1902
- result.props = cleanForBridge(result.props);
1903
- result.state = cleanForBridge(result.state);
1904
- return result;
1925
+ mostRecentlyInspectedElement.context = cleanForBridge(
1926
+ mostRecentlyInspectedElement.context
1927
+ );
1928
+ mostRecentlyInspectedElement.events = cleanForBridge(
1929
+ mostRecentlyInspectedElement.events
1930
+ );
1931
+ mostRecentlyInspectedElement.hooks = cleanForBridge(
1932
+ mostRecentlyInspectedElement.hooks
1933
+ );
1934
+ mostRecentlyInspectedElement.props = cleanForBridge(
1935
+ mostRecentlyInspectedElement.props
1936
+ );
1937
+ mostRecentlyInspectedElement.state = cleanForBridge(
1938
+ mostRecentlyInspectedElement.state
1939
+ );
1940
+ return mostRecentlyInspectedElement;
1941
}
1942
1943
function logElementToConsole(id) {