Files
react/src/hydration.js
T

281 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @flow
import {
isElement,
typeOf,
AsyncMode,
ConcurrentMode,
ContextConsumer,
ContextProvider,
ForwardRef,
Fragment,
Lazy,
Memo,
Portal,
Profiler,
StrictMode,
Suspense,
} from 'react-is';
import { getDisplayName } from './utils';
export const meta = {
name: Symbol('name'),
type: Symbol('type'),
inspected: Symbol('inspected'),
meta: Symbol('meta'),
proto: Symbol('proto'),
};
// This threshold determines the depth at which the bridge "dehydrates" nested data.
// Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
// unless the frontend explicitly requests it (e.g. a user clicks to expand a props object).
// We tried reducing this value from 2 to 1 to improve performance:
// https://github.com/facebook/react-devtools/issues/1200
// But this caused problems with the Profiler's interaction tracing output.
// Because React mutates Fibers, profiling data that is dehydrated for old commits
// will not be available later from within the Profiler.
// This impacts props/state as well as Interactions.
// https://github.com/facebook/react-devtools/issues/1262
const LEVEL_THRESHOLD = 6;
/**
* Get a enhanced/artificial type string based on the object instance
*/
function getPropType(data: Object): string | null {
if (!data) {
return null;
}
if (isElement(data)) {
return 'react_element';
}
const type = typeof data;
if (type === 'object') {
if (Array.isArray(data)) {
return 'array';
}
if (ArrayBuffer.isView(data)) {
if (data instanceof DataView) {
return 'data_view';
}
return 'typed_array';
}
if (data instanceof ArrayBuffer) {
return 'array_buffer';
}
if (typeof data[Symbol.iterator] === 'function') {
return 'iterator';
}
if (Object.prototype.toString.call(data) === '[object Date]') {
return 'date';
}
}
return type;
}
/**
* Generate the dehydrated metadata for complex object instances
*/
function createDehydrated(
type: string,
data: Object,
cleaned: Array<Array<string>>,
path: Array<string>
): Object {
const meta = {};
if (type === 'array' || type === 'typed_array') {
meta.length = data.length;
}
if (type === 'iterator' || type === 'typed_array') {
meta.readOnly = true;
}
cleaned.push(path);
return {
type,
meta,
name:
!data.constructor || data.constructor.name === 'Object'
? ''
: data.constructor.name,
};
}
/**
* Strip out complex data (instances, functions, and data nested > LEVEL_THRESHOLD levels deep).
* The paths of the stripped out objects are appended to the `cleaned` list.
* On the other side of the barrier, the cleaned list is used to "re-hydrate" the cleaned representation into
* an object with symbols as attributes, so that a sanitized object can be distinguished from a normal object.
*
* Input: {"some": {"attr": fn()}, "other": AnInstance}
* Output: {
* "some": {
* "attr": {"name": the fn.name, type: "function"}
* },
* "other": {
* "name": "AnInstance",
* "type": "object",
* },
* }
* and cleaned = [["some", "attr"], ["other"]]
*/
export function dehydrate(
data: Object,
cleaned: Array<Array<string>>,
path?: Array<string> = [],
level?: number = 0
): string | Object {
const type = getPropType(data);
switch (type) {
case 'function':
cleaned.push(path);
return {
name: data.name,
type: 'function',
};
case 'string':
return data.length <= 500 ? data : data.slice(0, 500) + '...';
// We have to do this assignment b/c Flow doesn't think "symbol" is
// something typeof would return. Error 'unexpected predicate "symbol"'
case 'symbol':
cleaned.push(path);
return {
type: 'symbol',
name: data.toString(),
};
// React Elements aren't very inspector-friendly,
// and often contain private fields or circular references.
case 'react_element':
cleaned.push(path);
return {
name: getDisplayNameForReactElement(data),
type: 'react_element',
};
// ArrayBuffers error if you try to inspect them.
case 'array_buffer':
case 'data_view':
cleaned.push(path);
return {
type,
name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
meta: {
length: data.byteLength,
uninspectable: true,
},
};
case 'array':
if (level > LEVEL_THRESHOLD) {
return createDehydrated(type, data, cleaned, path);
}
return data.map((item, i) =>
dehydrate(item, cleaned, path.concat([i]), level + 1)
);
case 'typed_array':
case 'iterator':
return createDehydrated(type, data, cleaned, path);
case 'date':
cleaned.push(path);
return {
name: data.toString(),
type: 'date',
meta: {
uninspectable: true,
},
};
case 'object':
if (
level > LEVEL_THRESHOLD ||
(data.constructor &&
typeof data.constructor === 'function' &&
data.constructor.name !== 'Object')
) {
return createDehydrated(type, data, cleaned, path);
} else {
const res = {};
for (let name in data) {
res[name] = dehydrate(
data[name],
cleaned,
path.concat([name]),
level + 1
);
}
return res;
}
default:
return data;
}
}
export function hydrate(data: Object, cleaned: Array<Array<string>>): Object {
cleaned.forEach((path: Array<string>) => {
const last = path.pop();
const reduced: Object = path.reduce(
(object: Object, attr: string) => (object ? object[attr] : (null: any)),
data
);
if (!reduced || !reduced[last]) {
return;
}
const replace: { [key: Symbol]: boolean | string } = {};
replace[meta.name] = reduced[last].name;
replace[meta.type] = reduced[last].type;
replace[meta.meta] = reduced[last].meta;
replace[meta.inspected] = false;
reduced[last] = replace;
});
return data;
}
export function getDisplayNameForReactElement(
element: React$Element<any>
): string | null {
const elementType = typeOf(element);
switch (elementType) {
case AsyncMode:
case ConcurrentMode:
return 'ConcurrentMode';
case ContextConsumer:
return 'ContextConsumer';
case ContextProvider:
return 'ContextProvider';
case ForwardRef:
return 'ForwardRef';
case Fragment:
return 'Fragment';
case Lazy:
return 'Lazy';
case Memo:
return 'Memo';
case Portal:
return 'Portal';
case Profiler:
return 'Profiler';
case StrictMode:
return 'StrictMode';
case Suspense:
return 'Suspense';
default:
const { type } = element;
if (typeof type === 'string') {
return type;
} else if (type != null) {
return getDisplayName(type, 'Unknown');
} else {
return 'Element';
}
}
}