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

This commit is contained in:
Brian Vaughn
2019-04-22 10:39:21 -07:00
parent 76b29ad0a9
commit 433934f55e
9 changed files with 260 additions and 312 deletions
+21 -13
View File
@@ -13,7 +13,7 @@ import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
import Store from 'src/devtools/store';
import ButtonIcon from '../ButtonIcon';
import { createRegExp } from '../utils';
import { TreeContext } from './TreeContext';
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
import { StoreContext } from '../context';
import type { ItemData } from './Tree';
@@ -28,18 +28,21 @@ type Props = {
};
export default function ElementView({ data, index, style }: Props) {
const [isHovered, setIsHovered] = useState(false);
const store = useContext(StoreContext);
const {
baseDepth,
getElementAtIndex,
ownerFlatTree,
ownerStack,
selectOwner,
selectedElementID,
selectElementByID,
} = useContext(TreeContext);
const store = useContext(StoreContext);
} = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const element = getElementAtIndex(index);
const element =
ownerFlatTree !== null
? store.getElementByID(ownerFlatTree[index])
: store.getElementAtIndex(index);
const [isHovered, setIsHovered] = useState(false);
const {
lastScrolledIDRef,
@@ -52,9 +55,9 @@ export default function ElementView({ data, index, style }: Props) {
const handleDoubleClick = useCallback(() => {
if (id !== null) {
selectOwner(id);
dispatch({ type: 'SELECT_OWNER', payload: id });
}
}, [id, selectOwner]);
}, [dispatch, id]);
const scrollAnchorStartRef = useRef<HTMLSpanElement | null>(null);
const scrollAnchorEndRef = useRef<HTMLSpanElement | null>(null);
@@ -102,10 +105,13 @@ export default function ElementView({ data, index, style }: Props) {
const handleMouseDown = useCallback(
({ metaKey }) => {
if (id !== null) {
selectElementByID(metaKey ? null : id);
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: metaKey ? null : id,
});
}
},
[id, selectElementByID]
[dispatch, id]
);
const handleMouseEnter = useCallback(() => {
@@ -234,7 +240,9 @@ type DisplayNameProps = {|
|};
function DisplayName({ displayName, id }: DisplayNameProps) {
const { searchIndex, searchResults, searchText } = useContext(TreeContext);
const { searchIndex, searchResults, searchText } = useContext(
TreeStateContext
);
const isSearchResult = useMemo(() => {
return searchResults.includes(id);
}, [id, searchResults]);
+10 -10
View File
@@ -11,7 +11,7 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Toggle from '../Toggle';
import { TreeContext } from './TreeContext';
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
import { StoreContext } from '../context';
import { useIsOverflowing } from '../hooks';
@@ -20,9 +20,8 @@ import type { Element } from './types';
import styles from './OwnersStack.css';
export default function OwnerStack() {
const { ownerStack, ownerStackIndex, resetOwnerStack } = useContext(
TreeContext
);
const { ownerStack, ownerStackIndex } = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
const elementsBarRef = useRef<HTMLDivElement | null>(null);
@@ -73,7 +72,7 @@ export default function OwnerStack() {
<div className={styles.VRule} />
<Button
className={styles.IconButton}
onClick={resetOwnerStack}
onClick={() => dispatch({ type: 'RESET_OWNER_STACK' })}
title="Back to tree view"
>
<ButtonIcon type="close" />
@@ -91,7 +90,7 @@ function ElementsDropdown({
ownerStackIndex,
}: ElementsDropdownProps) {
const store = useContext(StoreContext);
const { selectOwner } = useContext(TreeContext);
const dispatch = useContext(TreeDispatcherContext);
return (
<Menu>
@@ -107,7 +106,7 @@ function ElementsDropdown({
<MenuItem
key={id}
className={styles.Component}
onSelect={() => selectOwner(id)}
onSelect={() => dispatch({ type: 'SELECT_OWNER', payload: id })}
>
{((store.getElementByID(id): any): Element).displayName}
</MenuItem>
@@ -123,7 +122,8 @@ type ElementViewProps = {
};
function ElementView({ id, index }: ElementViewProps) {
const store = useContext(StoreContext);
const { ownerStackIndex, selectOwner } = useContext(TreeContext);
const { ownerStackIndex } = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const { displayName } = ((store.getElementByID(id): any): Element);
@@ -131,9 +131,9 @@ function ElementView({ id, index }: ElementViewProps) {
const handleChange = useCallback(() => {
if (!isChecked) {
selectOwner(id);
dispatch({ type: 'SELECT_OWNER', payload: id });
}
}, [id, isChecked, selectOwner]);
}, [dispatch, id, isChecked]);
return (
<Toggle
+19 -21
View File
@@ -1,7 +1,7 @@
// @flow
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import { TreeContext } from './TreeContext';
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Icon from '../Icon';
@@ -11,51 +11,49 @@ import styles from './SearchInput.css';
type Props = {||};
export default function SearchInput(props: Props) {
const {
goToNextSearchResult,
goToPreviousSearchResult,
searchIndex,
searchResults,
searchText,
selectNextElementInTree,
selectPreviousElementInTree,
setSearchText,
} = useContext(TreeContext);
const { searchIndex, searchResults, searchText } = useContext(
TreeStateContext
);
const dispatch = useContext(TreeDispatcherContext);
const inputRef = useRef<HTMLInputElement | null>(null);
const handleTextChange = useCallback(
({ currentTarget }) => setSearchText(currentTarget.value),
[setSearchText]
({ currentTarget }) =>
dispatch({ type: 'SET_SEARCH_TEXT', payload: currentTarget.value }),
[dispatch]
);
const resetSearch = useCallback(
() => dispatch({ type: 'SET_SEARCH_TEXT', payload: '' }),
[dispatch]
);
const resetSearch = useCallback(() => setSearchText(''), [setSearchText]);
const handleKeyDown = useCallback(
event => {
// For convenience, let up/down arrow keys change Tree selection.
switch (event.key) {
case 'ArrowDown':
selectNextElementInTree();
dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' });
event.preventDefault();
break;
case 'ArrowUp':
selectPreviousElementInTree();
dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' });
event.preventDefault();
break;
default:
break;
}
},
[selectNextElementInTree, selectPreviousElementInTree]
[dispatch]
);
const handleInputKeyPress = useCallback(
({ key }) => {
if (key === 'Enter') {
goToNextSearchResult();
dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' });
}
},
[goToNextSearchResult]
[dispatch]
);
// Auto-focus search input
@@ -106,7 +104,7 @@ export default function SearchInput(props: Props) {
<Button
className={styles.IconButton}
disabled={!searchText}
onClick={goToPreviousSearchResult}
onClick={() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' })}
title="Scroll to previous search result"
>
<ButtonIcon type="up" />
@@ -114,7 +112,7 @@ export default function SearchInput(props: Props) {
<Button
className={styles.IconButton}
disabled={!searchText}
onClick={goToNextSearchResult}
onClick={() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' })}
title="Scroll to next search result"
>
<ButtonIcon type="down" />
@@ -7,13 +7,14 @@ import React, {
useRef,
useState,
} from 'react';
import { TreeContext } from './TreeContext';
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
import { BridgeContext, StoreContext } from '../context';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import HooksTree from './HooksTree';
import InspectedElementTree from './InspectedElementTree';
import { hydrate } from 'src/hydration';
import ViewElementSourceContext from './ViewElementSourceContext';
import styles from './SelectedElement.css';
import {
ElementTypeClass,
@@ -29,7 +30,8 @@ import type { DehydratedData, Element } from './types';
export type Props = {||};
export default function SelectedElement(_: Props) {
const { selectedElementID, viewElementSource } = useContext(TreeContext);
const { selectedElementID } = useContext(TreeStateContext);
const viewElementSource = useContext(ViewElementSourceContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
@@ -155,7 +157,7 @@ function InspectedElementView({
state,
} = inspectedElement;
const { ownerStack } = useContext(TreeContext);
const { ownerStack } = useContext(TreeStateContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
@@ -243,12 +245,16 @@ function InspectedElementView({
}
function OwnerView({ displayName, id }: { displayName: string, id: number }) {
const { selectElementByID } = useContext(TreeContext);
const dispatch = useContext(TreeDispatcherContext);
const handleClick = useCallback(() => selectElementByID(id), [
id,
selectElementByID,
]);
const handleClick = useCallback(
() =>
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: id,
}),
[dispatch, id]
);
return (
<button
+16 -31
View File
@@ -11,7 +11,7 @@ import React, {
} from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import { TreeContext } from './TreeContext';
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
import { SettingsContext } from '../Settings/SettingsContext';
import { BridgeContext, StoreContext } from '../context';
import ElementView from './Element';
@@ -21,12 +21,9 @@ import SearchInput from './SearchInput';
import styles from './Tree.css';
import type { Element } from './types';
export type ItemData = {|
baseDepth: number,
numElements: number,
getElementAtIndex: (index: number) => Element | null,
isNavigatingWithKeyboard: boolean,
lastScrolledIDRef: { current: number | null },
onElementMouseEnter: (id: number) => void,
@@ -36,22 +33,16 @@ export type ItemData = {|
type Props = {||};
export default function Tree(props: Props) {
const dispatch = useContext(TreeDispatcherContext);
const {
baseDepth,
getElementAtIndex,
numElements,
ownerStack,
searchIndex,
searchResults,
selectedElementID,
selectedElementIndex,
selectChildElementInTree,
selectElementAtIndex,
selectNextElementInTree,
selectOwner,
selectParentElementInTree,
selectPreviousElementInTree,
} = useContext(TreeContext);
} = useContext(TreeStateContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const [isNavigatingWithKeyboard, setIsNavigatingWithKeyboard] = useState(
@@ -111,7 +102,7 @@ export default function Tree(props: Props) {
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
selectNextElementInTree();
dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' });
break;
case 'ArrowLeft':
event.preventDefault();
@@ -123,7 +114,7 @@ export default function Tree(props: Props) {
if (element.children.length > 0 && !element.isCollapsed) {
store.toggleIsCollapsed(element.id, true);
} else {
selectParentElementInTree();
dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' });
}
}
break;
@@ -137,13 +128,13 @@ export default function Tree(props: Props) {
if (element.children.length > 0 && element.isCollapsed) {
store.toggleIsCollapsed(element.id, false);
} else {
selectChildElementInTree();
dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' });
}
}
break;
case 'ArrowUp':
event.preventDefault();
selectPreviousElementInTree();
dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' });
break;
default:
return;
@@ -160,14 +151,7 @@ export default function Tree(props: Props) {
return () => {
ownerDocument.removeEventListener('keydown', handleKeyDown);
};
}, [
selectedElementID,
selectChildElementInTree,
selectNextElementInTree,
selectParentElementInTree,
selectPreviousElementInTree,
store,
]);
}, [dispatch, selectedElementID, store]);
// Focus management.
const handleBlur = useCallback(() => setTreeFocused(false), []);
@@ -175,9 +159,12 @@ export default function Tree(props: Props) {
setTreeFocused(true);
if (selectedElementIndex === null && numElements > 0) {
selectElementAtIndex(0);
dispatch({
type: 'SELECT_ELEMENT_AT_INDEX',
payload: 0,
});
}
}, [numElements, selectedElementIndex, selectElementAtIndex]);
}, [dispatch, numElements, selectedElementIndex]);
const handleKeyPress = useCallback(
event => {
@@ -185,14 +172,14 @@ export default function Tree(props: Props) {
case 'Enter':
case ' ':
if (selectedElementID !== null) {
selectOwner(selectedElementID);
dispatch({ type: 'SELECT_OWNER', payload: selectedElementID });
}
break;
default:
break;
}
},
[selectedElementID, selectOwner]
[dispatch, selectedElementID]
);
const highlightElementInDOM = useCallback(
@@ -270,7 +257,6 @@ export default function Tree(props: Props) {
() => ({
baseDepth,
numElements,
getElementAtIndex,
isNavigatingWithKeyboard,
onElementMouseEnter: handleElementMouseEnter,
lastScrolledIDRef,
@@ -279,7 +265,6 @@ export default function Tree(props: Props) {
[
baseDepth,
numElements,
getElementAtIndex,
isNavigatingWithKeyboard,
handleElementMouseEnter,
lastScrolledIDRef,
@@ -328,7 +313,7 @@ export default function Tree(props: Props) {
}
function InnerElementType({ style, ...rest }) {
const { ownerStack } = useContext(TreeContext);
const { ownerStack } = useContext(TreeStateContext);
// The list may need to scroll horizontally due to deeply nested elements.
// We don't know the maximum scroll width up front, because we're windowing.
+114 -187
View File
@@ -19,7 +19,6 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useLayoutEffect,
@@ -33,40 +32,91 @@ import Store from '../../store';
import type { Element } from './types';
type Context = {|
type StateContext = {|
// Tree
baseDepth: number,
numElements: number,
selectedElementID: number | null,
selectedElementIndex: number | null,
getElementAtIndex(index: number): Element | null,
selectChildElementInTree(): void,
selectElementAtIndex(index: number): void,
selectElementByID(id: number | null): void,
selectNextElementInTree(): void,
selectParentElementInTree(): void,
selectPreviousElementInTree(): void,
// Search
searchIndex: number | null,
searchResults: Array<number>,
searchText: string,
setSearchText(text: string): void,
goToNextSearchResult(): void,
goToPreviousSearchResult(): void,
// Owners
ownerFlatTree: Array<number> | null,
ownerStack: Array<number>,
ownerStackIndex: number | null,
resetOwnerStack(): void,
selectOwner(id: number): void,
// Injected by parent HTML/JavaScript
viewElementSource: Function | null,
|};
const TreeContext = createContext<Context>(((null: any): Context));
TreeContext.displayName = 'TreeContext';
type ACTION_GO_TO_NEXT_SEARCH_RESULT = {|
type: 'GO_TO_NEXT_SEARCH_RESULT',
|};
type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = {|
type: 'GO_TO_PREVIOUS_SEARCH_RESULT',
|};
type ACTION_HANDLE_STORE_MUTATION = {|
type: 'HANDLE_STORE_MUTATION',
payload: [Uint32Array, Uint32Array],
|};
type ACTION_RESET_OWNER_STACK = {|
type: 'RESET_OWNER_STACK',
|};
type ACTION_SELECT_CHILD_ELEMENT_IN_TREE = {|
type: 'SELECT_CHILD_ELEMENT_IN_TREE',
|};
type ACTION_SELECT_ELEMENT_AT_INDEX = {|
type: 'SELECT_ELEMENT_AT_INDEX',
payload: number | null,
|};
type ACTION_SELECT_ELEMENT_BY_ID = {|
type: 'SELECT_ELEMENT_BY_ID',
payload: number | null,
|};
type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {|
type: 'SELECT_NEXT_ELEMENT_IN_TREE',
|};
type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {|
type: 'SELECT_PARENT_ELEMENT_IN_TREE',
|};
type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {|
type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
|};
type ACTION_SELECT_OWNER = {|
type: 'SELECT_OWNER',
payload: number,
|};
type ACTION_SET_SEARCH_TEXT = {|
type: 'SET_SEARCH_TEXT',
payload: string,
|};
type Action =
| ACTION_GO_TO_NEXT_SEARCH_RESULT
| ACTION_GO_TO_PREVIOUS_SEARCH_RESULT
| ACTION_HANDLE_STORE_MUTATION
| ACTION_RESET_OWNER_STACK
| ACTION_SELECT_CHILD_ELEMENT_IN_TREE
| ACTION_SELECT_ELEMENT_AT_INDEX
| ACTION_SELECT_ELEMENT_BY_ID
| ACTION_SELECT_NEXT_ELEMENT_IN_TREE
| ACTION_SELECT_PARENT_ELEMENT_IN_TREE
| ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
| ACTION_SELECT_OWNER
| ACTION_SET_SEARCH_TEXT;
type DispatcherContext = (action: Action) => void;
const TreeStateContext = createContext<StateContext>(
((null: any): StateContext)
);
TreeStateContext.displayName = 'TreeStateContext';
const TreeDispatcherContext = createContext<DispatcherContext>(
((null: any): DispatcherContext)
);
TreeDispatcherContext.displayName = 'TreeDispatcherContext';
type State = {|
// Tree
@@ -83,29 +133,10 @@ type State = {|
// Owners
ownerStack: Array<number>,
ownerStackIndex: number | null,
_ownerFlatTree: Array<number> | null,
|};
type Action = {|
type:
| 'GO_TO_NEXT_SEARCH_RESULT'
| 'GO_TO_PREVIOUS_SEARCH_RESULT'
| 'HANDLE_STORE_MUTATION'
| 'RESET_OWNER_STACK'
| 'SELECT_CHILD_ELEMENT_IN_TREE'
| 'SELECT_ELEMENT_AT_INDEX'
| 'SELECT_ELEMENT_BY_ID'
| 'SELECT_NEXT_ELEMENT_IN_TREE'
| 'SELECT_PARENT_ELEMENT_IN_TREE'
| 'SELECT_PREVIOUS_ELEMENT_IN_TREE'
| 'SELECT_OWNER'
| 'SET_SEARCH_TEXT',
payload?: any,
ownerFlatTree: Array<number> | null,
|};
function reduceTreeState(store: Store, state: State, action: Action): State {
const { type, payload } = action;
let {
numElements,
ownerStack,
@@ -117,7 +148,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
// Base tree should ignore selected element changes when the owner's tree is active.
if (ownerStack.length === 0) {
switch (type) {
switch (action.type) {
case 'HANDLE_STORE_MUTATION':
numElements = store.numElements;
@@ -148,18 +179,18 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
break;
case 'SELECT_ELEMENT_AT_INDEX':
selectedElementIndex = ((payload: any): number | null);
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
break;
case 'SELECT_ELEMENT_BY_ID':
// Skip lookup in this case; it would be redundant.
// It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
lookupIDForIndex = false;
selectedElementID = payload;
selectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
selectedElementIndex =
payload === null
selectedElementID === null
? null
: store.getIndexOfElementID(((payload: any): number));
: store.getIndexOfElementID(selectedElementID);
break;
case 'SELECT_NEXT_ELEMENT_IN_TREE':
if (
@@ -220,8 +251,6 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
}
function reduceSearchState(store: Store, state: State, action: Action): State {
const { type, payload } = action;
let {
ownerStack,
searchIndex,
@@ -243,7 +272,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
// Search isn't supported when the owner's tree is active.
if (ownerStack.length === 0) {
switch (type) {
switch (action.type) {
case 'GO_TO_NEXT_SEARCH_RESULT':
if (numPrevSearchResults > 0) {
didRequestSearch = true;
@@ -265,7 +294,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
const [
addedElementIDs,
removedElementIDs,
] = ((payload: any): Array<Uint32Array>);
] = (action: ACTION_HANDLE_STORE_MUTATION).payload;
removedElementIDs.forEach(id => {
// Prune this item from the search results.
@@ -327,7 +356,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
case 'SET_SEARCH_TEXT':
searchIndex = null;
searchResults = [];
searchText = ((payload: any): string);
searchText = (action: ACTION_SET_SEARCH_TEXT).payload;
if (searchText !== '') {
const regExp = createRegExp(searchText);
@@ -385,24 +414,22 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
}
function reduceOwnersState(store: Store, state: State, action: Action): State {
const { payload, type } = action;
let {
baseDepth,
numElements,
selectedElementID,
selectedElementIndex,
ownerFlatTree,
ownerStack,
ownerStackIndex,
searchIndex,
searchResults,
searchText,
_ownerFlatTree,
} = state;
let prevSelectedElementIndex = selectedElementIndex;
switch (type) {
switch (action.type) {
case 'HANDLE_STORE_MUTATION':
if (ownerStack.length > 0) {
let indexOfRemovedItem = -1;
@@ -416,15 +443,15 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
if (indexOfRemovedItem >= 0) {
ownerStack = ownerStack.slice(0, indexOfRemovedItem);
if (ownerStack.length === 0) {
_ownerFlatTree = null;
ownerFlatTree = null;
ownerStackIndex = null;
} else {
ownerStackIndex = ownerStack.length - 1;
}
}
if (selectedElementID !== null && _ownerFlatTree !== null) {
if (selectedElementID !== null && ownerFlatTree !== null) {
// Mutation might have caused the index of this ID to shift.
selectedElementIndex = _ownerFlatTree.indexOf(selectedElementID);
selectedElementIndex = ownerFlatTree.indexOf(selectedElementID);
}
} else {
if (selectedElementID !== null) {
@@ -445,30 +472,31 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
selectedElementID !== null
? store.getIndexOfElementID(selectedElementID)
: null;
_ownerFlatTree = null;
ownerFlatTree = null;
break;
case 'SELECT_ELEMENT_AT_INDEX':
if (_ownerFlatTree !== null) {
selectedElementIndex = ((payload: any): number | null);
if (ownerFlatTree !== null) {
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
}
break;
case 'SELECT_ELEMENT_BY_ID':
if (_ownerFlatTree !== null) {
if (ownerFlatTree !== null) {
const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
selectedElementIndex =
payload === null ? null : _ownerFlatTree.indexOf(payload);
payload === null ? null : ownerFlatTree.indexOf(payload);
}
break;
case 'SELECT_NEXT_ELEMENT_IN_TREE':
if (_ownerFlatTree !== null && _ownerFlatTree.length > 0) {
if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
if (selectedElementIndex === null) {
selectedElementIndex = 0;
} else if (selectedElementIndex + 1 < _ownerFlatTree.length) {
} else if (selectedElementIndex + 1 < ownerFlatTree.length) {
selectedElementIndex++;
}
}
break;
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
if (_ownerFlatTree !== null && _ownerFlatTree.length > 0) {
if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
if (selectedElementIndex !== null && selectedElementIndex > 0) {
selectedElementIndex--;
}
@@ -478,7 +506,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
// If the Store doesn't have any owners metadata, don't drill into an empty stack.
// This is a confusing user experience.
if (store.hasOwnerMetadata) {
ownerStackIndex = ownerStack.indexOf(payload);
const id = (action: ACTION_SELECT_OWNER).payload;
ownerStackIndex = ownerStack.indexOf(id);
// Always force reset selection to be the top of the new owner tree.
selectedElementIndex = 0;
@@ -489,7 +518,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
if (ownerStackIndex < 0) {
// Add this new owner, and fill in the owners above it as well.
ownerStack = [];
let currentOwnerID = ((payload: any): number);
let currentOwnerID = id;
while (currentOwnerID !== 0) {
ownerStack.unshift(currentOwnerID);
currentOwnerID = ((store.getElementByID(
@@ -515,23 +544,23 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
if (
ownerStackIndex !== state.ownerStackIndex ||
ownerStack !== state.ownerStack ||
type === 'HANDLE_STORE_MUTATION'
action.type === 'HANDLE_STORE_MUTATION'
) {
if (ownerStackIndex === null) {
_ownerFlatTree = null;
ownerFlatTree = null;
baseDepth = 0;
numElements = store.numElements;
} else {
_ownerFlatTree = calculateCurrentOwnerList(
ownerFlatTree = calculateCurrentOwnerList(
store,
ownerStack[ownerStackIndex],
ownerStack[ownerStackIndex],
[]
);
baseDepth = ((store.getElementByID(_ownerFlatTree[0]): any): Element)
baseDepth = ((store.getElementByID(ownerFlatTree[0]): any): Element)
.depth;
numElements = _ownerFlatTree.length;
numElements = ownerFlatTree.length;
}
}
@@ -539,8 +568,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
if (selectedElementIndex !== prevSelectedElementIndex) {
if (selectedElementIndex === null) {
selectedElementID = null;
} else if (_ownerFlatTree !== null) {
selectedElementID = _ownerFlatTree[((selectedElementIndex: any): number)];
} else if (ownerFlatTree !== null) {
selectedElementID = ownerFlatTree[((selectedElementIndex: any): number)];
}
}
@@ -558,17 +587,14 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
ownerStack,
ownerStackIndex,
_ownerFlatTree,
ownerFlatTree,
};
}
type Props = {|
children: React$Node,
viewElementSource: Function | null,
|};
type Props = {| children: React$Node |};
// TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
function TreeContextController({ children, viewElementSource }: Props) {
function TreeContextController({ children }: Props) {
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
@@ -633,114 +659,9 @@ function TreeContextController({ children, viewElementSource }: Props) {
// Owners
ownerStack: [],
ownerStackIndex: null,
_ownerFlatTree: null,
ownerFlatTree: null,
});
const getElementAtIndex = useCallback(
(index: number) => {
return state._ownerFlatTree === null
? store.getElementAtIndex(index)
: store.getElementByID(state._ownerFlatTree[index]);
},
[state, store]
);
const selectElementAtIndex = useCallback(
(index: number) =>
dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: index }),
[dispatch]
);
const selectElementByID = useCallback(
(id: number | null) =>
dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: id }),
[dispatch]
);
const setSearchText = useCallback(
(text: string) => dispatch({ type: 'SET_SEARCH_TEXT', payload: text }),
[dispatch]
);
const goToNextSearchResult = useCallback(
() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }),
[dispatch]
);
const goToPreviousSearchResult = useCallback(
() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }),
[dispatch]
);
const resetOwnerStack = useCallback(
() => dispatch({ type: 'RESET_OWNER_STACK' }),
[dispatch]
);
const selectChildElementInTree = useCallback(
() => dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }),
[dispatch]
);
const selectNextElementInTree = useCallback(
() => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }),
[dispatch]
);
const selectParentElementInTree = useCallback(
() => dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }),
[dispatch]
);
const selectPreviousElementInTree = useCallback(
() => dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }),
[dispatch]
);
const selectOwner = useCallback(
(id: number) => dispatch({ type: 'SELECT_OWNER', payload: id }),
[dispatch]
);
const value = useMemo(
() => ({
// Tree (derived from Store or owners state)
baseDepth: state.baseDepth,
numElements: state.numElements,
selectedElementID: state.selectedElementID,
selectedElementIndex: state.selectedElementIndex,
getElementAtIndex,
selectChildElementInTree,
selectElementByID,
selectElementAtIndex,
selectNextElementInTree,
selectParentElementInTree,
selectPreviousElementInTree,
// Search
searchIndex: state.searchIndex,
searchResults: state.searchResults,
searchText: state.searchText,
setSearchText,
goToNextSearchResult,
goToPreviousSearchResult,
// Owners
ownerStack: state.ownerStack,
ownerStackIndex: state.ownerStackIndex,
resetOwnerStack,
selectOwner,
// Injected by parent HTML/JavaScript
viewElementSource,
}),
[
getElementAtIndex,
goToNextSearchResult,
goToPreviousSearchResult,
resetOwnerStack,
selectChildElementInTree,
selectElementAtIndex,
selectElementByID,
selectNextElementInTree,
selectParentElementInTree,
selectOwner,
selectPreviousElementInTree,
setSearchText,
state,
viewElementSource,
]
);
// Listen for host element selections.
useEffect(() => {
const handleSelectFiber = (id: number) =>
@@ -793,7 +714,13 @@ function TreeContextController({ children, viewElementSource }: Props) {
return () => store.removeListener('mutated', handleStoreMutated);
}, [dispatch, initialRevision, store]);
return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
return (
<TreeStateContext.Provider value={state}>
<TreeDispatcherContext.Provider value={dispatch}>
{children}
</TreeDispatcherContext.Provider>
</TreeStateContext.Provider>
);
}
function calculateCurrentOwnerList(
@@ -842,4 +769,4 @@ function recursivelySearchTree(
);
}
export { TreeContext, TreeContextController };
export { TreeDispatcherContext, TreeStateContext, TreeContextController };
@@ -0,0 +1,8 @@
// @flow
import { createContext } from 'react';
const ViewElementSourceContext = createContext<Function | null>(null);
ViewElementSourceContext.displayName = 'ViewElementSourceContext';
export default ViewElementSourceContext;
+47 -38
View File
@@ -14,6 +14,7 @@ import Settings from './Settings/Settings';
import TabBar from './TabBar';
import { SettingsContextController } from './Settings/SettingsContext';
import { TreeContextController } from './Components/TreeContext';
import ViewElementSourceContext from './Components/ViewElementSourceContext';
import { ProfilerContextController } from './Profiler/ProfilerContext';
import ReactLogo from './ReactLogo';
@@ -121,47 +122,55 @@ export default function DevTools({
profilerPortalContainer={profilerPortalContainer}
settingsPortalContainer={settingsPortalContainer}
>
<TreeContextController viewElementSource={viewElementSource}>
<ProfilerContextController>
<div className={styles.DevTools}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={setTab}
size="large"
tabs={
supportsProfiling
? tabsWithProfiler
: tabsWithoutProfiler
}
<ViewElementSourceContext.Provider value={viewElementSource}>
<TreeContextController>
<ProfilerContextController>
<div className={styles.DevTools}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={setTab}
size="large"
tabs={
supportsProfiling
? tabsWithProfiler
: tabsWithoutProfiler
}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}
>
<Components portalContainer={componentsPortalContainer} />
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}
>
<Profiler
portalContainer={profilerPortalContainer}
supportsProfiling={supportsProfiling}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}
>
<Components portalContainer={componentsPortalContainer} />
<div
className={styles.TabContent}
hidden={tab !== 'settings'}
>
<Settings portalContainer={settingsPortalContainer} />
</div>
</div>
<div className={styles.TabContent} hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
supportsProfiling={supportsProfiling}
/>
</div>
<div className={styles.TabContent} hidden={tab !== 'settings'}>
<Settings portalContainer={settingsPortalContainer} />
</div>
</div>
</ProfilerContextController>
</TreeContextController>
</ProfilerContextController>
</TreeContextController>
</ViewElementSourceContext.Provider>
</SettingsContextController>
</StoreContext.Provider>
</BridgeContext.Provider>
+11 -4
View File
@@ -9,7 +9,10 @@ import React, {
} from 'react';
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
import { useLocalStorage, useSubscription } from '../hooks';
import { TreeContext } from '../Components/TreeContext';
import {
TreeDispatcherContext,
TreeStateContext,
} from '../Components/TreeContext';
import { StoreContext } from '../context';
import Store from '../../store';
@@ -79,7 +82,8 @@ type Props = {|
function ProfilerContextController({ children }: Props) {
const store = useContext(StoreContext);
const { selectElementByID, selectedElementID } = useContext(TreeContext);
const { selectedElementID } = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const subscription = useMemo(
() => ({
@@ -155,11 +159,14 @@ function ProfilerContextController({ children }: Props) {
// If this element is still in the store, then select it in the Components tab as well.
const element = store.getElementByID(id);
if (element !== null) {
selectElementByID(id);
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: id,
});
}
}
},
[selectElementByID, selectFiberID, selectFiberName, store]
[dispatch, selectFiberID, selectFiberName, store]
);
if (isProfiling) {