Cache inspected element data until it is updated

This commit is contained in:
Brian Vaughn
2019-05-24 07:37:46 -07:00
parent 408fbee23b
commit 07db37f030
3 changed files with 187 additions and 9 deletions
@@ -51,6 +51,58 @@ exports[`InspectedElementContext should inspect the currently selected element:
}
`;
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: mount 1`] = `
[root]
▾ <Wrapper>
<Anonymous>
`;
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: initial render 1`] = `
{
"id": 3,
"owners": null,
"context": null,
"events": null,
"hooks": [
{
"id": 0,
"isStateEditable": true,
"name": "State",
"value": 0,
"subHooks": []
}
],
"props": {
"foo": 1,
"bar": "abc"
},
"state": null
}
`;
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 3: updated state 1`] = `
{
"id": 3,
"owners": null,
"context": null,
"events": null,
"hooks": [
{
"id": 0,
"isStateEditable": true,
"name": "State",
"value": 0,
"subHooks": []
}
],
"props": {
"foo": 2,
"bar": "def"
},
"state": null
}
`;
exports[`InspectedElementContext should poll for updates for the currently selected element: 1: mount 1`] = `
[root]
<Example>
@@ -159,4 +159,94 @@ describe('InspectedElementContext', () => {
done();
});
it('should not re-render a function with hooks if it did not update since it was last inspected', async done => {
let targetRenderCount = 0;
const Wrapper = ({ children }) => children;
const Target = React.memo(props => {
targetRenderCount++;
React.useState(0);
return null;
});
const container = document.createElement('div');
utils.act(() =>
ReactDOM.render(
<Wrapper>
<Target foo={1} bar="abc" />
</Wrapper>,
container
)
);
expect(store).toMatchSnapshot('1: mount');
const id = ((store.getElementIDAtIndex(1): any): number);
let inspectedElement = null;
function Suspender({ target }) {
const { read } = React.useContext(InspectedElementContext);
inspectedElement = read(target);
return null;
}
targetRenderCount = 0;
let renderer;
await utils.actAsync(
() =>
(renderer = TestRenderer.create(
<Contexts
defaultSelectedElementID={id}
defaultSelectedElementIndex={1}
>
<React.Suspense fallback={null}>
<Suspender target={id} />
</React.Suspense>
</Contexts>
)),
3
);
expect(targetRenderCount).toBe(1);
expect(inspectedElement).toMatchSnapshot('2: initial render');
const initialInspectedElement = inspectedElement;
targetRenderCount = 0;
inspectedElement = null;
await utils.actAsync(
() =>
renderer.update(
<Contexts
defaultSelectedElementID={id}
defaultSelectedElementIndex={1}
>
<React.Suspense fallback={null}>
<Suspender target={id} />
</React.Suspense>
</Contexts>
),
1
);
expect(targetRenderCount).toBe(0);
expect(inspectedElement).toEqual(initialInspectedElement);
targetRenderCount = 0;
await utils.actAsync(() =>
ReactDOM.render(
<Wrapper>
<Target foo={2} bar="def" />
</Wrapper>,
container
)
);
// Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
expect(targetRenderCount).toBe(2);
expect(inspectedElement).toMatchSnapshot('3: updated state');
done();
});
});
+45 -9
View File
@@ -1102,6 +1102,19 @@ export function attach(
if (__DEBUG__) {
debug('updateFiberRecursively()', nextFiber, parentFiber);
}
if (
mostRecentlyInspectedElement !== null &&
mostRecentlyInspectedElementID ===
getFiberID(getPrimaryFiber(nextFiber)) &&
hasDataChanged(prevFiber, nextFiber)
) {
// If this Fiber has updated, clear cached inspected data.
// If it is inspected again, it may need to be re-run to obtain updated hooks values.
mostRecentlyInspectedElement = null;
mostRecentlyInspectedElementID = null;
}
const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
const isSuspense = nextFiber.tag === SuspenseComponent;
let shouldResetChildren = false;
@@ -1890,18 +1903,41 @@ export function attach(
};
}
let mostRecentlyInspectedElementID: number | null = null;
let mostRecentlyInspectedElement: InspectedElement | null = null;
function inspectElement(id: number): InspectedElement | null {
let result = inspectElementRaw(id);
if (result === null) {
// If this element has not been updated since it was last inspected, reuse the last value.
// This avoids re-invoking a function component with hooks.
// TODO We could send a special signal (e.g. true) to avoid serialization too.
if (
mostRecentlyInspectedElement !== null &&
mostRecentlyInspectedElementID === id
) {
return mostRecentlyInspectedElement;
}
mostRecentlyInspectedElementID = id;
mostRecentlyInspectedElement = inspectElementRaw(id);
if (mostRecentlyInspectedElement === null) {
return null;
}
// TODO Review sanitization approach for the below inspectable values.
result.context = cleanForBridge(result.context);
result.events = cleanForBridge(result.events);
result.hooks = cleanForBridge(result.hooks);
result.props = cleanForBridge(result.props);
result.state = cleanForBridge(result.state);
return result;
mostRecentlyInspectedElement.context = cleanForBridge(
mostRecentlyInspectedElement.context
);
mostRecentlyInspectedElement.events = cleanForBridge(
mostRecentlyInspectedElement.events
);
mostRecentlyInspectedElement.hooks = cleanForBridge(
mostRecentlyInspectedElement.hooks
);
mostRecentlyInspectedElement.props = cleanForBridge(
mostRecentlyInspectedElement.props
);
mostRecentlyInspectedElement.state = cleanForBridge(
mostRecentlyInspectedElement.state
);
return mostRecentlyInspectedElement;
}
function logElementToConsole(id) {