Skip to content

Commit

Permalink
[Shallow] Implement callEffects option (facebook#15275)
Browse files Browse the repository at this point in the history
  • Loading branch information
insidewhy committed Aug 6, 2019
1 parent 606f76b commit 031519d
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 7 deletions.
110 changes: 105 additions & 5 deletions packages/react-test-renderer/src/ReactShallowRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,21 @@ function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}

type ShallowRenderOptions = {
callEffects: boolean,
};

const isEffectHook = Symbol();

class ReactShallowRenderer {
static createRenderer = function() {
return new ReactShallowRenderer();
static createRenderer = function(
options: ShallowRenderOptions = {callEffects: false},
) {
return new ReactShallowRenderer(options);
};

constructor() {
constructor(options: ShallowRenderOptions) {
this._options = options;
this._reset();
}

Expand All @@ -202,6 +211,7 @@ class ReactShallowRenderer {
this._numberOfReRenders = 0;
}

_options: ShallowRenderOptions;
_context: null | Object;
_newState: null | Object;
_instance: any;
Expand Down Expand Up @@ -311,6 +321,54 @@ class ReactShallowRenderer {
);
};

const useGenericEffect = (
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
isLayoutEffect: boolean,
) => {
this._validateCurrentlyRenderingComponent();
this._createWorkInProgressHook();

if (this._workInProgressHook !== null) {
if (this._workInProgressHook.memoizedState == null) {
this._workInProgressHook.memoizedState = {
isEffectHook,
isLayoutEffect,
create,
inputs,
cleanup: null,
run: true,
};
} else {
const {memoizedState} = this._workInProgressHook;
this._workInProgressHook.memoizedState = {
isEffectHook,
isLayoutEffect,
create,
inputs,
cleanup: memoizedState.cleanup,
run:
inputs == null ||
shouldRunEffectsBasedOnInputs(memoizedState.inputs, inputs),
};
}
}
};

const useEffect = (
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
) => {
useGenericEffect(create, inputs, false);
};

const useLayoutEffect = (
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
) => {
useGenericEffect(create, inputs, true);
};

const useMemo = <T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
Expand Down Expand Up @@ -385,9 +443,9 @@ class ReactShallowRenderer {
return readContext(context);
},
useDebugValue: noOp,
useEffect: noOp,
useEffect: this._options.callEffects ? useEffect : noOp,
useImperativeHandle: noOp,
useLayoutEffect: noOp,
useLayoutEffect: this._options.callEffects ? useLayoutEffect : noOp,
useMemo,
useReducer,
useRef,
Expand Down Expand Up @@ -630,6 +688,7 @@ class ReactShallowRenderer {
ReactCurrentDispatcher.current = prevDispatcher;
}
this._finishHooks(element, context);
this._callEffectsIfDesired();
}
}
}
Expand Down Expand Up @@ -690,6 +749,36 @@ class ReactShallowRenderer {
// because DOM refs are not available.
}

_callEffectsIfDesired() {
if (!this._options.callEffects) {
return;
}

this._callEffects(true);
this._callEffects(false);
}

_callEffects(callLayoutEffects: boolean) {
for (
let hook = this._firstWorkInProgressHook;
hook !== null;
hook = hook.next
) {
const {memoizedState} = hook;
if (
memoizedState != null &&
memoizedState.isEffectHook === isEffectHook &&
memoizedState.isLayoutEffect === callLayoutEffects &&
memoizedState.run
) {
if (memoizedState.cleanup) {
memoizedState.cleanup();
}
memoizedState.cleanup = memoizedState.create();
}
}
}

_updateClassComponent(
elementType: Function,
element: ReactElement,
Expand Down Expand Up @@ -833,4 +922,15 @@ function getMaskedContext(contextTypes, unmaskedContext) {
return context;
}

function shouldRunEffectsBasedOnInputs(
before: Array<mixed> | void | null,
after: Array<mixed>,
) {
if (before == null || before.length !== after.length) {
return true;
}

return before.some((value, i) => after[i] !== value);
}

export default ReactShallowRenderer;
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ describe('ReactShallowRenderer with hooks', () => {
);
});

it('should not trigger effects', () => {
let effectsCalled = [];
it('should not trigger effects by default', () => {
const effectsCalled = [];

function SomeComponent({defaultName}) {
React.useEffect(() => {
Expand All @@ -252,6 +252,36 @@ describe('ReactShallowRenderer with hooks', () => {
expect(effectsCalled).toEqual([]);
});

describe('when callEffects option is used', () => {
it('should trigger effects after render', () => {
const happenings = [];

function SomeComponent({defaultName}) {
React.useEffect(() => {
happenings.push('call effect');
});

React.useLayoutEffect(() => {
happenings.push('call layout effect');
});

happenings.push('render');

return <div>Hello world</div>;
}

const shallowRenderer = createRenderer({callEffects: true});
shallowRenderer.render(<SomeComponent />);

// Note the layout effect is triggered first.
expect(happenings).toEqual([
'render',
'call layout effect',
'call effect',
]);
});
});

it('should work with useRef', () => {
function SomeComponent() {
const randomNumberRef = React.useRef({number: Math.random()});
Expand Down

0 comments on commit 031519d

Please sign in to comment.