Replace ReactShallowRenderer with a dependency (#18144)

Closes #17321.
This commit is contained in:
Minh Nguyen
2020-02-27 18:10:25 +00:00
committed by GitHub
parent abcca45951
commit 293878e079
4 changed files with 11 additions and 864 deletions

View File

@@ -22,6 +22,7 @@
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"react-is": "^16.8.6",
"react-shallow-renderer": "^16.12.0",
"scheduler": "^0.19.0"
},
"peerDependencies": {

View File

@@ -7,4 +7,4 @@
* @flow
*/
export {default} from './src/ReactShallowRenderer';
export {default} from 'react-shallow-renderer';

View File

@@ -1,863 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {isForwardRef, isMemo, ForwardRef} from 'react-is';
import describeComponentFrame from 'shared/describeComponentFrame';
import getComponentName from 'shared/getComponentName';
import shallowEqual from 'shared/shallowEqual';
import invariant from 'shared/invariant';
import checkPropTypes from 'prop-types/checkPropTypes';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberHooks';
import type {
ReactContext,
ReactEventResponderListener,
} from 'shared/ReactTypes';
import type {ReactElement} from 'shared/ReactElementType';
type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
type Update<A> = {|
action: A,
next: Update<A> | null,
|};
type UpdateQueue<A> = {|
first: Update<A> | null,
dispatch: any,
|};
type Hook = {|
memoizedState: any,
queue: UpdateQueue<any> | null,
next: Hook | null,
|};
const {ReactCurrentDispatcher} = ReactSharedInternals;
const RE_RENDER_LIMIT = 25;
const emptyObject = {};
if (__DEV__) {
Object.freeze(emptyObject);
}
// In DEV, this is the name of the currently executing primitive hook
let currentHookNameInDev: ?string;
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
if (__DEV__) {
console.error(
'%s received a final argument during this render, but not during ' +
'the previous render. Even though the final argument is optional, ' +
'its type cannot change between renders.',
currentHookNameInDev,
);
}
return false;
}
if (__DEV__) {
// Don't bother comparing lengths in prod because these arrays should be
// passed inline.
if (nextDeps.length !== prevDeps.length) {
console.error(
'The final argument passed to %s changed size between renders. The ' +
'order and size of this array must remain constant.\n\n' +
'Previous: %s\n' +
'Incoming: %s',
currentHookNameInDev,
`[${nextDeps.join(', ')}]`,
`[${prevDeps.join(', ')}]`,
);
}
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
class Updater {
constructor(renderer) {
this._renderer = renderer;
this._callbacks = [];
}
_renderer: ReactShallowRenderer;
_callbacks: Array<any>;
_enqueueCallback(callback, publicInstance) {
if (typeof callback === 'function' && publicInstance) {
this._callbacks.push({
callback,
publicInstance,
});
}
}
_invokeCallbacks() {
const callbacks = this._callbacks;
this._callbacks = [];
callbacks.forEach(({callback, publicInstance}) => {
callback.call(publicInstance);
});
}
isMounted(publicInstance) {
return !!this._renderer._element;
}
enqueueForceUpdate(publicInstance, callback, callerName) {
this._enqueueCallback(callback, publicInstance);
this._renderer._forcedUpdate = true;
this._renderer.render(this._renderer._element, this._renderer._context);
}
enqueueReplaceState(publicInstance, completeState, callback, callerName) {
this._enqueueCallback(callback, publicInstance);
this._renderer._newState = completeState;
this._renderer.render(this._renderer._element, this._renderer._context);
}
enqueueSetState(publicInstance, partialState, callback, callerName) {
this._enqueueCallback(callback, publicInstance);
const currentState = this._renderer._newState || publicInstance.state;
if (typeof partialState === 'function') {
partialState = partialState.call(
publicInstance,
currentState,
publicInstance.props,
);
}
// Null and undefined are treated as no-ops.
if (partialState === null || partialState === undefined) {
return;
}
this._renderer._newState = {
...currentState,
...partialState,
};
this._renderer.render(this._renderer._element, this._renderer._context);
}
}
function createHook(): Hook {
return {
memoizedState: null,
queue: null,
next: null,
};
}
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
// $FlowFixMe: Flow doesn't like mixed types
return typeof action === 'function' ? action(state) : action;
}
class ReactShallowRenderer {
static createRenderer = function() {
return new ReactShallowRenderer();
};
constructor() {
this._reset();
}
_reset() {
this._context = null;
this._element = null;
this._instance = null;
this._newState = null;
this._rendered = null;
this._rendering = false;
this._forcedUpdate = false;
this._updater = new Updater(this);
this._dispatcher = this._createDispatcher();
this._workInProgressHook = null;
this._firstWorkInProgressHook = null;
this._isReRender = false;
this._didScheduleRenderPhaseUpdate = false;
this._renderPhaseUpdates = null;
this._numberOfReRenders = 0;
}
_context: null | Object;
_newState: null | Object;
_instance: any;
_element: null | ReactElement;
_rendered: null | mixed;
_updater: Updater;
_rendering: boolean;
_forcedUpdate: boolean;
_dispatcher: DispatcherType;
_workInProgressHook: null | Hook;
_firstWorkInProgressHook: null | Hook;
_renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null;
_isReRender: boolean;
_didScheduleRenderPhaseUpdate: boolean;
_numberOfReRenders: number;
_validateCurrentlyRenderingComponent() {
invariant(
this._rendering && !this._instance,
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
}
_createDispatcher(): DispatcherType {
const useReducer = <S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] => {
this._validateCurrentlyRenderingComponent();
this._createWorkInProgressHook();
const workInProgressHook: Hook = (this._workInProgressHook: any);
if (this._isReRender) {
// This is a re-render.
const queue: UpdateQueue<A> = (workInProgressHook.queue: any);
const dispatch: Dispatch<A> = (queue.dispatch: any);
if (this._numberOfReRenders > 0) {
// Apply the new render phase updates to the previous current hook.
if (this._renderPhaseUpdates !== null) {
// Render phase updates are stored in a map of queue -> linked list
const firstRenderPhaseUpdate = this._renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate !== undefined) {
(this._renderPhaseUpdates: any).delete(queue);
let newState = workInProgressHook.memoizedState;
let update = firstRenderPhaseUpdate;
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
workInProgressHook.memoizedState = newState;
return [newState, dispatch];
}
}
return [workInProgressHook.memoizedState, dispatch];
}
// Process updates outside of render
let newState = workInProgressHook.memoizedState;
let update = queue.first;
if (update !== null) {
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null);
queue.first = null;
workInProgressHook.memoizedState = newState;
}
return [newState, dispatch];
} else {
let initialState;
if (reducer === basicStateReducer) {
// Special case for `useState`.
initialState =
typeof initialArg === 'function'
? ((initialArg: any): () => S)()
: ((initialArg: any): S);
} else {
initialState =
init !== undefined ? init(initialArg) : ((initialArg: any): S);
}
workInProgressHook.memoizedState = initialState;
const queue: UpdateQueue<A> = (workInProgressHook.queue = {
first: null,
dispatch: null,
});
const dispatch: Dispatch<A> = (queue.dispatch = (this._dispatchAction.bind(
this,
queue,
): any));
return [workInProgressHook.memoizedState, dispatch];
}
};
const useState = <S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] => {
return useReducer(
basicStateReducer,
// useReducer has a special case to support lazy useState initializers
(initialState: any),
);
};
const useMemo = <T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T => {
this._validateCurrentlyRenderingComponent();
this._createWorkInProgressHook();
const nextDeps = deps !== undefined ? deps : null;
if (
this._workInProgressHook !== null &&
this._workInProgressHook.memoizedState !== null
) {
const prevState = this._workInProgressHook.memoizedState;
const prevDeps = prevState[1];
if (nextDeps !== null) {
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
(this._workInProgressHook: any).memoizedState = [nextValue, nextDeps];
return nextValue;
};
const useRef = <T>(initialValue: T): {|current: T|} => {
this._validateCurrentlyRenderingComponent();
this._createWorkInProgressHook();
const previousRef = (this._workInProgressHook: any).memoizedState;
if (previousRef === null) {
const ref = {current: initialValue};
if (__DEV__) {
Object.seal(ref);
}
(this._workInProgressHook: any).memoizedState = ref;
return ref;
} else {
return previousRef;
}
};
const readContext = <T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T => {
return context._currentValue;
};
const noOp = () => {
this._validateCurrentlyRenderingComponent();
};
const identity = (fn: Function): Function => {
return fn;
};
const useResponder = (
responder,
props,
): ReactEventResponderListener<any, any> => ({
props: props,
responder,
});
// TODO: implement if we decide to keep the shallow renderer
const useTransition = (
config,
): [(callback: () => void) => void, boolean] => {
this._validateCurrentlyRenderingComponent();
const startTransition = callback => {
callback();
};
return [startTransition, false];
};
// TODO: implement if we decide to keep the shallow renderer
const useDeferredValue = <T>(value: T, config): T => {
this._validateCurrentlyRenderingComponent();
return value;
};
return {
readContext,
useCallback: (identity: any),
useContext: <T>(context: ReactContext<T>): T => {
this._validateCurrentlyRenderingComponent();
return readContext(context);
},
useDebugValue: noOp,
useEffect: noOp,
useImperativeHandle: noOp,
useLayoutEffect: noOp,
useMemo,
useReducer,
useRef,
useState,
useResponder,
useTransition,
useDeferredValue,
};
}
_dispatchAction<A>(queue: UpdateQueue<A>, action: A) {
invariant(
this._numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
if (this._rendering) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
this._didScheduleRenderPhaseUpdate = true;
const update: Update<A> = {
action,
next: null,
};
let renderPhaseUpdates = this._renderPhaseUpdates;
if (renderPhaseUpdates === null) {
this._renderPhaseUpdates = renderPhaseUpdates = new Map();
}
const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
const update: Update<A> = {
action,
next: null,
};
// Append the update to the end of the list.
let last = queue.first;
if (last === null) {
queue.first = update;
} else {
while (last.next !== null) {
last = last.next;
}
last.next = update;
}
// Re-render now.
this.render(this._element, this._context);
}
}
_createWorkInProgressHook(): Hook {
if (this._workInProgressHook === null) {
// This is the first hook in the list
if (this._firstWorkInProgressHook === null) {
this._isReRender = false;
this._firstWorkInProgressHook = this._workInProgressHook = createHook();
} else {
// There's already a work-in-progress. Reuse it.
this._isReRender = true;
this._workInProgressHook = this._firstWorkInProgressHook;
}
} else {
if (this._workInProgressHook.next === null) {
this._isReRender = false;
// Append to the end of the list
this._workInProgressHook = (this
._workInProgressHook: any).next = createHook();
} else {
// There's already a work-in-progress. Reuse it.
this._isReRender = true;
this._workInProgressHook = this._workInProgressHook.next;
}
}
return this._workInProgressHook;
}
_finishHooks(element: ReactElement, context: null | Object) {
if (this._didScheduleRenderPhaseUpdate) {
// Updates were scheduled during the render phase. They are stored in
// the `renderPhaseUpdates` map. Call the component again, reusing the
// work-in-progress hooks and applying the additional updates on top. Keep
// restarting until no more updates are scheduled.
this._didScheduleRenderPhaseUpdate = false;
this._numberOfReRenders += 1;
// Start over from the beginning of the list
this._workInProgressHook = null;
this._rendering = false;
this.render(element, context);
} else {
this._workInProgressHook = null;
this._renderPhaseUpdates = null;
this._numberOfReRenders = 0;
}
}
getMountedInstance() {
return this._instance;
}
getRenderOutput() {
return this._rendered;
}
render(element: ReactElement | null, context: null | Object = emptyObject) {
invariant(
React.isValidElement(element),
'ReactShallowRenderer render(): Invalid component element.%s',
typeof element === 'function'
? ' Instead of passing a component class, make sure to instantiate ' +
'it by passing it to React.createElement.'
: '',
);
element = ((element: any): ReactElement);
// Show a special message for host elements since it's a common case.
invariant(
typeof element.type !== 'string',
'ReactShallowRenderer render(): Shallow rendering works only with custom ' +
'components, not primitives (%s). Instead of calling `.render(el)` and ' +
'inspecting the rendered output, look at `el.props` directly instead.',
element.type,
);
invariant(
isForwardRef(element) ||
typeof element.type === 'function' ||
isMemo(element),
'ReactShallowRenderer render(): Shallow rendering works only with custom ' +
'components, but the provided element type was `%s`.',
Array.isArray(element.type)
? 'array'
: element.type === null
? 'null'
: typeof element.type,
);
if (this._rendering) {
return;
}
if (this._element != null && this._element.type !== element.type) {
this._reset();
}
const elementType = isMemo(element) ? element.type.type : element.type;
const previousElement = this._element;
this._rendering = true;
this._element = element;
this._context = getMaskedContext(elementType.contextTypes, context);
// Inner memo component props aren't currently validated in createElement.
if (isMemo(element) && elementType.propTypes) {
currentlyValidatingElement = element;
checkPropTypes(
elementType.propTypes,
element.props,
'prop',
getComponentName(elementType),
getStackAddendum,
);
}
if (this._instance) {
this._updateClassComponent(elementType, element, this._context);
} else {
if (shouldConstruct(elementType)) {
this._instance = new elementType(
element.props,
this._context,
this._updater,
);
if (typeof elementType.getDerivedStateFromProps === 'function') {
const partialState = elementType.getDerivedStateFromProps.call(
null,
element.props,
this._instance.state,
);
if (partialState != null) {
this._instance.state = Object.assign(
{},
this._instance.state,
partialState,
);
}
}
if (elementType.contextTypes) {
currentlyValidatingElement = element;
checkPropTypes(
elementType.contextTypes,
this._context,
'context',
getName(elementType, this._instance),
getStackAddendum,
);
currentlyValidatingElement = null;
}
this._mountClassComponent(elementType, element, this._context);
} else {
let shouldRender = true;
if (isMemo(element) && previousElement !== null) {
// This is a Memo component that is being re-rendered.
const compare = element.type.compare || shallowEqual;
if (compare(previousElement.props, element.props)) {
shouldRender = false;
}
}
if (shouldRender) {
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = this._dispatcher;
try {
// elementType could still be a ForwardRef if it was
// nested inside Memo.
if (elementType.$$typeof === ForwardRef) {
invariant(
typeof elementType.render === 'function',
'forwardRef requires a render function but was given %s.',
typeof elementType.render,
);
this._rendered = elementType.render.call(
undefined,
element.props,
element.ref,
);
} else {
this._rendered = elementType(element.props, this._context);
}
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
this._finishHooks(element, context);
}
}
}
this._rendering = false;
this._updater._invokeCallbacks();
return this.getRenderOutput();
}
unmount() {
if (this._instance) {
if (typeof this._instance.componentWillUnmount === 'function') {
this._instance.componentWillUnmount();
}
}
this._reset();
}
_mountClassComponent(
elementType: Function,
element: ReactElement,
context: null | Object,
) {
this._instance.context = context;
this._instance.props = element.props;
this._instance.state = this._instance.state || null;
this._instance.updater = this._updater;
if (
typeof this._instance.UNSAFE_componentWillMount === 'function' ||
typeof this._instance.componentWillMount === 'function'
) {
const beforeState = this._newState;
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
typeof elementType.getDerivedStateFromProps !== 'function' &&
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
) {
if (typeof this._instance.componentWillMount === 'function') {
this._instance.componentWillMount();
}
if (typeof this._instance.UNSAFE_componentWillMount === 'function') {
this._instance.UNSAFE_componentWillMount();
}
}
// setState may have been called during cWM
if (beforeState !== this._newState) {
this._instance.state = this._newState || emptyObject;
}
}
this._rendered = this._instance.render();
// Intentionally do not call componentDidMount()
// because DOM refs are not available.
}
_updateClassComponent(
elementType: Function,
element: ReactElement,
context: null | Object,
) {
const {props} = element;
const oldState = this._instance.state || emptyObject;
const oldProps = this._instance.props;
if (oldProps !== props) {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
typeof elementType.getDerivedStateFromProps !== 'function' &&
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
) {
if (typeof this._instance.componentWillReceiveProps === 'function') {
this._instance.componentWillReceiveProps(props, context);
}
if (
typeof this._instance.UNSAFE_componentWillReceiveProps === 'function'
) {
this._instance.UNSAFE_componentWillReceiveProps(props, context);
}
}
}
// Read state after cWRP in case it calls setState
let state = this._newState || oldState;
if (typeof elementType.getDerivedStateFromProps === 'function') {
const partialState = elementType.getDerivedStateFromProps.call(
null,
props,
state,
);
if (partialState != null) {
state = Object.assign({}, state, partialState);
}
}
let shouldUpdate = true;
if (this._forcedUpdate) {
shouldUpdate = true;
this._forcedUpdate = false;
} else if (typeof this._instance.shouldComponentUpdate === 'function') {
shouldUpdate = !!this._instance.shouldComponentUpdate(
props,
state,
context,
);
} else if (
elementType.prototype &&
elementType.prototype.isPureReactComponent
) {
shouldUpdate =
!shallowEqual(oldProps, props) || !shallowEqual(oldState, state);
}
if (shouldUpdate) {
// In order to support react-lifecycles-compat polyfilled components,
// Unsafe lifecycles should not be invoked for components using the new APIs.
if (
typeof elementType.getDerivedStateFromProps !== 'function' &&
typeof this._instance.getSnapshotBeforeUpdate !== 'function'
) {
if (typeof this._instance.componentWillUpdate === 'function') {
this._instance.componentWillUpdate(props, state, context);
}
if (typeof this._instance.UNSAFE_componentWillUpdate === 'function') {
this._instance.UNSAFE_componentWillUpdate(props, state, context);
}
}
}
this._instance.context = context;
this._instance.props = props;
this._instance.state = state;
this._newState = null;
if (shouldUpdate) {
this._rendered = this._instance.render();
}
// Intentionally do not call componentDidUpdate()
// because DOM refs are not available.
}
}
let currentlyValidatingElement = null;
function getDisplayName(element) {
if (element == null) {
return '#empty';
} else if (typeof element === 'string' || typeof element === 'number') {
return '#text';
} else if (typeof element.type === 'string') {
return element.type;
} else {
const elementType = isMemo(element) ? element.type.type : element.type;
return elementType.displayName || elementType.name || 'Unknown';
}
}
function getStackAddendum() {
let stack = '';
if (currentlyValidatingElement) {
const name = getDisplayName(currentlyValidatingElement);
const owner = currentlyValidatingElement._owner;
stack += describeComponentFrame(
name,
currentlyValidatingElement._source,
owner && getComponentName(owner.type),
);
}
return stack;
}
function getName(type, instance) {
const constructor = instance && instance.constructor;
return (
type.displayName ||
(constructor && constructor.displayName) ||
type.name ||
(constructor && constructor.name) ||
null
);
}
function shouldConstruct(Component) {
return !!(Component.prototype && Component.prototype.isReactComponent);
}
function getMaskedContext(contextTypes, unmaskedContext) {
if (!contextTypes || !unmaskedContext) {
return emptyObject;
}
const context = {};
for (let key in contextTypes) {
context[key] = unmaskedContext[key];
}
return context;
}
// This should probably be a default export and a named export.
// However, this not how any of other APIs are designed so doesn't line up
// with our build configs and makes it hard to properly support ES modules
// and CommonJS.
export default ReactShallowRenderer;

View File

@@ -10856,6 +10856,15 @@ react-native-web@^0.11.5:
prop-types "^15.6.0"
react-timer-mixin "^0.13.4"
react-shallow-renderer@^16.12.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.12.0.tgz#e68ca8fca7a616db42c36fdd6929723ba0bccef6"
integrity sha512-elIe2dhXJAO19IY1K3b4YWHBLnKE5wRH+PKx3AYzM6j2+N5UI+FGLKiRNFbLQcggrpBiy3GLhj7MqNUlRk9Z7g==
dependencies:
object-assign "^4.1.1"
prop-types "^15.7.2"
react-is "^16.12.0"
react-timer-mixin@^0.13.4:
version "0.13.4"
resolved "https://registry.yarnpkg.com/react-timer-mixin/-/react-timer-mixin-0.13.4.tgz#75a00c3c94c13abe29b43d63b4c65a88fc8264d3"