Skip to content

Commit 051d7d0

Browse files
committed
[Shallow] Implement callEffects option (facebook#15275)
1 parent 2c4d61e commit 051d7d0

File tree

2 files changed

+137
-7
lines changed

2 files changed

+137
-7
lines changed

packages/react-test-renderer/src/ReactShallowRenderer.js

+105-5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ function areHookInputsEqual(
9191
return true;
9292
}
9393

94+
function shouldRunEffectsBasedOnInputs(
95+
before: Array<mixed>,
96+
after: Array<mixed> | void | null,
97+
) {
98+
if (after == null || before.length !== after.length) {
99+
return true;
100+
}
101+
102+
return before.some((value, i) => (after: any)[i] !== value);
103+
}
104+
94105
class Updater {
95106
constructor(renderer) {
96107
this._renderer = renderer;
@@ -172,12 +183,21 @@ function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
172183
return typeof action === 'function' ? action(state) : action;
173184
}
174185

186+
type ShallowRenderOptions = {
187+
callEffects: boolean,
188+
};
189+
190+
const isEffectHook = Symbol();
191+
175192
class ReactShallowRenderer {
176-
static createRenderer = function() {
177-
return new ReactShallowRenderer();
193+
static createRenderer = function(
194+
options: ShallowRenderOptions = {callEffects: false},
195+
) {
196+
return new ReactShallowRenderer(options);
178197
};
179198

180-
constructor() {
199+
constructor(options: ShallowRenderOptions) {
200+
this._options = options;
181201
this._reset();
182202
}
183203

@@ -199,6 +219,7 @@ class ReactShallowRenderer {
199219
this._numberOfReRenders = 0;
200220
}
201221

222+
_options: ShallowRenderOptions;
202223
_context: null | Object;
203224
_newState: null | Object;
204225
_instance: any;
@@ -308,6 +329,54 @@ class ReactShallowRenderer {
308329
);
309330
};
310331

332+
const useGenericEffect = (
333+
create: () => (() => void) | void,
334+
inputs: Array<mixed> | void | null,
335+
isLayoutEffect: boolean,
336+
) => {
337+
this._validateCurrentlyRenderingComponent();
338+
this._createWorkInProgressHook();
339+
340+
if (this._workInProgressHook !== null) {
341+
if (this._workInProgressHook.memoizedState == null) {
342+
this._workInProgressHook.memoizedState = {
343+
isEffectHook,
344+
isLayoutEffect,
345+
create,
346+
inputs,
347+
cleanup: null,
348+
run: true,
349+
};
350+
} else {
351+
const {memoizedState} = this._workInProgressHook;
352+
this._workInProgressHook.memoizedState = {
353+
isEffectHook,
354+
isLayoutEffect,
355+
create,
356+
inputs,
357+
cleanup: memoizedState.cleanup,
358+
run:
359+
inputs == null ||
360+
shouldRunEffectsBasedOnInputs(inputs, memoizedState.inputs),
361+
};
362+
}
363+
}
364+
};
365+
366+
const useEffect = (
367+
create: () => (() => void) | void,
368+
inputs: Array<mixed> | void | null,
369+
) => {
370+
useGenericEffect(create, inputs, false);
371+
};
372+
373+
const useLayoutEffect = (
374+
create: () => (() => void) | void,
375+
inputs: Array<mixed> | void | null,
376+
) => {
377+
useGenericEffect(create, inputs, true);
378+
};
379+
311380
const useMemo = <T>(
312381
nextCreate: () => T,
313382
deps: Array<mixed> | void | null,
@@ -374,9 +443,9 @@ class ReactShallowRenderer {
374443
return readContext(context);
375444
},
376445
useDebugValue: noOp,
377-
useEffect: noOp,
446+
useEffect: this._options.callEffects ? useEffect : noOp,
378447
useImperativeHandle: noOp,
379-
useLayoutEffect: noOp,
448+
useLayoutEffect: this._options.callEffects ? useLayoutEffect : noOp,
380449
useMemo,
381450
useReducer,
382451
useRef,
@@ -619,6 +688,7 @@ class ReactShallowRenderer {
619688
ReactCurrentDispatcher.current = prevDispatcher;
620689
}
621690
this._finishHooks(element, context);
691+
this._callEffectsIfDesired();
622692
}
623693
}
624694
}
@@ -679,6 +749,36 @@ class ReactShallowRenderer {
679749
// because DOM refs are not available.
680750
}
681751

752+
_callEffectsIfDesired() {
753+
if (!this._options.callEffects) {
754+
return;
755+
}
756+
757+
this._callEffects(true);
758+
this._callEffects(false);
759+
}
760+
761+
_callEffects(callLayoutEffects: boolean) {
762+
for (
763+
let hook = this._firstWorkInProgressHook;
764+
hook !== null;
765+
hook = hook.next
766+
) {
767+
const {memoizedState} = hook;
768+
if (
769+
memoizedState != null &&
770+
memoizedState.isEffectHook === isEffectHook &&
771+
memoizedState.isLayoutEffect === callLayoutEffects &&
772+
memoizedState.run
773+
) {
774+
if (memoizedState.cleanup) {
775+
memoizedState.cleanup();
776+
}
777+
memoizedState.cleanup = memoizedState.create();
778+
}
779+
}
780+
}
781+
682782
_updateClassComponent(
683783
elementType: Function,
684784
element: ReactElement,

packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.js

+32-2
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ describe('ReactShallowRenderer with hooks', () => {
231231
);
232232
});
233233

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

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

255+
describe('when callEffects option is used', () => {
256+
it('should trigger effects after render', () => {
257+
const happenings = [];
258+
259+
function SomeComponent({defaultName}) {
260+
React.useEffect(() => {
261+
happenings.push('call effect');
262+
});
263+
264+
React.useLayoutEffect(() => {
265+
happenings.push('call layout effect');
266+
});
267+
268+
happenings.push('render');
269+
270+
return <div>Hello world</div>;
271+
}
272+
273+
const shallowRenderer = createRenderer({callEffects: true});
274+
shallowRenderer.render(<SomeComponent />);
275+
276+
// Note the layout effect is triggered first.
277+
expect(happenings).toEqual([
278+
'render',
279+
'call layout effect',
280+
'call effect',
281+
]);
282+
});
283+
});
284+
255285
it('should work with useRef', () => {
256286
function SomeComponent() {
257287
const randomNumberRef = React.useRef({number: Math.random()});

0 commit comments

Comments
 (0)