@samitouri / QOS-React / commits / 433934f55e

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