Refactored TreeContext to use less memoization (based on feedback from Sebastian)
Brian Vaughn committed
Apr 22, 2019 at 10:19 UTC
433934f55ef272b39281e9b56e4d6c3ff34f48fc
9 files changed
+260
-312
src/devtools/views/Components/Element.js
+21
-13
@@ -13,7 +13,7 @@ import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
13
import Store from 'src/devtools/store';
14
import ButtonIcon from '../ButtonIcon';
15
import { createRegExp } from '../utils';
16
-import { TreeContext } from './TreeContext';
16
+import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
17
import { StoreContext } from '../context';
18
19
import type { ItemData } from './Tree';
@@ -28,18 +28,21 @@ type Props = {
28
};
29
30
export default function ElementView({ data, index, style }: Props) {
31
- const [isHovered, setIsHovered] = useState(false);
31
+ const store = useContext(StoreContext);
32
const {
33
baseDepth,
34
- getElementAtIndex,
34
+ ownerFlatTree,
35
ownerStack,
36
- selectOwner,
36
selectedElementID,
38
- selectElementByID,
39
- } = useContext(TreeContext);
40
- const store = useContext(StoreContext);
37
+ } = useContext(TreeStateContext);
38
+ const dispatch = useContext(TreeDispatcherContext);
39
42
- const element = getElementAtIndex(index);
40
+ const element =
41
+ ownerFlatTree !== null
42
+ ? store.getElementByID(ownerFlatTree[index])
43
+ : store.getElementAtIndex(index);
44
+
45
+ const [isHovered, setIsHovered] = useState(false);
46
47
const {
48
lastScrolledIDRef,
@@ -52,9 +55,9 @@ export default function ElementView({ data, index, style }: Props) {
55
56
const handleDoubleClick = useCallback(() => {
57
if (id !== null) {
55
- selectOwner(id);
58
+ dispatch({ type: 'SELECT_OWNER', payload: id });
59
}
57
- }, [id, selectOwner]);
60
+ }, [dispatch, id]);
61
62
const scrollAnchorStartRef = useRef<HTMLSpanElement | null>(null);
63
const scrollAnchorEndRef = useRef<HTMLSpanElement | null>(null);
@@ -102,10 +105,13 @@ export default function ElementView({ data, index, style }: Props) {
105
const handleMouseDown = useCallback(
106
({ metaKey }) => {
107
if (id !== null) {
105
- selectElementByID(metaKey ? null : id);
108
+ dispatch({
109
+ type: 'SELECT_ELEMENT_BY_ID',
110
+ payload: metaKey ? null : id,
111
+ });
112
}
113
},
108
- [id, selectElementByID]
114
+ [dispatch, id]
115
);
116
117
const handleMouseEnter = useCallback(() => {
@@ -234,7 +240,9 @@ type DisplayNameProps = {|
240
|};
241
242
function DisplayName({ displayName, id }: DisplayNameProps) {
237
- const { searchIndex, searchResults, searchText } = useContext(TreeContext);
243
+ const { searchIndex, searchResults, searchText } = useContext(
244
+ TreeStateContext
245
+ );
246
const isSearchResult = useMemo(() => {
247
return searchResults.includes(id);
248
}, [id, searchResults]);
src/devtools/views/Components/OwnersStack.js
+10
-10
@@ -11,7 +11,7 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
11
import Button from '../Button';
12
import ButtonIcon from '../ButtonIcon';
13
import Toggle from '../Toggle';
14
-import { TreeContext } from './TreeContext';
14
+import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
15
import { StoreContext } from '../context';
16
import { useIsOverflowing } from '../hooks';
17
@@ -20,9 +20,8 @@ import type { Element } from './types';
20
import styles from './OwnersStack.css';
21
22
export default function OwnerStack() {
23
- const { ownerStack, ownerStackIndex, resetOwnerStack } = useContext(
24
- TreeContext
25
- );
23
+ const { ownerStack, ownerStackIndex } = useContext(TreeStateContext);
24
+ const dispatch = useContext(TreeDispatcherContext);
25
26
const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
27
const elementsBarRef = useRef<HTMLDivElement | null>(null);
@@ -73,7 +72,7 @@ export default function OwnerStack() {
72
<div className={styles.VRule} />
73
<Button
74
className={styles.IconButton}
76
- onClick={resetOwnerStack}
75
+ onClick={() => dispatch({ type: 'RESET_OWNER_STACK' })}
76
title="Back to tree view"
77
>
78
<ButtonIcon type="close" />
@@ -91,7 +90,7 @@ function ElementsDropdown({
90
ownerStackIndex,
91
}: ElementsDropdownProps) {
92
const store = useContext(StoreContext);
94
- const { selectOwner } = useContext(TreeContext);
93
+ const dispatch = useContext(TreeDispatcherContext);
94
95
return (
96
<Menu>
@@ -107,7 +106,7 @@ function ElementsDropdown({
106
<MenuItem
107
key={id}
108
className={styles.Component}
110
- onSelect={() => selectOwner(id)}
109
+ onSelect={() => dispatch({ type: 'SELECT_OWNER', payload: id })}
110
>
111
{((store.getElementByID(id): any): Element).displayName}
112
</MenuItem>
@@ -123,7 +122,8 @@ type ElementViewProps = {
122
};
123
function ElementView({ id, index }: ElementViewProps) {
124
const store = useContext(StoreContext);
126
- const { ownerStackIndex, selectOwner } = useContext(TreeContext);
125
+ const { ownerStackIndex } = useContext(TreeStateContext);
126
+ const dispatch = useContext(TreeDispatcherContext);
127
128
const { displayName } = ((store.getElementByID(id): any): Element);
129
@@ -131,9 +131,9 @@ function ElementView({ id, index }: ElementViewProps) {
131
132
const handleChange = useCallback(() => {
133
if (!isChecked) {
134
- selectOwner(id);
134
+ dispatch({ type: 'SELECT_OWNER', payload: id });
135
}
136
- }, [id, isChecked, selectOwner]);
136
+ }, [dispatch, id, isChecked]);
137
138
return (
139
<Toggle
src/devtools/views/Components/SearchInput.js
+19
-21
@@ -1,7 +1,7 @@
1
// @flow
2
3
import React, { useCallback, useContext, useEffect, useRef } from 'react';
4
-import { TreeContext } from './TreeContext';
4
+import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
5
import Button from '../Button';
6
import ButtonIcon from '../ButtonIcon';
7
import Icon from '../Icon';
@@ -11,51 +11,49 @@ import styles from './SearchInput.css';
11
type Props = {||};
12
13
export default function SearchInput(props: Props) {
14
- const {
15
- goToNextSearchResult,
16
- goToPreviousSearchResult,
17
- searchIndex,
18
- searchResults,
19
- searchText,
20
- selectNextElementInTree,
21
- selectPreviousElementInTree,
22
- setSearchText,
23
- } = useContext(TreeContext);
14
+ const { searchIndex, searchResults, searchText } = useContext(
15
+ TreeStateContext
16
+ );
17
+ const dispatch = useContext(TreeDispatcherContext);
18
19
const inputRef = useRef<HTMLInputElement | null>(null);
20
21
const handleTextChange = useCallback(
28
- ({ currentTarget }) => setSearchText(currentTarget.value),
29
- [setSearchText]
22
+ ({ currentTarget }) =>
23
+ dispatch({ type: 'SET_SEARCH_TEXT', payload: currentTarget.value }),
24
+ [dispatch]
25
+ );
26
+ const resetSearch = useCallback(
27
+ () => dispatch({ type: 'SET_SEARCH_TEXT', payload: '' }),
28
+ [dispatch]
29
);
31
- const resetSearch = useCallback(() => setSearchText(''), [setSearchText]);
30
31
const handleKeyDown = useCallback(
32
event => {
33
// For convenience, let up/down arrow keys change Tree selection.
34
switch (event.key) {
35
case 'ArrowDown':
38
- selectNextElementInTree();
36
+ dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' });
37
event.preventDefault();
38
break;
39
case 'ArrowUp':
42
- selectPreviousElementInTree();
40
+ dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' });
41
event.preventDefault();
42
break;
43
default:
44
break;
45
}
46
},
49
- [selectNextElementInTree, selectPreviousElementInTree]
47
+ [dispatch]
48
);
49
50
const handleInputKeyPress = useCallback(
51
({ key }) => {
52
if (key === 'Enter') {
55
- goToNextSearchResult();
53
+ dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' });
54
}
55
},
58
- [goToNextSearchResult]
56
+ [dispatch]
57
);
58
59
// Auto-focus search input
@@ -106,7 +104,7 @@ export default function SearchInput(props: Props) {
104
<Button
105
className={styles.IconButton}
106
disabled={!searchText}
109
- onClick={goToPreviousSearchResult}
107
+ onClick={() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' })}
108
title="Scroll to previous search result"
109
>
110
<ButtonIcon type="up" />
@@ -114,7 +112,7 @@ export default function SearchInput(props: Props) {
112
<Button
113
className={styles.IconButton}
114
disabled={!searchText}
117
- onClick={goToNextSearchResult}
115
+ onClick={() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' })}
116
title="Scroll to next search result"
117
>
118
<ButtonIcon type="down" />
src/devtools/views/Components/SelectedElement.js
+15
-9
@@ -7,13 +7,14 @@ import React, {
7
useRef,
8
useState,
9
} from 'react';
10
-import { TreeContext } from './TreeContext';
10
+import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
11
import { BridgeContext, StoreContext } from '../context';
12
import Button from '../Button';
13
import ButtonIcon from '../ButtonIcon';
14
import HooksTree from './HooksTree';
15
import InspectedElementTree from './InspectedElementTree';
16
import { hydrate } from 'src/hydration';
17
+import ViewElementSourceContext from './ViewElementSourceContext';
18
import styles from './SelectedElement.css';
19
import {
20
ElementTypeClass,
@@ -29,7 +30,8 @@ import type { DehydratedData, Element } from './types';
30
export type Props = {||};
31
32
export default function SelectedElement(_: Props) {
32
- const { selectedElementID, viewElementSource } = useContext(TreeContext);
33
+ const { selectedElementID } = useContext(TreeStateContext);
34
+ const viewElementSource = useContext(ViewElementSourceContext);
35
const bridge = useContext(BridgeContext);
36
const store = useContext(StoreContext);
37
@@ -155,7 +157,7 @@ function InspectedElementView({
157
state,
158
} = inspectedElement;
159
158
- const { ownerStack } = useContext(TreeContext);
160
+ const { ownerStack } = useContext(TreeStateContext);
161
const bridge = useContext(BridgeContext);
162
const store = useContext(StoreContext);
163
@@ -243,12 +245,16 @@ function InspectedElementView({
245
}
246
247
function OwnerView({ displayName, id }: { displayName: string, id: number }) {
246
- const { selectElementByID } = useContext(TreeContext);
247
-
248
- const handleClick = useCallback(() => selectElementByID(id), [
249
- id,
250
- selectElementByID,
251
- ]);
248
+ const dispatch = useContext(TreeDispatcherContext);
249
+
250
+ const handleClick = useCallback(
251
+ () =>
252
+ dispatch({
253
+ type: 'SELECT_ELEMENT_BY_ID',
254
+ payload: id,
255
+ }),
256
+ [dispatch, id]
257
+ );
258
259
return (
260
<button
src/devtools/views/Components/Tree.js
+16
-31
@@ -11,7 +11,7 @@ import React, {
11
} from 'react';
12
import AutoSizer from 'react-virtualized-auto-sizer';
13
import { FixedSizeList } from 'react-window';
14
-import { TreeContext } from './TreeContext';
14
+import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
15
import { SettingsContext } from '../Settings/SettingsContext';
16
import { BridgeContext, StoreContext } from '../context';
17
import ElementView from './Element';
@@ -21,12 +21,9 @@ import SearchInput from './SearchInput';
21
22
import styles from './Tree.css';
23
24
-import type { Element } from './types';
25
-
24
export type ItemData = {|
25
baseDepth: number,
26
numElements: number,
29
- getElementAtIndex: (index: number) => Element | null,
27
isNavigatingWithKeyboard: boolean,
28
lastScrolledIDRef: { current: number | null },
29
onElementMouseEnter: (id: number) => void,
@@ -36,22 +33,16 @@ export type ItemData = {|
33
type Props = {||};
34
35
export default function Tree(props: Props) {
36
+ const dispatch = useContext(TreeDispatcherContext);
37
const {
38
baseDepth,
41
- getElementAtIndex,
39
numElements,
40
ownerStack,
41
searchIndex,
42
searchResults,
43
selectedElementID,
44
selectedElementIndex,
48
- selectChildElementInTree,
49
- selectElementAtIndex,
50
- selectNextElementInTree,
51
- selectOwner,
52
- selectParentElementInTree,
53
- selectPreviousElementInTree,
54
- } = useContext(TreeContext);
45
+ } = useContext(TreeStateContext);
46
const bridge = useContext(BridgeContext);
47
const store = useContext(StoreContext);
48
const [isNavigatingWithKeyboard, setIsNavigatingWithKeyboard] = useState(
@@ -111,7 +102,7 @@ export default function Tree(props: Props) {
102
switch (event.key) {
103
case 'ArrowDown':
104
event.preventDefault();
114
- selectNextElementInTree();
105
+ dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' });
106
break;
107
case 'ArrowLeft':
108
event.preventDefault();
@@ -123,7 +114,7 @@ export default function Tree(props: Props) {
114
if (element.children.length > 0 && !element.isCollapsed) {
115
store.toggleIsCollapsed(element.id, true);
116
} else {
126
- selectParentElementInTree();
117
+ dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' });
118
}
119
}
120
break;
@@ -137,13 +128,13 @@ export default function Tree(props: Props) {
128
if (element.children.length > 0 && element.isCollapsed) {
129
store.toggleIsCollapsed(element.id, false);
130
} else {
140
- selectChildElementInTree();
131
+ dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' });
132
}
133
}
134
break;
135
case 'ArrowUp':
136
event.preventDefault();
146
- selectPreviousElementInTree();
137
+ dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' });
138
break;
139
default:
140
return;
@@ -160,14 +151,7 @@ export default function Tree(props: Props) {
151
return () => {
152
ownerDocument.removeEventListener('keydown', handleKeyDown);
153
};
163
- }, [
164
- selectedElementID,
165
- selectChildElementInTree,
166
- selectNextElementInTree,
167
- selectParentElementInTree,
168
- selectPreviousElementInTree,
169
- store,
170
- ]);
154
+ }, [dispatch, selectedElementID, store]);
155
156
// Focus management.
157
const handleBlur = useCallback(() => setTreeFocused(false), []);
@@ -175,9 +159,12 @@ export default function Tree(props: Props) {
159
setTreeFocused(true);
160
161
if (selectedElementIndex === null && numElements > 0) {
178
- selectElementAtIndex(0);
162
+ dispatch({
163
+ type: 'SELECT_ELEMENT_AT_INDEX',
164
+ payload: 0,
165
+ });
166
}
180
- }, [numElements, selectedElementIndex, selectElementAtIndex]);
167
+ }, [dispatch, numElements, selectedElementIndex]);
168
169
const handleKeyPress = useCallback(
170
event => {
@@ -185,14 +172,14 @@ export default function Tree(props: Props) {
172
case 'Enter':
173
case ' ':
174
if (selectedElementID !== null) {
188
- selectOwner(selectedElementID);
175
+ dispatch({ type: 'SELECT_OWNER', payload: selectedElementID });
176
}
177
break;
178
default:
179
break;
180
}
181
},
195
- [selectedElementID, selectOwner]
182
+ [dispatch, selectedElementID]
183
);
184
185
const highlightElementInDOM = useCallback(
@@ -270,7 +257,6 @@ export default function Tree(props: Props) {
257
() => ({
258
baseDepth,
259
numElements,
273
- getElementAtIndex,
260
isNavigatingWithKeyboard,
261
onElementMouseEnter: handleElementMouseEnter,
262
lastScrolledIDRef,
@@ -279,7 +265,6 @@ export default function Tree(props: Props) {
265
[
266
baseDepth,
267
numElements,
282
- getElementAtIndex,
268
isNavigatingWithKeyboard,
269
handleElementMouseEnter,
270
lastScrolledIDRef,
@@ -328,7 +313,7 @@ export default function Tree(props: Props) {
313
}
314
315
function InnerElementType({ style, ...rest }) {
331
- const { ownerStack } = useContext(TreeContext);
316
+ const { ownerStack } = useContext(TreeStateContext);
317
318
// The list may need to scroll horizontally due to deeply nested elements.
319
// We don't know the maximum scroll width up front, because we're windowing.
src/devtools/views/Components/TreeContext.js
+113
-186
@@ -19,7 +19,6 @@
19
20
import React, {
21
createContext,
22
- useCallback,
22
useContext,
23
useEffect,
24
useLayoutEffect,
@@ -33,40 +32,91 @@ import Store from '../../store';
32
33
import type { Element } from './types';
34
36
-type Context = {|
35
+type StateContext = {|
36
// Tree
37
baseDepth: number,
38
numElements: number,
39
selectedElementID: number | null,
40
selectedElementIndex: number | null,
42
- getElementAtIndex(index: number): Element | null,
43
- selectChildElementInTree(): void,
44
- selectElementAtIndex(index: number): void,
45
- selectElementByID(id: number | null): void,
46
- selectNextElementInTree(): void,
47
- selectParentElementInTree(): void,
48
- selectPreviousElementInTree(): void,
41
42
// Search
43
searchIndex: number | null,
44
searchResults: Array<number>,
45
searchText: string,
54
- setSearchText(text: string): void,
55
- goToNextSearchResult(): void,
56
- goToPreviousSearchResult(): void,
46
47
// Owners
48
+ ownerFlatTree: Array<number> | null,
49
ownerStack: Array<number>,
50
ownerStackIndex: number | null,
61
- resetOwnerStack(): void,
62
- selectOwner(id: number): void,
51
+|};
52
64
- // Injected by parent HTML/JavaScript
65
- viewElementSource: Function | null,
53
+type ACTION_GO_TO_NEXT_SEARCH_RESULT = {|
54
+ type: 'GO_TO_NEXT_SEARCH_RESULT',
55
+|};
56
+type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = {|
57
+ type: 'GO_TO_PREVIOUS_SEARCH_RESULT',
58
+|};
59
+type ACTION_HANDLE_STORE_MUTATION = {|
60
+ type: 'HANDLE_STORE_MUTATION',
61
+ payload: [Uint32Array, Uint32Array],
62
+|};
63
+type ACTION_RESET_OWNER_STACK = {|
64
+ type: 'RESET_OWNER_STACK',
65
+|};
66
+type ACTION_SELECT_CHILD_ELEMENT_IN_TREE = {|
67
+ type: 'SELECT_CHILD_ELEMENT_IN_TREE',
68
+|};
69
+type ACTION_SELECT_ELEMENT_AT_INDEX = {|
70
+ type: 'SELECT_ELEMENT_AT_INDEX',
71
+ payload: number | null,
72
+|};
73
+type ACTION_SELECT_ELEMENT_BY_ID = {|
74
+ type: 'SELECT_ELEMENT_BY_ID',
75
+ payload: number | null,
76
+|};
77
+type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {|
78
+ type: 'SELECT_NEXT_ELEMENT_IN_TREE',
79
+|};
80
+type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {|
81
+ type: 'SELECT_PARENT_ELEMENT_IN_TREE',
82
+|};
83
+type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {|
84
+ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
85
+|};
86
+type ACTION_SELECT_OWNER = {|
87
+ type: 'SELECT_OWNER',
88
+ payload: number,
89
+|};
90
+type ACTION_SET_SEARCH_TEXT = {|
91
+ type: 'SET_SEARCH_TEXT',
92
+ payload: string,
93
|};
94
68
-const TreeContext = createContext<Context>(((null: any): Context));
69
-TreeContext.displayName = 'TreeContext';
95
+type Action =
96
+ | ACTION_GO_TO_NEXT_SEARCH_RESULT
97
+ | ACTION_GO_TO_PREVIOUS_SEARCH_RESULT
98
+ | ACTION_HANDLE_STORE_MUTATION
99
+ | ACTION_RESET_OWNER_STACK
100
+ | ACTION_SELECT_CHILD_ELEMENT_IN_TREE
101
+ | ACTION_SELECT_ELEMENT_AT_INDEX
102
+ | ACTION_SELECT_ELEMENT_BY_ID
103
+ | ACTION_SELECT_NEXT_ELEMENT_IN_TREE
104
+ | ACTION_SELECT_PARENT_ELEMENT_IN_TREE
105
+ | ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
106
+ | ACTION_SELECT_OWNER
107
+ | ACTION_SET_SEARCH_TEXT;
108
+
109
+type DispatcherContext = (action: Action) => void;
110
+
111
+const TreeStateContext = createContext<StateContext>(
112
+ ((null: any): StateContext)
113
+);
114
+TreeStateContext.displayName = 'TreeStateContext';
115
+
116
+const TreeDispatcherContext = createContext<DispatcherContext>(
117
+ ((null: any): DispatcherContext)
118
+);
119
+TreeDispatcherContext.displayName = 'TreeDispatcherContext';
120
121
type State = {|
122
// Tree
@@ -83,29 +133,10 @@ type State = {|
133
// Owners
134
ownerStack: Array<number>,
135
ownerStackIndex: number | null,
86
- _ownerFlatTree: Array<number> | null,
87
-|};
88
-
89
-type Action = {|
90
- type:
91
- | 'GO_TO_NEXT_SEARCH_RESULT'
92
- | 'GO_TO_PREVIOUS_SEARCH_RESULT'
93
- | 'HANDLE_STORE_MUTATION'
94
- | 'RESET_OWNER_STACK'
95
- | 'SELECT_CHILD_ELEMENT_IN_TREE'
96
- | 'SELECT_ELEMENT_AT_INDEX'
97
- | 'SELECT_ELEMENT_BY_ID'
98
- | 'SELECT_NEXT_ELEMENT_IN_TREE'
99
- | 'SELECT_PARENT_ELEMENT_IN_TREE'
100
- | 'SELECT_PREVIOUS_ELEMENT_IN_TREE'
101
- | 'SELECT_OWNER'
102
- | 'SET_SEARCH_TEXT',
103
- payload?: any,
136
+ ownerFlatTree: Array<number> | null,
137
|};
138
139
function reduceTreeState(store: Store, state: State, action: Action): State {
107
- const { type, payload } = action;
108
-
140
let {
141
numElements,
142
ownerStack,
@@ -117,7 +148,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
148
149
// Base tree should ignore selected element changes when the owner's tree is active.
150
if (ownerStack.length === 0) {
120
- switch (type) {
151
+ switch (action.type) {
152
case 'HANDLE_STORE_MUTATION':
153
numElements = store.numElements;
154
@@ -148,18 +179,18 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
179
}
180
break;
181
case 'SELECT_ELEMENT_AT_INDEX':
151
- selectedElementIndex = ((payload: any): number | null);
182
+ selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
183
break;
184
case 'SELECT_ELEMENT_BY_ID':
185
// Skip lookup in this case; it would be redundant.
186
// It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
187
lookupIDForIndex = false;
188
158
- selectedElementID = payload;
189
+ selectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
190
selectedElementIndex =
160
- payload === null
191
+ selectedElementID === null
192
? null
162
- : store.getIndexOfElementID(((payload: any): number));
193
+ : store.getIndexOfElementID(selectedElementID);
194
break;
195
case 'SELECT_NEXT_ELEMENT_IN_TREE':
196
if (
@@ -220,8 +251,6 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
251
}
252
253
function reduceSearchState(store: Store, state: State, action: Action): State {
223
- const { type, payload } = action;
224
-
254
let {
255
ownerStack,
256
searchIndex,
@@ -243,7 +272,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
272
273
// Search isn't supported when the owner's tree is active.
274
if (ownerStack.length === 0) {
246
- switch (type) {
275
+ switch (action.type) {
276
case 'GO_TO_NEXT_SEARCH_RESULT':
277
if (numPrevSearchResults > 0) {
278
didRequestSearch = true;
@@ -265,7 +294,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
294
const [
295
addedElementIDs,
296
removedElementIDs,
268
- ] = ((payload: any): Array<Uint32Array>);
297
+ ] = (action: ACTION_HANDLE_STORE_MUTATION).payload;
298
299
removedElementIDs.forEach(id => {
300
// Prune this item from the search results.
@@ -327,7 +356,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
356
case 'SET_SEARCH_TEXT':
357
searchIndex = null;
358
searchResults = [];
330
- searchText = ((payload: any): string);
359
+ searchText = (action: ACTION_SET_SEARCH_TEXT).payload;
360
361
if (searchText !== '') {
362
const regExp = createRegExp(searchText);
@@ -385,24 +414,22 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
414
}
415
416
function reduceOwnersState(store: Store, state: State, action: Action): State {
388
- const { payload, type } = action;
389
-
417
let {
418
baseDepth,
419
numElements,
420
selectedElementID,
421
selectedElementIndex,
422
+ ownerFlatTree,
423
ownerStack,
424
ownerStackIndex,
425
searchIndex,
426
searchResults,
427
searchText,
400
- _ownerFlatTree,
428
} = state;
429
430
let prevSelectedElementIndex = selectedElementIndex;
431
405
- switch (type) {
432
+ switch (action.type) {
433
case 'HANDLE_STORE_MUTATION':
434
if (ownerStack.length > 0) {
435
let indexOfRemovedItem = -1;
@@ -416,15 +443,15 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
443
if (indexOfRemovedItem >= 0) {
444
ownerStack = ownerStack.slice(0, indexOfRemovedItem);
445
if (ownerStack.length === 0) {
419
- _ownerFlatTree = null;
446
+ ownerFlatTree = null;
447
ownerStackIndex = null;
448
} else {
449
ownerStackIndex = ownerStack.length - 1;
450
}
451
}
425
- if (selectedElementID !== null && _ownerFlatTree !== null) {
452
+ if (selectedElementID !== null && ownerFlatTree !== null) {
453
// Mutation might have caused the index of this ID to shift.
427
- selectedElementIndex = _ownerFlatTree.indexOf(selectedElementID);
454
+ selectedElementIndex = ownerFlatTree.indexOf(selectedElementID);
455
}
456
} else {
457
if (selectedElementID !== null) {
@@ -445,30 +472,31 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
472
selectedElementID !== null
473
? store.getIndexOfElementID(selectedElementID)
474
: null;
448
- _ownerFlatTree = null;
475
+ ownerFlatTree = null;
476
break;
477
case 'SELECT_ELEMENT_AT_INDEX':
451
- if (_ownerFlatTree !== null) {
452
- selectedElementIndex = ((payload: any): number | null);
478
+ if (ownerFlatTree !== null) {
479
+ selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
480
}
481
break;
482
case 'SELECT_ELEMENT_BY_ID':
456
- if (_ownerFlatTree !== null) {
483
+ if (ownerFlatTree !== null) {
484
+ const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
485
selectedElementIndex =
458
- payload === null ? null : _ownerFlatTree.indexOf(payload);
486
+ payload === null ? null : ownerFlatTree.indexOf(payload);
487
}
488
break;
489
case 'SELECT_NEXT_ELEMENT_IN_TREE':
462
- if (_ownerFlatTree !== null && _ownerFlatTree.length > 0) {
490
+ if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
491
if (selectedElementIndex === null) {
492
selectedElementIndex = 0;
465
- } else if (selectedElementIndex + 1 < _ownerFlatTree.length) {
493
+ } else if (selectedElementIndex + 1 < ownerFlatTree.length) {
494
selectedElementIndex++;
495
}
496
}
497
break;
498
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
471
- if (_ownerFlatTree !== null && _ownerFlatTree.length > 0) {
499
+ if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
500
if (selectedElementIndex !== null && selectedElementIndex > 0) {
501
selectedElementIndex--;
502
}
@@ -478,7 +506,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
506
// If the Store doesn't have any owners metadata, don't drill into an empty stack.
507
// This is a confusing user experience.
508
if (store.hasOwnerMetadata) {
481
- ownerStackIndex = ownerStack.indexOf(payload);
509
+ const id = (action: ACTION_SELECT_OWNER).payload;
510
+ ownerStackIndex = ownerStack.indexOf(id);
511
512
// Always force reset selection to be the top of the new owner tree.
513
selectedElementIndex = 0;
@@ -489,7 +518,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
518
if (ownerStackIndex < 0) {
519
// Add this new owner, and fill in the owners above it as well.
520
ownerStack = [];
492
- let currentOwnerID = ((payload: any): number);
521
+ let currentOwnerID = id;
522
while (currentOwnerID !== 0) {
523
ownerStack.unshift(currentOwnerID);
524
currentOwnerID = ((store.getElementByID(
@@ -515,23 +544,23 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
544
if (
545
ownerStackIndex !== state.ownerStackIndex ||
546
ownerStack !== state.ownerStack ||
518
- type === 'HANDLE_STORE_MUTATION'
547
+ action.type === 'HANDLE_STORE_MUTATION'
548
) {
549
if (ownerStackIndex === null) {
521
- _ownerFlatTree = null;
550
+ ownerFlatTree = null;
551
baseDepth = 0;
552
numElements = store.numElements;
553
} else {
525
- _ownerFlatTree = calculateCurrentOwnerList(
554
+ ownerFlatTree = calculateCurrentOwnerList(
555
store,
556
ownerStack[ownerStackIndex],
557
ownerStack[ownerStackIndex],
558
[]
559
);
560
532
- baseDepth = ((store.getElementByID(_ownerFlatTree[0]): any): Element)
561
+ baseDepth = ((store.getElementByID(ownerFlatTree[0]): any): Element)
562
.depth;
534
- numElements = _ownerFlatTree.length;
563
+ numElements = ownerFlatTree.length;
564
}
565
}
566
@@ -539,8 +568,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
568
if (selectedElementIndex !== prevSelectedElementIndex) {
569
if (selectedElementIndex === null) {
570
selectedElementID = null;
542
- } else if (_ownerFlatTree !== null) {
543
- selectedElementID = _ownerFlatTree[((selectedElementIndex: any): number)];
571
+ } else if (ownerFlatTree !== null) {
572
+ selectedElementID = ownerFlatTree[((selectedElementIndex: any): number)];
573
}
574
}
575
@@ -558,17 +587,14 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
587
588
ownerStack,
589
ownerStackIndex,
561
- _ownerFlatTree,
590
+ ownerFlatTree,
591
};
592
}
593
565
-type Props = {|
566
- children: React$Node,
567
- viewElementSource: Function | null,
568
-|};
594
+type Props = {| children: React$Node |};
595
596
// TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
571
-function TreeContextController({ children, viewElementSource }: Props) {
597
+function TreeContextController({ children }: Props) {
598
const bridge = useContext(BridgeContext);
599
const store = useContext(StoreContext);
600
@@ -633,114 +659,9 @@ function TreeContextController({ children, viewElementSource }: Props) {
659
// Owners
660
ownerStack: [],
661
ownerStackIndex: null,
636
- _ownerFlatTree: null,
662
+ ownerFlatTree: null,
663
});
664
639
- const getElementAtIndex = useCallback(
640
- (index: number) => {
641
- return state._ownerFlatTree === null
642
- ? store.getElementAtIndex(index)
643
- : store.getElementByID(state._ownerFlatTree[index]);
644
- },
645
- [state, store]
646
- );
647
- const selectElementAtIndex = useCallback(
648
- (index: number) =>
649
- dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: index }),
650
- [dispatch]
651
- );
652
- const selectElementByID = useCallback(
653
- (id: number | null) =>
654
- dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: id }),
655
- [dispatch]
656
- );
657
- const setSearchText = useCallback(
658
- (text: string) => dispatch({ type: 'SET_SEARCH_TEXT', payload: text }),
659
- [dispatch]
660
- );
661
- const goToNextSearchResult = useCallback(
662
- () => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }),
663
- [dispatch]
664
- );
665
- const goToPreviousSearchResult = useCallback(
666
- () => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }),
667
- [dispatch]
668
- );
669
- const resetOwnerStack = useCallback(
670
- () => dispatch({ type: 'RESET_OWNER_STACK' }),
671
- [dispatch]
672
- );
673
- const selectChildElementInTree = useCallback(
674
- () => dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }),
675
- [dispatch]
676
- );
677
- const selectNextElementInTree = useCallback(
678
- () => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }),
679
- [dispatch]
680
- );
681
- const selectParentElementInTree = useCallback(
682
- () => dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }),
683
- [dispatch]
684
- );
685
- const selectPreviousElementInTree = useCallback(
686
- () => dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }),
687
- [dispatch]
688
- );
689
- const selectOwner = useCallback(
690
- (id: number) => dispatch({ type: 'SELECT_OWNER', payload: id }),
691
- [dispatch]
692
- );
693
-
694
- const value = useMemo(
695
- () => ({
696
- // Tree (derived from Store or owners state)
697
- baseDepth: state.baseDepth,
698
- numElements: state.numElements,
699
- selectedElementID: state.selectedElementID,
700
- selectedElementIndex: state.selectedElementIndex,
701
- getElementAtIndex,
702
- selectChildElementInTree,
703
- selectElementByID,
704
- selectElementAtIndex,
705
- selectNextElementInTree,
706
- selectParentElementInTree,
707
- selectPreviousElementInTree,
708
-
709
- // Search
710
- searchIndex: state.searchIndex,
711
- searchResults: state.searchResults,
712
- searchText: state.searchText,
713
- setSearchText,
714
- goToNextSearchResult,
715
- goToPreviousSearchResult,
716
-
717
- // Owners
718
- ownerStack: state.ownerStack,
719
- ownerStackIndex: state.ownerStackIndex,
720
- resetOwnerStack,
721
- selectOwner,
722
-
723
- // Injected by parent HTML/JavaScript
724
- viewElementSource,
725
- }),
726
- [
727
- getElementAtIndex,
728
- goToNextSearchResult,
729
- goToPreviousSearchResult,
730
- resetOwnerStack,
731
- selectChildElementInTree,
732
- selectElementAtIndex,
733
- selectElementByID,
734
- selectNextElementInTree,
735
- selectParentElementInTree,
736
- selectOwner,
737
- selectPreviousElementInTree,
738
- setSearchText,
739
- state,
740
- viewElementSource,
741
- ]
742
- );
743
-
665
// Listen for host element selections.
666
useEffect(() => {
667
const handleSelectFiber = (id: number) =>
@@ -793,7 +714,13 @@ function TreeContextController({ children, viewElementSource }: Props) {
714
return () => store.removeListener('mutated', handleStoreMutated);
715
}, [dispatch, initialRevision, store]);
716
796
- return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
717
+ return (
718
+ <TreeStateContext.Provider value={state}>
719
+ <TreeDispatcherContext.Provider value={dispatch}>
720
+ {children}
721
+ </TreeDispatcherContext.Provider>
722
+ </TreeStateContext.Provider>
723
+ );
724
}
725
726
function calculateCurrentOwnerList(
@@ -842,4 +769,4 @@ function recursivelySearchTree(
769
);
770
}
771
845
-export { TreeContext, TreeContextController };
772
+export { TreeDispatcherContext, TreeStateContext, TreeContextController };
src/devtools/views/Components/ViewElementSourceContext.js
new
+8
@@ -0,0 +1,8 @@
1
+// @flow
2
+
3
+import { createContext } from 'react';
4
+
5
+const ViewElementSourceContext = createContext<Function | null>(null);
6
+ViewElementSourceContext.displayName = 'ViewElementSourceContext';
7
+
8
+export default ViewElementSourceContext;
src/devtools/views/DevTools.js
+47
-38
@@ -14,6 +14,7 @@ import Settings from './Settings/Settings';
14
import TabBar from './TabBar';
15
import { SettingsContextController } from './Settings/SettingsContext';
16
import { TreeContextController } from './Components/TreeContext';
17
+import ViewElementSourceContext from './Components/ViewElementSourceContext';
18
import { ProfilerContextController } from './Profiler/ProfilerContext';
19
import ReactLogo from './ReactLogo';
20
@@ -121,47 +122,55 @@ export default function DevTools({
122
profilerPortalContainer={profilerPortalContainer}
123
settingsPortalContainer={settingsPortalContainer}
124
>
124
- <TreeContextController viewElementSource={viewElementSource}>
125
- <ProfilerContextController>
126
- <div className={styles.DevTools}>
127
- {showTabBar && (
128
- <div className={styles.TabBar}>
129
- <ReactLogo />
130
- <span className={styles.DevToolsVersion}>
131
- {process.env.DEVTOOLS_VERSION}
132
- </span>
133
- <div className={styles.Spacer} />
134
- <TabBar
135
- currentTab={tab}
136
- id="DevTools"
137
- selectTab={setTab}
138
- size="large"
139
- tabs={
140
- supportsProfiling
141
- ? tabsWithProfiler
142
- : tabsWithoutProfiler
143
- }
125
+ <ViewElementSourceContext.Provider value={viewElementSource}>
126
+ <TreeContextController>
127
+ <ProfilerContextController>
128
+ <div className={styles.DevTools}>
129
+ {showTabBar && (
130
+ <div className={styles.TabBar}>
131
+ <ReactLogo />
132
+ <span className={styles.DevToolsVersion}>
133
+ {process.env.DEVTOOLS_VERSION}
134
+ </span>
135
+ <div className={styles.Spacer} />
136
+ <TabBar
137
+ currentTab={tab}
138
+ id="DevTools"
139
+ selectTab={setTab}
140
+ size="large"
141
+ tabs={
142
+ supportsProfiling
143
+ ? tabsWithProfiler
144
+ : tabsWithoutProfiler
145
+ }
146
+ />
147
+ </div>
148
+ )}
149
+ <div
150
+ className={styles.TabContent}
151
+ hidden={tab !== 'components'}
152
+ >
153
+ <Components portalContainer={componentsPortalContainer} />
154
+ </div>
155
+ <div
156
+ className={styles.TabContent}
157
+ hidden={tab !== 'profiler'}
158
+ >
159
+ <Profiler
160
+ portalContainer={profilerPortalContainer}
161
+ supportsProfiling={supportsProfiling}
162
/>
163
</div>
146
- )}
147
- <div
148
- className={styles.TabContent}
149
- hidden={tab !== 'components'}
150
- >
151
- <Components portalContainer={componentsPortalContainer} />
152
- </div>
153
- <div className={styles.TabContent} hidden={tab !== 'profiler'}>
154
- <Profiler
155
- portalContainer={profilerPortalContainer}
156
- supportsProfiling={supportsProfiling}
157
- />
158
- </div>
159
- <div className={styles.TabContent} hidden={tab !== 'settings'}>
160
- <Settings portalContainer={settingsPortalContainer} />
164
+ <div
165
+ className={styles.TabContent}
166
+ hidden={tab !== 'settings'}
167
+ >
168
+ <Settings portalContainer={settingsPortalContainer} />
169
+ </div>
170
</div>
162
- </div>
163
- </ProfilerContextController>
164
- </TreeContextController>
171
+ </ProfilerContextController>
172
+ </TreeContextController>
173
+ </ViewElementSourceContext.Provider>
174
</SettingsContextController>
175
</StoreContext.Provider>
176
</BridgeContext.Provider>
src/devtools/views/Profiler/ProfilerContext.js
+11
-4
@@ -9,7 +9,10 @@ import React, {
9
} from 'react';
10
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
11
import { useLocalStorage, useSubscription } from '../hooks';
12
-import { TreeContext } from '../Components/TreeContext';
12
+import {
13
+ TreeDispatcherContext,
14
+ TreeStateContext,
15
+} from '../Components/TreeContext';
16
import { StoreContext } from '../context';
17
import Store from '../../store';
18
@@ -79,7 +82,8 @@ type Props = {|
82
83
function ProfilerContextController({ children }: Props) {
84
const store = useContext(StoreContext);
82
- const { selectElementByID, selectedElementID } = useContext(TreeContext);
85
+ const { selectedElementID } = useContext(TreeStateContext);
86
+ const dispatch = useContext(TreeDispatcherContext);
87
88
const subscription = useMemo(
89
() => ({
@@ -155,11 +159,14 @@ function ProfilerContextController({ children }: Props) {
159
// If this element is still in the store, then select it in the Components tab as well.
160
const element = store.getElementByID(id);
161
if (element !== null) {
158
- selectElementByID(id);
162
+ dispatch({
163
+ type: 'SELECT_ELEMENT_BY_ID',
164
+ payload: id,
165
+ });
166
}
167
}
168
},
162
- [selectElementByID, selectFiberID, selectFiberName, store]
169
+ [dispatch, selectFiberID, selectFiberName, store]
170
);
171
172
if (isProfiling) {