Fetch owners list from renderer (using suspense)
Owners in the list may have been filtered out of the Store, but in the owners list view- it's important to still show them. The frontend cannot do this on its own, so this list needs to come from the renderer interface.
Brian Vaughn committed
May 9, 2019 at 11:47 UTC
564a2233685d52fceccfd2f4a515a654ec6f9879
13 files changed
+404
-182
src/backend/agent.js
+17
-5
@@ -16,6 +16,7 @@ import type {
16
RendererID,
17
RendererInterface,
18
} from './types';
19
+import type { OwnersList } from 'src/devtools/views/Components/types';
20
import type { Bridge, ComponentFilter } from '../types';
21
22
const debug = (methodName, ...args) => {
@@ -29,7 +30,7 @@ const debug = (methodName, ...args) => {
30
}
31
};
32
32
-type InspectSelectParams = {|
33
+type ElementAndRendererID = {|
34
id: number,
35
rendererID: number,
36
|};
@@ -99,6 +100,7 @@ export default class Agent extends EventEmitter {
100
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
101
bridge.addListener('getProfilingSummary', this.getProfilingSummary);
102
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
103
+ bridge.addListener('getOwnersList', this.getOwnersList);
104
bridge.addListener('inspectElement', this.inspectElement);
105
bridge.addListener('logElementToConsole', this.logElementToConsole);
106
bridge.addListener('overrideContext', this.overrideContext);
@@ -303,7 +305,17 @@ export default class Agent extends EventEmitter {
305
}
306
};
307
306
- inspectElement = ({ id, rendererID }: InspectSelectParams) => {
308
+ getOwnersList = ({ id, rendererID }: ElementAndRendererID) => {
309
+ const renderer = this._rendererInterfaces[rendererID];
310
+ if (renderer == null) {
311
+ console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
312
+ } else {
313
+ const owners = renderer.getOwnersList(id);
314
+ this._bridge.send('ownersList', ({ id, owners }: OwnersList));
315
+ }
316
+ };
317
+
318
+ inspectElement = ({ id, rendererID }: ElementAndRendererID) => {
319
const renderer = this._rendererInterfaces[rendererID];
320
if (renderer == null) {
321
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -312,7 +324,7 @@ export default class Agent extends EventEmitter {
324
}
325
};
326
315
- logElementToConsole = ({ id, rendererID }: InspectSelectParams) => {
327
+ logElementToConsole = ({ id, rendererID }: ElementAndRendererID) => {
328
const renderer = this._rendererInterfaces[rendererID];
329
if (renderer == null) {
330
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -340,7 +352,7 @@ export default class Agent extends EventEmitter {
352
this._bridge.send('screenshotCaptured', { commitIndex, dataURL });
353
};
354
343
- selectElement = ({ id, rendererID }: InspectSelectParams) => {
355
+ selectElement = ({ id, rendererID }: ElementAndRendererID) => {
356
const renderer = this._rendererInterfaces[rendererID];
357
if (renderer == null) {
358
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -506,7 +518,7 @@ export default class Agent extends EventEmitter {
518
}
519
};
520
509
- viewElementSource = ({ id, rendererID }: InspectSelectParams) => {
521
+ viewElementSource = ({ id, rendererID }: ElementAndRendererID) => {
522
const renderer = this._rendererInterfaces[rendererID];
523
if (renderer == null) {
524
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
src/backend/renderer.js
+30
-16
@@ -50,7 +50,10 @@ import type {
50
ReactRenderer,
51
RendererInterface,
52
} from './types';
53
-import type { InspectedElement } from 'src/devtools/views/Components/types';
53
+import type {
54
+ InspectedElement,
55
+ Owner,
56
+} from 'src/devtools/views/Components/types';
57
import type { ComponentFilter, ElementType } from 'src/types';
58
59
function getInternalReactConstants(version) {
@@ -1685,6 +1688,30 @@ export function attach(
1688
}
1689
}
1690
1691
+ function getOwnersList(id: number): Array<Owner> | null {
1692
+ let fiber = findCurrentFiberUsingSlowPathById(id);
1693
+ if (fiber == null) {
1694
+ return null;
1695
+ }
1696
+
1697
+ const { _debugOwner } = fiber;
1698
+
1699
+ let owners = null;
1700
+ if (_debugOwner) {
1701
+ owners = [];
1702
+ let owner = _debugOwner;
1703
+ while (owner !== null) {
1704
+ owners.push({
1705
+ displayName: getDisplayNameForFiber(owner) || 'Unknown',
1706
+ id: getFiberID(getPrimaryFiber(owner)),
1707
+ });
1708
+ owner = owner._debugOwner || null;
1709
+ }
1710
+ }
1711
+
1712
+ return owners;
1713
+ }
1714
+
1715
function inspectElementRaw(id: number): InspectedElement | null {
1716
let fiber = findCurrentFiberUsingSlowPathById(id);
1717
if (fiber == null) {
@@ -1692,7 +1719,6 @@ export function attach(
1719
}
1720
1721
const {
1695
- _debugOwner,
1722
_debugSource,
1723
stateNode,
1724
memoizedProps,
@@ -1764,19 +1790,6 @@ export function attach(
1790
context = { value: context };
1791
}
1792
1767
- let owners = null;
1768
- if (_debugOwner) {
1769
- owners = [];
1770
- let owner = _debugOwner;
1771
- while (owner !== null) {
1772
- owners.push({
1773
- displayName: getDisplayNameForFiber(owner) || 'Unknown',
1774
- id: getFiberID(getPrimaryFiber(owner)),
1775
- });
1776
- owner = owner._debugOwner || null;
1777
- }
1778
- }
1779
-
1793
const isTimedOutSuspense =
1794
tag === SuspenseComponent && memoizedState !== null;
1795
@@ -1812,7 +1825,7 @@ export function attach(
1825
state: usesHooks ? null : memoizedState,
1826
1827
// List of owners
1815
- owners,
1828
+ owners: getOwnersList(id),
1829
1830
// Location of component in source coude.
1831
source: _debugSource,
@@ -2385,6 +2398,7 @@ export function attach(
2398
getFiberCommits,
2399
getInteractions,
2400
findNativeByFiberID,
2401
+ getOwnersList,
2402
getPathForElement,
2403
getProfilingDataForDownload,
2404
getProfilingSummary,
src/backend/types.js
+5
-1
@@ -1,7 +1,10 @@
1
// @flow
2
3
import type { ComponentFilter, ElementType } from 'src/types';
4
-import type { InspectedElement } from 'src/devtools/views/Components/types';
4
+import type {
5
+ InspectedElement,
6
+ Owner,
7
+} from 'src/devtools/views/Components/types';
8
9
type BundleType =
10
| 0 // PROD
@@ -175,6 +178,7 @@ export type RendererInterface = {
178
) => number | null,
179
getFiberCommits: (rootID: number, fiberID: number) => FiberCommitsBackend,
180
getInteractions: (rootID: number) => InteractionsBackend,
181
+ getOwnersList: (id: number) => Array<Owner> | null,
182
getProfilingDataForDownload: (rootID: number) => Object,
183
getProfilingSummary: (rootID: number) => ProfilingSummaryBackend,
184
getPathForElement: (id: number) => Array<PathFrame> | null,
src/devtools/views/Components/Components.js
+16
-13
@@ -4,6 +4,7 @@ import React, { Suspense } from 'react';
4
import Tree from './Tree';
5
import SelectedElement from './SelectedElement';
6
import { InspectedElementContextController } from './InspectedElementContext';
7
+import { OwnersListContextController } from './OwnersListContext';
8
import portaledContent from '../portaledContent';
9
import { ModalDialog } from '../ModalDialog';
10
@@ -12,19 +13,21 @@ import styles from './Components.css';
13
function Components(_: {||}) {
14
// TODO Flex wrappers below should be user resizable.
15
return (
15
- <div className={styles.Components}>
16
- <div className={styles.TreeWrapper}>
17
- <Tree />
18
- </div>
19
- <div className={styles.SelectedElementWrapper}>
20
- <InspectedElementContextController>
21
- <Suspense fallback={<Loading />}>
22
- <SelectedElement />
23
- </Suspense>
24
- </InspectedElementContextController>
25
- </div>
26
- <ModalDialog />
27
- </div>
16
+ <OwnersListContextController>
17
+ <InspectedElementContextController>
18
+ <div className={styles.Components}>
19
+ <div className={styles.TreeWrapper}>
20
+ <Tree />
21
+ </div>
22
+ <div className={styles.SelectedElementWrapper}>
23
+ <Suspense fallback={<Loading />}>
24
+ <SelectedElement />
25
+ </Suspense>
26
+ </div>
27
+ <ModalDialog />
28
+ </div>
29
+ </InspectedElementContextController>
30
+ </OwnersListContextController>
31
);
32
}
33
src/devtools/views/Components/Element.js
+2
-2
@@ -29,7 +29,7 @@ type Props = {
29
30
export default function ElementView({ data, index, style }: Props) {
31
const store = useContext(StoreContext);
32
- const { ownerFlatTree, ownerStack, selectedElementID } = useContext(
32
+ const { ownerFlatTree, ownerID, selectedElementID } = useContext(
33
TreeStateContext
34
);
35
const dispatch = useContext(TreeDispatcherContext);
@@ -168,7 +168,7 @@ export default function ElementView({ data, index, style }: Props) {
168
}}
169
>
170
<span className={styles.ScrollAnchor} ref={scrollAnchorStartRef} />
171
- {ownerStack.length === 0 ? (
171
+ {ownerID === null ? (
172
<ExpandCollapseToggle element={element} store={store} />
173
) : null}
174
<span className={styles.Component}>
src/devtools/views/Components/OwnersListContext.js
new
+109
@@ -0,0 +1,109 @@
1
+// @flow
2
+
3
+import React, {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+} from 'react';
9
+import { createResource } from '../../cache';
10
+import { BridgeContext, StoreContext } from '../context';
11
+import { TreeStateContext } from './TreeContext';
12
+
13
+import type {
14
+ Element,
15
+ Owner,
16
+ OwnersList,
17
+} from 'src/devtools/views/Components/types';
18
+import type { Resource, Thenable } from '../../cache';
19
+
20
+type Context = (id: number) => Array<Owner> | null;
21
+
22
+const OwnersListContext = createContext<Context>(((null: any): Context));
23
+OwnersListContext.displayName = 'OwnersListContext';
24
+
25
+type ResolveFn = (ownersList: Array<Owner> | null) => void;
26
+type InProgressRequest = {|
27
+ promise: Thenable<Array<Owner>>,
28
+ resolveFn: ResolveFn,
29
+|};
30
+
31
+const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
32
+const resource: Resource<Element, Element, Array<Owner>> = createResource(
33
+ (element: Element) => {
34
+ let request = inProgressRequests.get(element);
35
+ if (request != null) {
36
+ return request.promise;
37
+ }
38
+
39
+ let resolveFn = ((null: any): ResolveFn);
40
+ const promise = new Promise(resolve => {
41
+ resolveFn = resolve;
42
+ });
43
+
44
+ inProgressRequests.set(element, { promise, resolveFn });
45
+
46
+ return promise;
47
+ },
48
+ (element: Element) => element,
49
+ { useWeakMap: true }
50
+);
51
+
52
+type Props = {|
53
+ children: React$Node,
54
+|};
55
+
56
+function OwnersListContextController({ children }: Props) {
57
+ const bridge = useContext(BridgeContext);
58
+ const store = useContext(StoreContext);
59
+ const { ownerID } = useContext(TreeStateContext);
60
+
61
+ const read = useCallback(
62
+ (id: number) => {
63
+ const element = store.getElementByID(id);
64
+ if (element !== null) {
65
+ return resource.read(element);
66
+ } else {
67
+ return null;
68
+ }
69
+ },
70
+ [store]
71
+ );
72
+
73
+ useEffect(() => {
74
+ const onOwnersList = (ownersList: OwnersList) => {
75
+ const id = ownersList.id;
76
+
77
+ const element = store.getElementByID(id);
78
+ if (element !== null) {
79
+ const request = inProgressRequests.get(element);
80
+ if (request != null) {
81
+ inProgressRequests.delete(element);
82
+ request.resolveFn(ownersList.owners);
83
+ }
84
+ }
85
+ };
86
+
87
+ bridge.addListener('ownersList', onOwnersList);
88
+ return () => bridge.removeListener('ownersList', onOwnersList);
89
+ }, [bridge, store]);
90
+
91
+ // This effect requests an updated owners list any time the selected owner changes
92
+ useEffect(() => {
93
+ if (ownerID !== null) {
94
+ const rendererID = store.getRendererIDForElement(ownerID);
95
+
96
+ bridge.send('getOwnersList', { id: ownerID, rendererID });
97
+ }
98
+
99
+ return () => {};
100
+ }, [bridge, ownerID, store]);
101
+
102
+ return (
103
+ <OwnersListContext.Provider value={read}>
104
+ {children}
105
+ </OwnersListContext.Provider>
106
+ );
107
+}
108
+
109
+export { OwnersListContext, OwnersListContextController };
src/devtools/views/Components/OwnersStack.css
+5
@@ -91,3 +91,8 @@
91
font-family: var(--font-family-monospace);
92
font-size: var(--font-size-monospace-normal);
93
}
94
+
95
+.NotInStore,
96
+.NotInStore:hover {
97
+ color: var(--color-dimmest);
98
+}
src/devtools/views/Components/OwnersStack.js
+156
-56
@@ -4,6 +4,7 @@ import React, {
4
useCallback,
5
useContext,
6
useLayoutEffect,
7
+ useReducer,
8
useRef,
9
useState,
10
} from 'react';
@@ -12,22 +13,111 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
13
import Button from '../Button';
14
import ButtonIcon from '../ButtonIcon';
15
import Toggle from '../Toggle';
16
+import { OwnersListContext } from './OwnersListContext';
17
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
16
-import { StoreContext } from '../context';
18
import { useIsOverflowing } from '../hooks';
19
+import { StoreContext } from '../context';
20
19
-import type { Element } from './types';
21
+import type { Owner } from './types';
22
23
import styles from './OwnersStack.css';
24
25
+type SelectOwner = (owner: Owner | null) => void;
26
+
27
+type ACTION_UPDATE_OWNER_ID = {|
28
+ type: 'UPDATE_OWNER_ID',
29
+ ownerID: number | null,
30
+ owners: Array<Owner>,
31
+|};
32
+type ACTION_UPDATE_SELECTED_INDEX = {|
33
+ type: 'UPDATE_SELECTED_INDEX',
34
+ selectedIndex: number,
35
+|};
36
+
37
+type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX;
38
+
39
+type State = {|
40
+ ownerID: number | null,
41
+ owners: Array<Owner>,
42
+ selectedIndex: number,
43
+|};
44
+
45
+function dialogReducer(state, action) {
46
+ switch (action.type) {
47
+ case 'UPDATE_OWNER_ID':
48
+ const selectedIndex = state.owners.findIndex(
49
+ owner => owner.id === action.ownerID
50
+ );
51
+ return {
52
+ ownerID: action.ownerID,
53
+ owners: action.owners,
54
+ selectedIndex,
55
+ };
56
+ case 'UPDATE_SELECTED_INDEX':
57
+ return {
58
+ ...state,
59
+ selectedIndex: action.selectedIndex,
60
+ };
61
+ default:
62
+ throw new Error(`Invalid action "${action.type}"`);
63
+ }
64
+}
65
+
66
export default function OwnerStack() {
24
- const { ownerStack, ownerStackIndex } = useContext(TreeStateContext);
25
- const dispatch = useContext(TreeDispatcherContext);
67
+ const read = useContext(OwnersListContext);
68
+ const { ownerID } = useContext(TreeStateContext);
69
+ const treeDispatch = useContext(TreeDispatcherContext);
70
+
71
+ const [state, dispatch] = useReducer<State, Action>(dialogReducer, {
72
+ ownerID: null,
73
+ owners: [],
74
+ selectedIndex: -1,
75
+ });
76
+
77
+ // TODO (owners) Explain this and use reducer with ownerID too to avoid inf. loop
78
+ if (ownerID === null) {
79
+ dispatch({
80
+ type: 'UPDATE_OWNER_ID',
81
+ ownerID: null,
82
+ owners: [],
83
+ });
84
+ } else if (ownerID !== state.ownerID) {
85
+ const isInList = state.owners.findIndex(owner => owner.id === ownerID) >= 0;
86
+ dispatch({
87
+ type: 'UPDATE_OWNER_ID',
88
+ ownerID,
89
+ owners: isInList ? state.owners : read(ownerID) || [],
90
+ });
91
+ }
92
+
93
+ const { owners, selectedIndex } = state;
94
+
95
+ const selectOwner = useCallback<SelectOwner>(
96
+ (owner: Owner | null) => {
97
+ if (owner !== null) {
98
+ const index = owners.indexOf(owner);
99
+ dispatch({
100
+ type: 'UPDATE_SELECTED_INDEX',
101
+ selectedIndex: index >= 0 ? index : 0,
102
+ });
103
+ treeDispatch({ type: 'SELECT_OWNER', payload: owner.id });
104
+ } else {
105
+ dispatch({
106
+ type: 'UPDATE_SELECTED_INDEX',
107
+ selectedIndex: 0,
108
+ });
109
+ treeDispatch({ type: 'RESET_OWNER_STACK' });
110
+ }
111
+ },
112
+ [owners, treeDispatch]
113
+ );
114
115
const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
116
const elementsBarRef = useRef<HTMLDivElement | null>(null);
117
const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);
118
119
+ const selectedOwner = owners[selectedIndex];
120
+
121
useLayoutEffect(() => {
122
// If we're already overflowing, then we don't need to re-measure items.
123
// That's because once the owners stack is open, it can only get larger (by driling in).
@@ -37,7 +127,7 @@ export default function OwnerStack() {
127
}
128
129
let elementsTotalWidth = 0;
40
- for (let i = 0; i < ownerStack.length; i++) {
130
+ for (let i = 0; i < owners.length; i++) {
131
const element = elementsBarRef.current.children[i];
132
const computedStyle = getComputedStyle(element);
133
@@ -48,7 +138,7 @@ export default function OwnerStack() {
138
}
139
140
setElementsTotalWidth(elementsTotalWidth);
51
- }, [elementsBarRef, isOverflowing, ownerStack.length]);
141
+ }, [elementsBarRef, isOverflowing, owners.length]);
142
143
return (
144
<div className={styles.OwnerStack}>
@@ -56,28 +146,38 @@ export default function OwnerStack() {
146
{isOverflowing && (
147
<Fragment>
148
<ElementsDropdown
59
- ownerStack={ownerStack}
60
- ownerStackIndex={ownerStackIndex}
149
+ owners={owners}
150
+ selectedIndex={selectedIndex}
151
+ selectOwner={selectOwner}
152
/>
153
<BackToOwnerButton
63
- ownerStack={ownerStack}
64
- ownerStackIndex={ownerStackIndex}
65
- />
66
- <ElementView
67
- id={ownerStack[((ownerStackIndex: any): number)]}
68
- index={ownerStackIndex}
154
+ owners={owners}
155
+ selectedIndex={selectedIndex}
156
+ selectOwner={selectOwner}
157
/>
158
+ {selectedOwner != null && (
159
+ <ElementView
160
+ owner={selectedOwner}
161
+ isSelected
162
+ selectOwner={selectOwner}
163
+ />
164
+ )}
165
</Fragment>
166
)}
167
{!isOverflowing &&
73
- ownerStack.map((id, index) => (
74
- <ElementView key={id} id={id} index={index} />
168
+ owners.map((owner, index) => (
169
+ <ElementView
170
+ key={index}
171
+ owner={owner}
172
+ isSelected={index === selectedIndex}
173
+ selectOwner={selectOwner}
174
+ />
175
))}
176
</div>
177
<div className={styles.VRule} />
178
<Button
179
className={styles.IconButton}
80
- onClick={() => dispatch({ type: 'RESET_OWNER_STACK' })}
180
+ onClick={() => selectOwner(null)}
181
title="Back to tree view"
182
>
183
<ButtonIcon type="close" />
@@ -87,26 +187,28 @@ export default function OwnerStack() {
187
}
188
189
type ElementsDropdownProps = {
90
- ownerStack: Array<number>,
91
- ownerStackIndex: number | null,
190
+ owners: Array<Owner>,
191
+ selectedIndex: number,
192
+ selectOwner: SelectOwner,
193
};
194
function ElementsDropdown({
94
- ownerStack,
95
- ownerStackIndex,
195
+ owners,
196
+ selectedIndex,
197
+ selectOwner,
198
}: ElementsDropdownProps) {
199
const store = useContext(StoreContext);
98
- const dispatch = useContext(TreeDispatcherContext);
200
201
const menuItems = [];
101
- for (let index = ownerStack.length - 1; index >= 0; index--) {
102
- const id = ownerStack[index];
202
+ for (let index = owners.length - 1; index >= 0; index--) {
203
+ const owner = owners[index];
204
+ const isInStore = store.containsElement(owner.id);
205
menuItems.push(
206
<MenuItem
105
- key={id}
106
- className={styles.Component}
107
- onSelect={() => dispatch({ type: 'SELECT_OWNER', payload: id })}
207
+ key={owner.id}
208
+ className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
209
+ onSelect={() => (isInStore ? selectOwner(owner) : null)}
210
>
109
- {((store.getElementByID(id): any): Element).displayName}
211
+ {owner.displayName}
212
</MenuItem>
213
);
214
}
@@ -126,28 +228,26 @@ function ElementsDropdown({
228
}
229
230
type ElementViewProps = {
129
- id: number,
130
- index: number | null,
231
+ isSelected: boolean,
232
+ owner: Owner,
233
+ selectOwner: SelectOwner,
234
};
132
-function ElementView({ id, index }: ElementViewProps) {
235
+function ElementView({ isSelected, owner, selectOwner }: ElementViewProps) {
236
const store = useContext(StoreContext);
134
- const { ownerStackIndex } = useContext(TreeStateContext);
135
- const dispatch = useContext(TreeDispatcherContext);
136
-
137
- const { displayName } = ((store.getElementByID(id): any): Element);
237
139
- const isChecked = ownerStackIndex === index;
238
+ const { displayName } = owner;
239
+ const isInStore = store.containsElement(owner.id);
240
241
const handleChange = useCallback(() => {
142
- if (!isChecked) {
143
- dispatch({ type: 'SELECT_OWNER', payload: id });
242
+ if (isInStore) {
243
+ selectOwner(owner);
244
}
145
- }, [dispatch, id, isChecked]);
245
+ }, [isInStore, selectOwner, owner]);
246
247
return (
248
<Toggle
149
- className={styles.Component}
150
- isChecked={isChecked}
249
+ className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
250
+ isChecked={isSelected}
251
onChange={handleChange}
252
>
253
{displayName}
@@ -156,32 +256,32 @@ function ElementView({ id, index }: ElementViewProps) {
256
}
257
258
type BackToOwnerButtonProps = {|
159
- ownerStack: Array<number>,
160
- ownerStackIndex: number | null,
259
+ owners: Array<Owner>,
260
+ selectedIndex: number,
261
+ selectOwner: SelectOwner,
262
|};
263
function BackToOwnerButton({
163
- ownerStack,
164
- ownerStackIndex,
264
+ owners,
265
+ selectedIndex,
266
+ selectOwner,
267
}: BackToOwnerButtonProps) {
268
const store = useContext(StoreContext);
167
- const dispatch = useContext(TreeDispatcherContext);
269
169
- if (ownerStackIndex === null || ownerStackIndex === 0) {
270
+ if (selectedIndex <= 0) {
271
return null;
272
}
273
173
- const ownerID = ownerStack[ownerStackIndex - 1];
174
- const owner = store.getElementByID(ownerID);
274
+ const owner = owners[selectedIndex - 1];
275
+ if (owner == null) {
276
+ debugger;
277
+ }
278
+ const isInStore = store.containsElement(owner.id);
279
280
return (
281
<Button
178
- onClick={() =>
179
- dispatch({
180
- type: 'SELECT_OWNER',
181
- payload: ownerID,
182
- })
183
- }
184
- title={`Up to ${(owner !== null && owner.displayName) || 'owner'}`}
282
+ className={isInStore ? undefined : styles.NotInStore}
283
+ onClick={() => (isInStore ? selectOwner(owner) : null)}
284
+ title={`Up to ${owner.displayName || 'owner'}`}
285
>
286
<ButtonIcon type="previous" />
287
</Button>
src/devtools/views/Components/SelectedElement.js
+2
-2
@@ -227,7 +227,7 @@ function InspectedElementView({
227
state,
228
} = inspectedElement;
229
230
- const { ownerStack } = useContext(TreeStateContext);
230
+ const { ownerID } = useContext(TreeStateContext);
231
const bridge = useContext(BridgeContext);
232
const store = useContext(StoreContext);
233
@@ -298,7 +298,7 @@ function InspectedElementView({
298
overrideValueFn={overrideContextFn}
299
/>
300
301
- {ownerStack.length === 0 && owners !== null && owners.length > 0 && (
301
+ {ownerID === null && owners !== null && owners.length > 0 && (
302
<div className={styles.Owners}>
303
<div className={styles.OwnersHeader}>rendered by</div>
304
{owners.map(owner => (
src/devtools/views/Components/Tree.css
+11
@@ -37,3 +37,14 @@
37
margin: 0 0.5rem;
38
background-color: var(--color-border);
39
}
40
+
41
+.Loading {
42
+ height: 100%;
43
+ padding-left: 0.5rem;
44
+ display: flex;
45
+ align-items: center;
46
+ flex: 1;
47
+ justify-content: flex-start;
48
+ font-size: var(--font-size-sans-large);
49
+ color: var(--color-dim);
50
+}
src/devtools/views/Components/Tree.js
+13
-7
@@ -1,6 +1,7 @@
1
// @flow
2
3
import React, {
4
+ Suspense,
5
useState,
6
useCallback,
7
useContext,
@@ -38,7 +39,7 @@ export default function Tree(props: Props) {
39
const dispatch = useContext(TreeDispatcherContext);
40
const {
41
numElements,
41
- ownerStack,
42
+ ownerID,
43
searchIndex,
44
searchResults,
45
selectedElementID,
@@ -277,7 +278,9 @@ export default function Tree(props: Props) {
278
<div className={styles.SearchInput}>
279
<InspectHostNodesToggle />
280
<div className={styles.VRule} />
280
- {ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
281
+ <Suspense fallback={<Loading />}>
282
+ {ownerID !== null ? <OwnersStack /> : <SearchInput />}
283
+ </Suspense>
284
<div className={styles.VRule} />
285
<ToggleComponentFiltersModalButton />
286
</div>
@@ -317,7 +320,7 @@ export default function Tree(props: Props) {
320
}
321
322
function InnerElementType({ style, ...rest }) {
320
- const { ownerStack } = useContext(TreeStateContext);
323
+ const { ownerID } = useContext(TreeStateContext);
324
325
// The list may need to scroll horizontally due to deeply nested elements.
326
// We don't know the maximum scroll width up front, because we're windowing.
@@ -346,10 +349,9 @@ function InnerElementType({ style, ...rest }) {
349
350
// We shouldn't retain this width across different conceptual trees though,
351
// so when the user opens the "owners tree" view, we should discard the previous width.
349
- const hasOwnerStack = ownerStack.length > 0;
350
- const [prevHasOwnerStack, setPrevHasOwnerStack] = useState(hasOwnerStack);
351
- if (hasOwnerStack !== prevHasOwnerStack) {
352
- setPrevHasOwnerStack(hasOwnerStack);
352
+ const [prevOwnerID, setPrevOwnerID] = useState(ownerID);
353
+ if (ownerID !== prevOwnerID) {
354
+ setPrevOwnerID(ownerID);
355
setMinWidth(null);
356
}
357
@@ -371,3 +373,7 @@ function InnerElementType({ style, ...rest }) {
373
/>
374
);
375
}
376
+
377
+function Loading() {
378
+ return <div className={styles.Loading}>Loading...</div>;
379
+}
src/devtools/views/Components/TreeContext.js
+33
-80
@@ -50,9 +50,8 @@ type StateContext = {|
50
searchText: string,
51
52
// Owners
53
+ ownerID: number | null,
54
ownerFlatTree: Array<Element> | null,
54
- ownerStack: Array<number>,
55
- ownerStackIndex: number | null,
55
56
// Inspection element panel
57
inspectedElementID: number | null,
@@ -142,8 +141,7 @@ type State = {|
141
searchText: string,
142
143
// Owners
145
- ownerStack: Array<number>,
146
- ownerStackIndex: number | null,
144
+ ownerID: number | null,
145
ownerFlatTree: Array<Element> | null,
146
147
// Inspection element panel
@@ -151,17 +149,12 @@ type State = {|
149
|};
150
151
function reduceTreeState(store: Store, state: State, action: Action): State {
154
- let {
155
- numElements,
156
- ownerStack,
157
- selectedElementIndex,
158
- selectedElementID,
159
- } = state;
152
+ let { numElements, ownerID, selectedElementIndex, selectedElementID } = state;
153
154
let lookupIDForIndex = true;
155
156
// Base tree should ignore selected element changes when the owner's tree is active.
164
- if (ownerStack.length === 0) {
157
+ if (ownerID === null) {
158
switch (action.type) {
159
case 'HANDLE_STORE_MUTATION':
160
numElements = store.numElements;
@@ -276,7 +269,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
269
270
function reduceSearchState(store: Store, state: State, action: Action): State {
271
let {
279
- ownerStack,
272
+ ownerID,
273
searchIndex,
274
searchResults,
275
searchText,
@@ -295,7 +288,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
288
let didRequestSearch = false;
289
290
// Search isn't supported when the owner's tree is active.
298
- if (ownerStack.length === 0) {
291
+ if (ownerID === null) {
292
switch (action.type) {
293
case 'GO_TO_NEXT_SEARCH_RESULT':
294
if (numPrevSearchResults > 0) {
@@ -442,9 +435,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
435
numElements,
436
selectedElementID,
437
selectedElementIndex,
438
+ ownerID,
439
ownerFlatTree,
446
- ownerStack,
447
- ownerStackIndex,
440
searchIndex,
441
searchResults,
442
searchText,
@@ -454,25 +446,9 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
446
447
switch (action.type) {
448
case 'HANDLE_STORE_MUTATION':
457
- if (ownerStack.length > 0) {
458
- let indexOfRemovedItem = -1;
459
- for (let i = 0; i < ownerStack.length; i++) {
460
- if (store.getElementByID(ownerStack[i]) === null) {
461
- indexOfRemovedItem = i;
462
- break;
463
- }
464
- }
465
-
466
- if (indexOfRemovedItem >= 0) {
467
- ownerStack = ownerStack.slice(0, indexOfRemovedItem);
468
- if (ownerStack.length === 0) {
469
- ownerFlatTree = null;
470
- ownerStackIndex = null;
471
- } else {
472
- ownerStackIndex = ownerStack.length - 1;
473
- }
474
- }
475
- if (selectedElementID !== null && ownerFlatTree !== null) {
449
+ if (ownerID !== null) {
450
+ ownerFlatTree = store.getOwnersListForElement(ownerID);
451
+ if (selectedElementID !== null) {
452
// Mutation might have caused the index of this ID to shift.
453
selectedElementIndex = ownerFlatTree.findIndex(
454
element => element.id === selectedElementID
@@ -491,13 +467,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
467
}
468
break;
469
case 'RESET_OWNER_STACK':
494
- ownerStack = [];
495
- ownerStackIndex = null;
470
+ ownerID = null;
471
+ ownerFlatTree = null;
472
selectedElementIndex =
473
selectedElementID !== null
474
? store.getIndexOfElementID(selectedElementID)
475
: null;
500
- ownerFlatTree = null;
476
break;
477
case 'SELECT_ELEMENT_AT_INDEX':
478
if (ownerFlatTree !== null) {
@@ -533,33 +508,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
508
// If the Store doesn't have any owners metadata, don't drill into an empty stack.
509
// This is a confusing user experience.
510
if (store.hasOwnerMetadata) {
536
- const id = (action: ACTION_SELECT_OWNER).payload;
537
- ownerStackIndex = ownerStack.indexOf(id);
511
+ ownerID = (action: ACTION_SELECT_OWNER).payload;
512
+ ownerFlatTree = store.getOwnersListForElement(ownerID);
513
514
// Always force reset selection to be the top of the new owner tree.
515
selectedElementIndex = 0;
516
prevSelectedElementIndex = null;
542
-
543
- // If this owner is already in the current stack, just select it.
544
- // Otherwise, create a new stack.
545
- if (ownerStackIndex < 0) {
546
- // Add this new owner, and fill in the owners above it as well.
547
- ownerStack = [];
548
- let currentOwnerID = id;
549
- while (currentOwnerID !== 0) {
550
- ownerStack.unshift(currentOwnerID);
551
- currentOwnerID = ((store.getElementByID(
552
- currentOwnerID
553
- ): any): Element).ownerID;
554
- }
555
- ownerStackIndex = ownerStack.length - 1;
556
-
557
- if (searchText !== '') {
558
- searchIndex = null;
559
- searchResults = [];
560
- searchText = '';
561
- }
562
- }
517
}
518
break;
519
default:
@@ -569,17 +523,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
523
524
// Changes in the selected owner require re-calculating the owners tree.
525
if (
572
- ownerStackIndex !== state.ownerStackIndex ||
573
- ownerStack !== state.ownerStack ||
526
+ ownerFlatTree !== state.ownerFlatTree ||
527
action.type === 'HANDLE_STORE_MUTATION'
528
) {
576
- if (ownerStackIndex === null) {
577
- ownerFlatTree = null;
529
+ if (ownerFlatTree === null) {
530
numElements = store.numElements;
531
} else {
580
- ownerFlatTree = store.getOwnersListForElement(
581
- ownerStack[ownerStackIndex]
582
- );
532
numElements = ownerFlatTree.length;
533
}
534
}
@@ -588,9 +537,10 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
537
if (selectedElementIndex !== prevSelectedElementIndex) {
538
if (selectedElementIndex === null) {
539
selectedElementID = null;
591
- } else if (ownerFlatTree !== null) {
592
- selectedElementID =
593
- ownerFlatTree[((selectedElementIndex: any): number)].id;
540
+ } else {
541
+ if (ownerFlatTree !== null) {
542
+ selectedElementID = ownerFlatTree[selectedElementIndex].id;
543
+ }
544
}
545
}
546
@@ -605,8 +555,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
555
searchResults,
556
searchText,
557
608
- ownerStack,
609
- ownerStackIndex,
558
+ ownerID,
559
ownerFlatTree,
560
};
561
}
@@ -619,14 +568,19 @@ function reduceSuspenseState(
568
const { type } = action;
569
switch (type) {
570
case 'UPDATE_INSPECTED_ELEMENT_ID':
622
- return {
623
- ...state,
624
- inspectedElementID: state.selectedElementID,
625
- };
571
+ if (state.inspectedElementID !== state.selectedElementID) {
572
+ return {
573
+ ...state,
574
+ inspectedElementID: state.selectedElementID,
575
+ };
576
+ }
577
+ break;
578
default:
627
- // React can bailout of no-op updates.
628
- return state;
579
+ break;
580
}
581
+
582
+ // React can bailout of no-op updates.
583
+ return state;
584
}
585
586
type Props = {| children: React$Node |};
@@ -696,8 +650,7 @@ function TreeContextController({ children }: Props) {
650
searchText: '',
651
652
// Owners
699
- ownerStack: [],
700
- ownerStackIndex: null,
653
+ ownerID: null,
654
ownerFlatTree: null,
655
656
// Inspection element panel
src/devtools/views/Components/types.js
+5
@@ -35,6 +35,11 @@ export type Owner = {|
35
id: number,
36
|};
37
38
+export type OwnersList = {|
39
+ id: number,
40
+ owners: Array<Owner> | null,
41
+|};
42
+
43
export type InspectedElement = {|
44
id: number,
45