Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

MVP hooks support #1272

Merged
merged 11 commits into from
Jan 14, 2019
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins:
globals:
chrome: false
React$Element: false
React$Node: false

rules:
brace-style: [2, 1tbs]
Expand Down
73 changes: 70 additions & 3 deletions agent/Agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var assign = require('object-assign');
var nullthrows = require('nullthrows').default;
var guid = require('../utils/guid');
var getIn = require('./getIn');
var {inspectHooksOfFiber} = require('../backend/ReactDebugHooks');

import type {RendererID, DataType, OpaqueNodeHandle, NativeType, Helpers} from '../backend/types';

Expand Down Expand Up @@ -87,6 +88,7 @@ class Agent extends EventEmitter {
global: Object;
internalInstancesById: Map<ElementID, OpaqueNodeHandle>;
idsByInternalInstances: WeakMap<OpaqueNodeHandle, ElementID>;
rendererIdsByInternalInstance: WeakMap<OpaqueNodeHandle, RendererID>;
renderers: Map<ElementID, RendererID>;
elementData: Map<ElementID, DataType>;
roots: Set<ElementID>;
Expand All @@ -96,22 +98,34 @@ class Agent extends EventEmitter {
capabilities: {[key: string]: boolean};
_updateScroll: () => void;
_inspectEnabled: boolean;
_prevInspectedHooks: any = null;

constructor(global: Object, capabilities?: Object) {
super();
this.global = global;
this.internalInstancesById = new Map();
this.idsByInternalInstances = new WeakMap();
this.rendererIdsByInternalInstance = new WeakMap();
this.renderers = new Map();
this.elementData = new Map();
this.roots = new Set();
this.reactInternals = {};
var lastSelected;
this.on('selected', id => {
var data = this.elementData.get(id);
if (data && data.publicInstance && this.global.$r === lastSelected) {
this.global.$r = data.publicInstance;
lastSelected = data.publicInstance;
var inspectedHooks = null;
if (data) {
if (data.publicInstance && this.global.$r === lastSelected) {
this.global.$r = data.publicInstance;
lastSelected = data.publicInstance;
}
if (data.containsHooks) {
inspectedHooks = this.updateHooksTree(id);
}
}
if (this._prevInspectedHooks !== inspectedHooks) {
this._prevInspectedHooks = inspectedHooks;
this.emit('inspectedHooks', inspectedHooks);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this (storing data, having Fiber-specific code) in the Agent sucks. I'm open to suggestions for a better place for this logic to live, but at the same time– I didn't stress about it too much because I think this version of DevTools is short lived (without either a rewrite or a refactor).

}
});
this._prevSelected = null;
Expand Down Expand Up @@ -223,6 +237,7 @@ class Agent extends EventEmitter {
this.on('isRecording', isRecording => bridge.send('isRecording', isRecording));
this.on('storeSnapshot', (data) => bridge.send('storeSnapshot', data));
this.on('clearSnapshots', () => bridge.send('clearSnapshots'));
this.on('inspectedHooks', data => bridge.send('inspectedHooks', data));
}

scrollToNode(id: ElementID): void {
Expand Down Expand Up @@ -390,6 +405,15 @@ class Agent extends EventEmitter {
this.renderers.set(id, renderer);
this.elementData.set(id, data);

// We need to map Fiber instance IDs to their renderers to support Hooks inspection.
// This is because the Store does not know how to connect the two,
// but the Agent needs to use the parent renderer's injected internals to get the current dispatcher.
// We only need to store this for components that use hooks (to reduce memory impact).
// We can only store on-mount (to reduce write operations) since hooks cannot be conditional.
if (data.containsHooks) {
this.rendererIdsByInternalInstance.set(component, renderer);
}

var send = assign({}, data);
if (send.children && send.children.map) {
send.children = send.children.map(c => this.getId(c));
Expand All @@ -414,6 +438,18 @@ class Agent extends EventEmitter {
delete send.type;
delete send.updater;
this.emit('update', send);

// If the element that was just updated is also being inspected, update the hooks values.
if (
this._prevInspectedHooks !== null &&
this._prevInspectedHooks.elementID === id
) {
const inspectedHooks = this.updateHooksTree(id);
if (this._prevInspectedHooks !== inspectedHooks) {
this._prevInspectedHooks = inspectedHooks;
this.emit('inspectedHooks', inspectedHooks);
}
}
}

onUpdatedProfileTimes(component: OpaqueNodeHandle, data: DataType) {
Expand All @@ -431,6 +467,37 @@ class Agent extends EventEmitter {
this.emit('updateProfileTimes', send);
}

updateHooksTree(id: ElementID) {
const data = this.elementData.get(id);
const internalInstance = this.internalInstancesById.get(id);
if (internalInstance) {
const rendererID = this.rendererIdsByInternalInstance.get(internalInstance);
if (rendererID) {
const internals = this.reactInternals[rendererID].renderer;
if (internals && internals.currentDispatcherRef) {
// HACK: This leaks Fiber-specific logic into the Agent which is not ideal.
// $FlowFixMe
const currentFiber = data.state === internalInstance.memoizedState ? internalInstance : internalInstance.alternate;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gross for obvious reasons, but it needs to live somewhere. Happy to move it out of the Agent if anyone thinks it's too gross even for a temporary solution.


const hooksTree = inspectHooksOfFiber(currentFiber, internals.currentDispatcherRef);

// It's also important to store the element ID,
// so the frontend can avoid potentially showing the wrong hooks data for an element,
// (since hooks inspection is done as part of a separate Bridge message).
// But we can't store it as "id"– because the Bridge stores a map of "inspectable" data keyed by this field.
// Use an id that won't conflict with the element itself (because we don't want to override data).
// This is important if components have both inspectable props and inspectable hooks.
return {
elementID: id,
id: 'hooksTree',
hooksTree,
};
}
}
}
return null;
}

onUnmounted(component: OpaqueNodeHandle) {
var id = this.getId(component);
this.elementData.delete(id);
Expand Down
Loading