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

This commit is contained in:
Brian Vaughn
2019-04-22 10:19:59 -07:00
parent 70be637d48
commit d53ae2ea8a
10 changed files with 268 additions and 321 deletions

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]);

View File

@@ -10,7 +10,7 @@ import React, {
import { createResource } from '../../cache';
import { BridgeContext, StoreContext } from '../context';
import { hydrate } from 'src/hydration';
import { TreeContext } from './TreeContext';
import { TreeStateContext } from './TreeContext';
import type {
DehydratedData,
@@ -38,7 +38,7 @@ type Props = {|
function InspectedElementContextController({ children }: Props) {
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const { inspectedElementID } = useContext(TreeContext);
const { inspectedElementID } = useContext(TreeStateContext);
const [count, setCount] = useState<number>(0);

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

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" />

View File

@@ -1,13 +1,14 @@
// @flow
import React, { useCallback, useContext } 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 { InspectedElementContext } from './InspectedElementContext';
import ViewElementSourceContext from './ViewElementSourceContext';
import styles from './SelectedElement.css';
import {
ElementTypeClass,
@@ -22,7 +23,8 @@ import type { Element, InspectedElement } from './types';
export type Props = {||};
export default function SelectedElement(_: Props) {
const { inspectedElementID, viewElementSource } = useContext(TreeContext);
const { inspectedElementID } = useContext(TreeStateContext);
const viewElementSource = useContext(ViewElementSourceContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
@@ -153,7 +155,7 @@ function InspectedElementView({
state,
} = inspectedElement;
const { ownerStack } = useContext(TreeContext);
const { ownerStack } = useContext(TreeStateContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
@@ -241,12 +243,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

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.

View File

@@ -34,44 +34,98 @@ 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,
// Inspection element panel
// Updated separately so we can avoid suspending when selection changes
inspectedElementID: number | 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_UPDATE_INSPECTED_ELEMENT_ID = {|
type: 'UPDATE_INSPECTED_ELEMENT_ID',
|};
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
| ACTION_UPDATE_INSPECTED_ELEMENT_ID;
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
@@ -88,33 +142,13 @@ type State = {|
// Owners
ownerStack: Array<number>,
ownerStackIndex: number | null,
_ownerFlatTree: Array<number> | null,
ownerFlatTree: Array<number> | null,
// Inspection element panel
inspectedElementID: 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'
| 'UPDATE_INSPECTED_ELEMENT_ID',
payload?: any,
|};
function reduceTreeState(store: Store, state: State, action: Action): State {
const { type, payload } = action;
let {
numElements,
ownerStack,
@@ -126,7 +160,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;
@@ -157,18 +191,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 (
@@ -229,8 +263,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,
@@ -252,7 +284,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;
@@ -274,7 +306,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.
@@ -336,7 +368,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);
@@ -394,24 +426,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;
@@ -425,15 +455,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) {
@@ -454,30 +484,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--;
}
@@ -487,7 +518,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;
@@ -498,7 +530,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(
@@ -524,23 +556,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;
}
}
@@ -548,8 +580,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)];
}
}
@@ -567,7 +599,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
ownerStack,
ownerStackIndex,
_ownerFlatTree,
ownerFlatTree,
};
}
@@ -589,13 +621,10 @@ function reduceSuspenseState(
}
}
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);
@@ -662,129 +691,20 @@ function TreeContextController({ children, viewElementSource }: Props) {
// Owners
ownerStack: [],
ownerStackIndex: null,
_ownerFlatTree: null,
ownerFlatTree: null,
// Inspection element panel
inspectedElementID: null,
});
const dispatchWrapper = useCallback(
params => {
dispatch(params);
(action: Action) => {
dispatch(action);
next(() => dispatch({ type: 'UPDATE_INSPECTED_ELEMENT_ID' }));
},
[dispatch]
);
const getElementAtIndex = useCallback(
(index: number) => {
return state._ownerFlatTree === null
? store.getElementAtIndex(index)
: store.getElementByID(state._ownerFlatTree[index]);
},
[state, store]
);
const selectElementAtIndex = useCallback(
(index: number) =>
dispatchWrapper({ type: 'SELECT_ELEMENT_AT_INDEX', payload: index }),
[dispatchWrapper]
);
const selectElementByID = useCallback(
(id: number | null) =>
dispatchWrapper({ type: 'SELECT_ELEMENT_BY_ID', payload: id }),
[dispatchWrapper]
);
const setSearchText = useCallback(
(text: string) =>
dispatchWrapper({ type: 'SET_SEARCH_TEXT', payload: text }),
[dispatchWrapper]
);
const goToNextSearchResult = useCallback(
() => dispatchWrapper({ type: 'GO_TO_NEXT_SEARCH_RESULT' }),
[dispatchWrapper]
);
const goToPreviousSearchResult = useCallback(
() => dispatchWrapper({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }),
[dispatchWrapper]
);
const resetOwnerStack = useCallback(
() => dispatchWrapper({ type: 'RESET_OWNER_STACK' }),
[dispatchWrapper]
);
const selectChildElementInTree = useCallback(
() => dispatchWrapper({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }),
[dispatchWrapper]
);
const selectNextElementInTree = useCallback(
() => dispatchWrapper({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }),
[dispatchWrapper]
);
const selectParentElementInTree = useCallback(
() => dispatchWrapper({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }),
[dispatchWrapper]
);
const selectPreviousElementInTree = useCallback(
() => dispatchWrapper({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }),
[dispatchWrapper]
);
const selectOwner = useCallback(
(id: number) => dispatchWrapper({ type: 'SELECT_OWNER', payload: id }),
[dispatchWrapper]
);
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,
// Inspection element panel
inspectedElementID: state.inspectedElementID,
// 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) =>
@@ -837,7 +757,13 @@ function TreeContextController({ children, viewElementSource }: Props) {
return () => store.removeListener('mutated', handleStoreMutated);
}, [dispatchWrapper, initialRevision, store]);
return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
return (
<TreeStateContext.Provider value={state}>
<TreeDispatcherContext.Provider value={dispatchWrapper}>
{children}
</TreeDispatcherContext.Provider>
</TreeStateContext.Provider>
);
}
function calculateCurrentOwnerList(
@@ -886,4 +812,4 @@ function recursivelySearchTree(
);
}
export { TreeContext, TreeContextController };
export { TreeDispatcherContext, TreeStateContext, TreeContextController };

View File

@@ -0,0 +1,8 @@
// @flow
import { createContext } from 'react';
const ViewElementSourceContext = createContext<Function | null>(null);
ViewElementSourceContext.displayName = 'ViewElementSourceContext';
export default ViewElementSourceContext;

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>

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