@samitouri / QOS-React-2 / commits / 07db37f030

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) {