@samitouri / QOS-React / commits / d53ae2ea8a

Refactored TreeContext to use less memoization (based on feedback from Sebastian)

Brian Vaughn committed Apr 22, 2019 at 10:19 UTC d53ae2ea8a5db88232c6802fc8f480cb13c23576
10 files changed +268 -321
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/InspectedElementContext.js
+2 -2
@@ -10,7 +10,7 @@ import React, {
10 import { createResource } from '../../cache';
11 import { BridgeContext, StoreContext } from '../context';
12 import { hydrate } from 'src/hydration';
13 -import { TreeContext } from './TreeContext';
13 +import { TreeStateContext } from './TreeContext';
14
15 import type {
16 DehydratedData,
@@ -38,7 +38,7 @@ type Props = {|
38 function InspectedElementContextController({ children }: Props) {
39 const bridge = useContext(BridgeContext);
40 const store = useContext(StoreContext);
41 - const { inspectedElementID } = useContext(TreeContext);
41 + const { inspectedElementID } = useContext(TreeStateContext);
42
43 const [count, setCount] = useState<number>(0);
44
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
+14 -8
@@ -1,13 +1,14 @@
1 // @flow
2
3 import React, { useCallback, useContext } from 'react';
4 -import { TreeContext } from './TreeContext';
4 +import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
5 import { BridgeContext, StoreContext } from '../context';
6 import Button from '../Button';
7 import ButtonIcon from '../ButtonIcon';
8 import HooksTree from './HooksTree';
9 import InspectedElementTree from './InspectedElementTree';
10 import { InspectedElementContext } from './InspectedElementContext';
11 +import ViewElementSourceContext from './ViewElementSourceContext';
12 import styles from './SelectedElement.css';
13 import {
14 ElementTypeClass,
@@ -22,7 +23,8 @@ import type { Element, InspectedElement } from './types';
23 export type Props = {||};
24
25 export default function SelectedElement(_: Props) {
25 - const { inspectedElementID, viewElementSource } = useContext(TreeContext);
26 + const { inspectedElementID } = useContext(TreeStateContext);
27 + const viewElementSource = useContext(ViewElementSourceContext);
28 const bridge = useContext(BridgeContext);
29 const store = useContext(StoreContext);
30
@@ -153,7 +155,7 @@ function InspectedElementView({
155 state,
156 } = inspectedElement;
157
156 - const { ownerStack } = useContext(TreeContext);
158 + const { ownerStack } = useContext(TreeStateContext);
159 const bridge = useContext(BridgeContext);
160 const store = useContext(StoreContext);
161
@@ -241,12 +243,16 @@ function InspectedElementView({
243 }
244
245 function OwnerView({ displayName, id }: { displayName: string, id: number }) {
244 - const { selectElementByID } = useContext(TreeContext);
246 + const dispatch = useContext(TreeDispatcherContext);
247
246 - const handleClick = useCallback(() => selectElementByID(id), [
247 - id,
248 - selectElementByID,
249 - ]);
248 + const handleClick = useCallback(
249 + () =>
250 + dispatch({
251 + type: 'SELECT_ELEMENT_BY_ID',
252 + payload: id,
253 + }),
254 + [dispatch, id]
255 + );
256
257 return (
258 <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
+120 -194
@@ -34,44 +34,98 @@ import Store from '../../store';
34
35 import type { Element } from './types';
36
37 -type Context = {|
37 +type StateContext = {|
38 // Tree
39 baseDepth: number,
40 numElements: number,
41 selectedElementID: number | null,
42 selectedElementIndex: number | null,
43 - getElementAtIndex(index: number): Element | null,
44 - selectChildElementInTree(): void,
45 - selectElementAtIndex(index: number): void,
46 - selectElementByID(id: number | null): void,
47 - selectNextElementInTree(): void,
48 - selectParentElementInTree(): void,
49 - selectPreviousElementInTree(): void,
43
44 // Search
45 searchIndex: number | null,
46 searchResults: Array<number>,
47 searchText: string,
55 - setSearchText(text: string): void,
56 - goToNextSearchResult(): void,
57 - goToPreviousSearchResult(): void,
48
49 // Owners
50 + ownerFlatTree: Array<number> | null,
51 ownerStack: Array<number>,
52 ownerStackIndex: number | null,
62 - resetOwnerStack(): void,
63 - selectOwner(id: number): void,
64 -
65 - // Injected by parent HTML/JavaScript
66 - viewElementSource: Function | null,
53
54 // Inspection element panel
69 - // Updated separately so we can avoid suspending when selection changes
55 inspectedElementID: number | null,
56 |};
57
73 -const TreeContext = createContext<Context>(((null: any): Context));
74 -TreeContext.displayName = 'TreeContext';
58 +type ACTION_GO_TO_NEXT_SEARCH_RESULT = {|
59 + type: 'GO_TO_NEXT_SEARCH_RESULT',
60 +|};
61 +type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = {|
62 + type: 'GO_TO_PREVIOUS_SEARCH_RESULT',
63 +|};
64 +type ACTION_HANDLE_STORE_MUTATION = {|
65 + type: 'HANDLE_STORE_MUTATION',
66 + payload: [Uint32Array, Uint32Array],
67 +|};
68 +type ACTION_RESET_OWNER_STACK = {|
69 + type: 'RESET_OWNER_STACK',
70 +|};
71 +type ACTION_SELECT_CHILD_ELEMENT_IN_TREE = {|
72 + type: 'SELECT_CHILD_ELEMENT_IN_TREE',
73 +|};
74 +type ACTION_SELECT_ELEMENT_AT_INDEX = {|
75 + type: 'SELECT_ELEMENT_AT_INDEX',
76 + payload: number | null,
77 +|};
78 +type ACTION_SELECT_ELEMENT_BY_ID = {|
79 + type: 'SELECT_ELEMENT_BY_ID',
80 + payload: number | null,
81 +|};
82 +type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {|
83 + type: 'SELECT_NEXT_ELEMENT_IN_TREE',
84 +|};
85 +type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {|
86 + type: 'SELECT_PARENT_ELEMENT_IN_TREE',
87 +|};
88 +type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {|
89 + type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
90 +|};
91 +type ACTION_SELECT_OWNER = {|
92 + type: 'SELECT_OWNER',
93 + payload: number,
94 +|};
95 +type ACTION_SET_SEARCH_TEXT = {|
96 + type: 'SET_SEARCH_TEXT',
97 + payload: string,
98 +|};
99 +type ACTION_UPDATE_INSPECTED_ELEMENT_ID = {|
100 + type: 'UPDATE_INSPECTED_ELEMENT_ID',
101 +|};
102 +
103 +type Action =
104 + | ACTION_GO_TO_NEXT_SEARCH_RESULT
105 + | ACTION_GO_TO_PREVIOUS_SEARCH_RESULT
106 + | ACTION_HANDLE_STORE_MUTATION
107 + | ACTION_RESET_OWNER_STACK
108 + | ACTION_SELECT_CHILD_ELEMENT_IN_TREE
109 + | ACTION_SELECT_ELEMENT_AT_INDEX
110 + | ACTION_SELECT_ELEMENT_BY_ID
111 + | ACTION_SELECT_NEXT_ELEMENT_IN_TREE
112 + | ACTION_SELECT_PARENT_ELEMENT_IN_TREE
113 + | ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
114 + | ACTION_SELECT_OWNER
115 + | ACTION_SET_SEARCH_TEXT
116 + | ACTION_UPDATE_INSPECTED_ELEMENT_ID;
117 +
118 +type DispatcherContext = (action: Action) => void;
119 +
120 +const TreeStateContext = createContext<StateContext>(
121 + ((null: any): StateContext)
122 +);
123 +TreeStateContext.displayName = 'TreeStateContext';
124 +
125 +const TreeDispatcherContext = createContext<DispatcherContext>(
126 + ((null: any): DispatcherContext)
127 +);
128 +TreeDispatcherContext.displayName = 'TreeDispatcherContext';
129
130 type State = {|
131 // Tree
@@ -88,33 +142,13 @@ type State = {|
142 // Owners
143 ownerStack: Array<number>,
144 ownerStackIndex: number | null,
91 - _ownerFlatTree: Array<number> | null,
145 + ownerFlatTree: Array<number> | null,
146
147 // Inspection element panel
148 inspectedElementID: number | null,
149 |};
150
97 -type Action = {|
98 - type:
99 - | 'GO_TO_NEXT_SEARCH_RESULT'
100 - | 'GO_TO_PREVIOUS_SEARCH_RESULT'
101 - | 'HANDLE_STORE_MUTATION'
102 - | 'RESET_OWNER_STACK'
103 - | 'SELECT_CHILD_ELEMENT_IN_TREE'
104 - | 'SELECT_ELEMENT_AT_INDEX'
105 - | 'SELECT_ELEMENT_BY_ID'
106 - | 'SELECT_NEXT_ELEMENT_IN_TREE'
107 - | 'SELECT_PARENT_ELEMENT_IN_TREE'
108 - | 'SELECT_PREVIOUS_ELEMENT_IN_TREE'
109 - | 'SELECT_OWNER'
110 - | 'SET_SEARCH_TEXT'
111 - | 'UPDATE_INSPECTED_ELEMENT_ID',
112 - payload?: any,
113 -|};
114 -
151 function reduceTreeState(store: Store, state: State, action: Action): State {
116 - const { type, payload } = action;
117 -
152 let {
153 numElements,
154 ownerStack,
@@ -126,7 +160,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
160
161 // Base tree should ignore selected element changes when the owner's tree is active.
162 if (ownerStack.length === 0) {
129 - switch (type) {
163 + switch (action.type) {
164 case 'HANDLE_STORE_MUTATION':
165 numElements = store.numElements;
166
@@ -157,18 +191,18 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
191 }
192 break;
193 case 'SELECT_ELEMENT_AT_INDEX':
160 - selectedElementIndex = ((payload: any): number | null);
194 + selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
195 break;
196 case 'SELECT_ELEMENT_BY_ID':
197 // Skip lookup in this case; it would be redundant.
198 // It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
199 lookupIDForIndex = false;
200
167 - selectedElementID = payload;
201 + selectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
202 selectedElementIndex =
169 - payload === null
203 + selectedElementID === null
204 ? null
171 - : store.getIndexOfElementID(((payload: any): number));
205 + : store.getIndexOfElementID(selectedElementID);
206 break;
207 case 'SELECT_NEXT_ELEMENT_IN_TREE':
208 if (
@@ -229,8 +263,6 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
263 }
264
265 function reduceSearchState(store: Store, state: State, action: Action): State {
232 - const { type, payload } = action;
233 -
266 let {
267 ownerStack,
268 searchIndex,
@@ -252,7 +284,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
284
285 // Search isn't supported when the owner's tree is active.
286 if (ownerStack.length === 0) {
255 - switch (type) {
287 + switch (action.type) {
288 case 'GO_TO_NEXT_SEARCH_RESULT':
289 if (numPrevSearchResults > 0) {
290 didRequestSearch = true;
@@ -274,7 +306,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
306 const [
307 addedElementIDs,
308 removedElementIDs,
277 - ] = ((payload: any): Array<Uint32Array>);
309 + ] = (action: ACTION_HANDLE_STORE_MUTATION).payload;
310
311 removedElementIDs.forEach(id => {
312 // Prune this item from the search results.
@@ -336,7 +368,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
368 case 'SET_SEARCH_TEXT':
369 searchIndex = null;
370 searchResults = [];
339 - searchText = ((payload: any): string);
371 + searchText = (action: ACTION_SET_SEARCH_TEXT).payload;
372
373 if (searchText !== '') {
374 const regExp = createRegExp(searchText);
@@ -394,24 +426,22 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
426 }
427
428 function reduceOwnersState(store: Store, state: State, action: Action): State {
397 - const { payload, type } = action;
398 -
429 let {
430 baseDepth,
431 numElements,
432 selectedElementID,
433 selectedElementIndex,
434 + ownerFlatTree,
435 ownerStack,
436 ownerStackIndex,
437 searchIndex,
438 searchResults,
439 searchText,
409 - _ownerFlatTree,
440 } = state;
441
442 let prevSelectedElementIndex = selectedElementIndex;
443
414 - switch (type) {
444 + switch (action.type) {
445 case 'HANDLE_STORE_MUTATION':
446 if (ownerStack.length > 0) {
447 let indexOfRemovedItem = -1;
@@ -425,15 +455,15 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
455 if (indexOfRemovedItem >= 0) {
456 ownerStack = ownerStack.slice(0, indexOfRemovedItem);
457 if (ownerStack.length === 0) {
428 - _ownerFlatTree = null;
458 + ownerFlatTree = null;
459 ownerStackIndex = null;
460 } else {
461 ownerStackIndex = ownerStack.length - 1;
462 }
463 }
434 - if (selectedElementID !== null && _ownerFlatTree !== null) {
464 + if (selectedElementID !== null && ownerFlatTree !== null) {
465 // Mutation might have caused the index of this ID to shift.
436 - selectedElementIndex = _ownerFlatTree.indexOf(selectedElementID);
466 + selectedElementIndex = ownerFlatTree.indexOf(selectedElementID);
467 }
468 } else {
469 if (selectedElementID !== null) {
@@ -454,30 +484,31 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
484 selectedElementID !== null
485 ? store.getIndexOfElementID(selectedElementID)
486 : null;
457 - _ownerFlatTree = null;
487 + ownerFlatTree = null;
488 break;
489 case 'SELECT_ELEMENT_AT_INDEX':
460 - if (_ownerFlatTree !== null) {
461 - selectedElementIndex = ((payload: any): number | null);
490 + if (ownerFlatTree !== null) {
491 + selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
492 }
493 break;
494 case 'SELECT_ELEMENT_BY_ID':
465 - if (_ownerFlatTree !== null) {
495 + if (ownerFlatTree !== null) {
496 + const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
497 selectedElementIndex =
467 - payload === null ? null : _ownerFlatTree.indexOf(payload);
498 + payload === null ? null : ownerFlatTree.indexOf(payload);
499 }
500 break;
501 case 'SELECT_NEXT_ELEMENT_IN_TREE':
471 - if (_ownerFlatTree !== null && _ownerFlatTree.length > 0) {
502 + if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
503 if (selectedElementIndex === null) {
504 selectedElementIndex = 0;
474 - } else if (selectedElementIndex + 1 < _ownerFlatTree.length) {
505 + } else if (selectedElementIndex + 1 < ownerFlatTree.length) {
506 selectedElementIndex++;
507 }
508 }
509 break;
510 case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
480 - if (_ownerFlatTree !== null && _ownerFlatTree.length > 0) {
511 + if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
512 if (selectedElementIndex !== null && selectedElementIndex > 0) {
513 selectedElementIndex--;
514 }
@@ -487,7 +518,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
518 // If the Store doesn't have any owners metadata, don't drill into an empty stack.
519 // This is a confusing user experience.
520 if (store.hasOwnerMetadata) {
490 - ownerStackIndex = ownerStack.indexOf(payload);
521 + const id = (action: ACTION_SELECT_OWNER).payload;
522 + ownerStackIndex = ownerStack.indexOf(id);
523
524 // Always force reset selection to be the top of the new owner tree.
525 selectedElementIndex = 0;
@@ -498,7 +530,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
530 if (ownerStackIndex < 0) {
531 // Add this new owner, and fill in the owners above it as well.
532 ownerStack = [];
501 - let currentOwnerID = ((payload: any): number);
533 + let currentOwnerID = id;
534 while (currentOwnerID !== 0) {
535 ownerStack.unshift(currentOwnerID);
536 currentOwnerID = ((store.getElementByID(
@@ -524,23 +556,23 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
556 if (
557 ownerStackIndex !== state.ownerStackIndex ||
558 ownerStack !== state.ownerStack ||
527 - type === 'HANDLE_STORE_MUTATION'
559 + action.type === 'HANDLE_STORE_MUTATION'
560 ) {
561 if (ownerStackIndex === null) {
530 - _ownerFlatTree = null;
562 + ownerFlatTree = null;
563 baseDepth = 0;
564 numElements = store.numElements;
565 } else {
534 - _ownerFlatTree = calculateCurrentOwnerList(
566 + ownerFlatTree = calculateCurrentOwnerList(
567 store,
568 ownerStack[ownerStackIndex],
569 ownerStack[ownerStackIndex],
570 []
571 );
572
541 - baseDepth = ((store.getElementByID(_ownerFlatTree[0]): any): Element)
573 + baseDepth = ((store.getElementByID(ownerFlatTree[0]): any): Element)
574 .depth;
543 - numElements = _ownerFlatTree.length;
575 + numElements = ownerFlatTree.length;
576 }
577 }
578
@@ -548,8 +580,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
580 if (selectedElementIndex !== prevSelectedElementIndex) {
581 if (selectedElementIndex === null) {
582 selectedElementID = null;
551 - } else if (_ownerFlatTree !== null) {
552 - selectedElementID = _ownerFlatTree[((selectedElementIndex: any): number)];
583 + } else if (ownerFlatTree !== null) {
584 + selectedElementID = ownerFlatTree[((selectedElementIndex: any): number)];
585 }
586 }
587
@@ -567,7 +599,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
599
600 ownerStack,
601 ownerStackIndex,
570 - _ownerFlatTree,
602 + ownerFlatTree,
603 };
604 }
605
@@ -589,13 +621,10 @@ function reduceSuspenseState(
621 }
622 }
623
592 -type Props = {|
593 - children: React$Node,
594 - viewElementSource: Function | null,
595 -|};
624 +type Props = {| children: React$Node |};
625
626 // TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
598 -function TreeContextController({ children, viewElementSource }: Props) {
627 +function TreeContextController({ children }: Props) {
628 const bridge = useContext(BridgeContext);
629 const store = useContext(StoreContext);
630
@@ -662,129 +691,20 @@ function TreeContextController({ children, viewElementSource }: Props) {
691 // Owners
692 ownerStack: [],
693 ownerStackIndex: null,
665 - _ownerFlatTree: null,
694 + ownerFlatTree: null,
695
696 // Inspection element panel
697 inspectedElementID: null,
698 });
699
700 const dispatchWrapper = useCallback(
672 - params => {
673 - dispatch(params);
701 + (action: Action) => {
702 + dispatch(action);
703 next(() => dispatch({ type: 'UPDATE_INSPECTED_ELEMENT_ID' }));
704 },
705 [dispatch]
706 );
707
679 - const getElementAtIndex = useCallback(
680 - (index: number) => {
681 - return state._ownerFlatTree === null
682 - ? store.getElementAtIndex(index)
683 - : store.getElementByID(state._ownerFlatTree[index]);
684 - },
685 - [state, store]
686 - );
687 - const selectElementAtIndex = useCallback(
688 - (index: number) =>
689 - dispatchWrapper({ type: 'SELECT_ELEMENT_AT_INDEX', payload: index }),
690 - [dispatchWrapper]
691 - );
692 - const selectElementByID = useCallback(
693 - (id: number | null) =>
694 - dispatchWrapper({ type: 'SELECT_ELEMENT_BY_ID', payload: id }),
695 - [dispatchWrapper]
696 - );
697 - const setSearchText = useCallback(
698 - (text: string) =>
699 - dispatchWrapper({ type: 'SET_SEARCH_TEXT', payload: text }),
700 - [dispatchWrapper]
701 - );
702 - const goToNextSearchResult = useCallback(
703 - () => dispatchWrapper({ type: 'GO_TO_NEXT_SEARCH_RESULT' }),
704 - [dispatchWrapper]
705 - );
706 - const goToPreviousSearchResult = useCallback(
707 - () => dispatchWrapper({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }),
708 - [dispatchWrapper]
709 - );
710 - const resetOwnerStack = useCallback(
711 - () => dispatchWrapper({ type: 'RESET_OWNER_STACK' }),
712 - [dispatchWrapper]
713 - );
714 - const selectChildElementInTree = useCallback(
715 - () => dispatchWrapper({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }),
716 - [dispatchWrapper]
717 - );
718 - const selectNextElementInTree = useCallback(
719 - () => dispatchWrapper({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }),
720 - [dispatchWrapper]
721 - );
722 - const selectParentElementInTree = useCallback(
723 - () => dispatchWrapper({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }),
724 - [dispatchWrapper]
725 - );
726 - const selectPreviousElementInTree = useCallback(
727 - () => dispatchWrapper({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }),
728 - [dispatchWrapper]
729 - );
730 - const selectOwner = useCallback(
731 - (id: number) => dispatchWrapper({ type: 'SELECT_OWNER', payload: id }),
732 - [dispatchWrapper]
733 - );
734 -
735 - const value = useMemo(
736 - () => ({
737 - // Tree (derived from Store or owners state)
738 - baseDepth: state.baseDepth,
739 - numElements: state.numElements,
740 - selectedElementID: state.selectedElementID,
741 - selectedElementIndex: state.selectedElementIndex,
742 - getElementAtIndex,
743 - selectChildElementInTree,
744 - selectElementByID,
745 - selectElementAtIndex,
746 - selectNextElementInTree,
747 - selectParentElementInTree,
748 - selectPreviousElementInTree,
749 -
750 - // Search
751 - searchIndex: state.searchIndex,
752 - searchResults: state.searchResults,
753 - searchText: state.searchText,
754 - setSearchText,
755 - goToNextSearchResult,
756 - goToPreviousSearchResult,
757 -
758 - // Owners
759 - ownerStack: state.ownerStack,
760 - ownerStackIndex: state.ownerStackIndex,
761 - resetOwnerStack,
762 - selectOwner,
763 -
764 - // Inspection element panel
765 - inspectedElementID: state.inspectedElementID,
766 -
767 - // Injected by parent HTML/JavaScript
768 - viewElementSource,
769 - }),
770 - [
771 - getElementAtIndex,
772 - goToNextSearchResult,
773 - goToPreviousSearchResult,
774 - resetOwnerStack,
775 - selectChildElementInTree,
776 - selectElementAtIndex,
777 - selectElementByID,
778 - selectNextElementInTree,
779 - selectParentElementInTree,
780 - selectOwner,
781 - selectPreviousElementInTree,
782 - setSearchText,
783 - state,
784 - viewElementSource,
785 - ]
786 - );
787 -
708 // Listen for host element selections.
709 useEffect(() => {
710 const handleSelectFiber = (id: number) =>
@@ -837,7 +757,13 @@ function TreeContextController({ children, viewElementSource }: Props) {
757 return () => store.removeListener('mutated', handleStoreMutated);
758 }, [dispatchWrapper, initialRevision, store]);
759
840 - return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
760 + return (
761 + <TreeStateContext.Provider value={state}>
762 + <TreeDispatcherContext.Provider value={dispatchWrapper}>
763 + {children}
764 + </TreeDispatcherContext.Provider>
765 + </TreeStateContext.Provider>
766 + );
767 }
768
769 function calculateCurrentOwnerList(
@@ -886,4 +812,4 @@ function recursivelySearchTree(
812 );
813 }
814
889 -export { TreeContext, TreeContextController };
815 +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) {