Add hooks support to ReactShallowRenderer (#14567)
* Add hook support to ReactShallowRenderer
Dominic Gannaway committed
Jan 17, 2019 at 17:42 UTC
4feab7fc92125c15f8c022058f1ac78deb18d650
2 files changed
+723
-10
packages/react-test-renderer/src/ReactShallowRenderer.js
+403
-10
@@ -4,6 +4,7 @@
4
* This source code is licensed under the MIT license found in the
5
* LICENSE file in the root directory of this source tree.
6
*
7
+ * @flow
8
*/
9
10
import React from 'react';
@@ -13,18 +14,93 @@ import getComponentName from 'shared/getComponentName';
14
import shallowEqual from 'shared/shallowEqual';
15
import invariant from 'shared/invariant';
16
import checkPropTypes from 'prop-types/checkPropTypes';
17
+import ReactSharedInternals from 'shared/ReactSharedInternals';
18
+import {enableHooks} from 'shared/ReactFeatureFlags';
19
+import warning from 'shared/warning';
20
+import is from 'shared/objectIs';
21
+
22
+import typeof {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactFiberDispatcher';
23
+import type {ReactContext} from 'shared/ReactTypes';
24
+import type {ReactElement} from 'shared/ReactElementType';
25
+
26
+type BasicStateAction<S> = (S => S) | S;
27
+type Dispatch<A> = A => void;
28
+
29
+type Update<A> = {
30
+ action: A,
31
+ next: Update<A> | null,
32
+};
33
+
34
+type UpdateQueue<A> = {
35
+ last: Update<A> | null,
36
+ dispatch: any,
37
+};
38
+
39
+type Hook = {
40
+ memoizedState: any,
41
+ queue: UpdateQueue<any> | null,
42
+ next: Hook | null,
43
+};
44
+
45
+const {ReactCurrentDispatcher} = ReactSharedInternals;
46
+
47
+const RE_RENDER_LIMIT = 25;
48
49
const emptyObject = {};
50
if (__DEV__) {
51
Object.freeze(emptyObject);
52
}
53
54
+// In DEV, this is the name of the currently executing primitive hook
55
+let currentHookNameInDev: ?string;
56
+
57
+function areHookInputsEqual(
58
+ nextDeps: Array<mixed>,
59
+ prevDeps: Array<mixed> | null,
60
+) {
61
+ if (prevDeps === null) {
62
+ warning(
63
+ false,
64
+ '%s received a final argument during this render, but not during ' +
65
+ 'the previous render. Even though the final argument is optional, ' +
66
+ 'its type cannot change between renders.',
67
+ currentHookNameInDev,
68
+ );
69
+ return false;
70
+ }
71
+
72
+ // Don't bother comparing lengths in prod because these arrays should be
73
+ // passed inline.
74
+ if (nextDeps.length !== prevDeps.length) {
75
+ warning(
76
+ false,
77
+ 'The final argument passed to %s changed size between renders. The ' +
78
+ 'order and size of this array must remain constant.\n\n' +
79
+ 'Previous: %s\n' +
80
+ 'Incoming: %s',
81
+ currentHookNameInDev,
82
+ `[${nextDeps.join(', ')}]`,
83
+ `[${prevDeps.join(', ')}]`,
84
+ );
85
+ }
86
+ for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
87
+ if (is(nextDeps[i], prevDeps[i])) {
88
+ continue;
89
+ }
90
+ return false;
91
+ }
92
+ return true;
93
+}
94
+
95
class Updater {
96
constructor(renderer) {
97
this._renderer = renderer;
98
this._callbacks = [];
99
}
100
101
+ _renderer: ReactShallowRenderer;
102
+ _callbacks: Array<any>;
103
+
104
_enqueueCallback(callback, publicInstance) {
105
if (typeof callback === 'function' && publicInstance) {
106
this._callbacks.push({
@@ -85,6 +161,18 @@ class Updater {
161
}
162
}
163
164
+function createHook(): Hook {
165
+ return {
166
+ memoizedState: null,
167
+ queue: null,
168
+ next: null,
169
+ };
170
+}
171
+
172
+function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
173
+ return typeof action === 'function' ? action(state) : action;
174
+}
175
+
176
class ReactShallowRenderer {
177
static createRenderer = function() {
178
return new ReactShallowRenderer();
@@ -99,6 +187,289 @@ class ReactShallowRenderer {
187
this._rendering = false;
188
this._forcedUpdate = false;
189
this._updater = new Updater(this);
190
+ if (enableHooks) {
191
+ this._dispatcher = this._createDispatcher();
192
+ this._workInProgressHook = null;
193
+ this._firstWorkInProgressHook = null;
194
+ this._isReRender = false;
195
+ this._didScheduleRenderPhaseUpdate = false;
196
+ this._renderPhaseUpdates = null;
197
+ this._currentlyRenderingComponent = null;
198
+ this._numberOfReRenders = 0;
199
+ this._previousComponentIdentity = null;
200
+ }
201
+ }
202
+
203
+ _context: null | Object;
204
+ _newState: null | Object;
205
+ _instance: any;
206
+ _element: null | ReactElement;
207
+ _rendered: null | mixed;
208
+ _updater: Updater;
209
+ _rendering: boolean;
210
+ _forcedUpdate: boolean;
211
+ _dispatcher: DispatcherType;
212
+ _workInProgressHook: null | Hook;
213
+ _firstWorkInProgressHook: null | Hook;
214
+ _currentlyRenderingComponent: null | Object;
215
+ _previousComponentIdentity: null | Object;
216
+ _renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null;
217
+ _isReRender: boolean;
218
+ _didScheduleRenderPhaseUpdate: boolean;
219
+ _numberOfReRenders: number;
220
+
221
+ _validateCurrentlyRenderingComponent() {
222
+ invariant(
223
+ this._currentlyRenderingComponent !== null,
224
+ 'Hooks can only be called inside the body of a function component.',
225
+ );
226
+ }
227
+
228
+ _createDispatcher(): DispatcherType {
229
+ const useReducer = <S, A>(
230
+ reducer: (S, A) => S,
231
+ initialState: S,
232
+ initialAction: A | void | null,
233
+ ): [S, Dispatch<A>] => {
234
+ this._validateCurrentlyRenderingComponent();
235
+ this._createWorkInProgressHook();
236
+ const workInProgressHook: Hook = (this._workInProgressHook: any);
237
+ if (this._isReRender) {
238
+ // This is a re-render. Apply the new render phase updates to the previous
239
+ // current hook.
240
+ const queue: UpdateQueue<A> = (workInProgressHook.queue: any);
241
+ const dispatch: Dispatch<A> = (queue.dispatch: any);
242
+ if (this._renderPhaseUpdates !== null) {
243
+ // Render phase updates are stored in a map of queue -> linked list
244
+ const firstRenderPhaseUpdate = this._renderPhaseUpdates.get(queue);
245
+ if (firstRenderPhaseUpdate !== undefined) {
246
+ (this._renderPhaseUpdates: any).delete(queue);
247
+ let newState = workInProgressHook.memoizedState;
248
+ let update = firstRenderPhaseUpdate;
249
+ do {
250
+ // Process this render phase update. We don't have to check the
251
+ // priority because it will always be the same as the current
252
+ // render's.
253
+ const action = update.action;
254
+ newState = reducer(newState, action);
255
+ update = update.next;
256
+ } while (update !== null);
257
+
258
+ workInProgressHook.memoizedState = newState;
259
+
260
+ return [newState, dispatch];
261
+ }
262
+ }
263
+ return [workInProgressHook.memoizedState, dispatch];
264
+ } else {
265
+ if (reducer === basicStateReducer) {
266
+ // Special case for `useState`.
267
+ if (typeof initialState === 'function') {
268
+ initialState = initialState();
269
+ }
270
+ } else if (initialAction !== undefined && initialAction !== null) {
271
+ initialState = reducer(initialState, initialAction);
272
+ }
273
+ workInProgressHook.memoizedState = initialState;
274
+ const queue: UpdateQueue<A> = (workInProgressHook.queue = {
275
+ last: null,
276
+ dispatch: null,
277
+ });
278
+ const dispatch: Dispatch<
279
+ A,
280
+ > = (queue.dispatch = (this._dispatchAction.bind(
281
+ this,
282
+ (this._currentlyRenderingComponent: any),
283
+ queue,
284
+ ): any));
285
+ return [workInProgressHook.memoizedState, dispatch];
286
+ }
287
+ };
288
+
289
+ const useState = <S>(
290
+ initialState: (() => S) | S,
291
+ ): [S, Dispatch<BasicStateAction<S>>] => {
292
+ return useReducer(
293
+ basicStateReducer,
294
+ // useReducer has a special case to support lazy useState initializers
295
+ (initialState: any),
296
+ );
297
+ };
298
+
299
+ const useMemo = <T>(
300
+ nextCreate: () => T,
301
+ deps: Array<mixed> | void | null,
302
+ ): T => {
303
+ this._validateCurrentlyRenderingComponent();
304
+ this._createWorkInProgressHook();
305
+
306
+ const nextDeps = deps !== undefined && deps !== null ? deps : null;
307
+
308
+ if (
309
+ this._workInProgressHook !== null &&
310
+ this._workInProgressHook.memoizedState !== null
311
+ ) {
312
+ const prevState = this._workInProgressHook.memoizedState;
313
+ const prevDeps = prevState[1];
314
+ if (nextDeps !== null) {
315
+ if (areHookInputsEqual(nextDeps, prevDeps)) {
316
+ return prevState[0];
317
+ }
318
+ }
319
+ }
320
+
321
+ const nextValue = nextCreate();
322
+ (this._workInProgressHook: any).memoizedState = [nextValue, nextDeps];
323
+ return nextValue;
324
+ };
325
+
326
+ const useRef = <T>(initialValue: T): {current: T} => {
327
+ this._validateCurrentlyRenderingComponent();
328
+ this._createWorkInProgressHook();
329
+ const previousRef = (this._workInProgressHook: any).memoizedState;
330
+ if (previousRef === null) {
331
+ const ref = {current: initialValue};
332
+ if (__DEV__) {
333
+ Object.seal(ref);
334
+ }
335
+ (this._workInProgressHook: any).memoizedState = ref;
336
+ return ref;
337
+ } else {
338
+ return previousRef;
339
+ }
340
+ };
341
+
342
+ const readContext = <T>(
343
+ context: ReactContext<T>,
344
+ observedBits: void | number | boolean,
345
+ ): T => {
346
+ return context._currentValue;
347
+ };
348
+
349
+ const noOp = () => {
350
+ this._validateCurrentlyRenderingComponent();
351
+ };
352
+
353
+ const identity = (fn: Function): Function => {
354
+ return fn;
355
+ };
356
+
357
+ return {
358
+ readContext,
359
+ useCallback: (identity: any),
360
+ useContext: <T>(context: ReactContext<T>): T => {
361
+ this._validateCurrentlyRenderingComponent();
362
+ return readContext(context);
363
+ },
364
+ useDebugValue: noOp,
365
+ useEffect: noOp,
366
+ useImperativeHandle: noOp,
367
+ useLayoutEffect: noOp,
368
+ useMemo,
369
+ useReducer,
370
+ useRef,
371
+ useState,
372
+ };
373
+ }
374
+
375
+ _dispatchAction<A>(
376
+ componentIdentity: Object,
377
+ queue: UpdateQueue<A>,
378
+ action: A,
379
+ ) {
380
+ invariant(
381
+ this._numberOfReRenders < RE_RENDER_LIMIT,
382
+ 'Too many re-renders. React limits the number of renders to prevent ' +
383
+ 'an infinite loop.',
384
+ );
385
+
386
+ if (componentIdentity === this._currentlyRenderingComponent) {
387
+ // This is a render phase update. Stash it in a lazily-created map of
388
+ // queue -> linked list of updates. After this render pass, we'll restart
389
+ // and apply the stashed updates on top of the work-in-progress hook.
390
+ this._didScheduleRenderPhaseUpdate = true;
391
+ const update: Update<A> = {
392
+ action,
393
+ next: null,
394
+ };
395
+ let renderPhaseUpdates = this._renderPhaseUpdates;
396
+ if (renderPhaseUpdates === null) {
397
+ this._renderPhaseUpdates = renderPhaseUpdates = new Map();
398
+ }
399
+ const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
400
+ if (firstRenderPhaseUpdate === undefined) {
401
+ renderPhaseUpdates.set(queue, update);
402
+ } else {
403
+ // Append the update to the end of the list.
404
+ let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
405
+ while (lastRenderPhaseUpdate.next !== null) {
406
+ lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
407
+ }
408
+ lastRenderPhaseUpdate.next = update;
409
+ }
410
+ } else {
411
+ // This means an update has happened after the function component has
412
+ // returned. On the server this is a no-op. In React Fiber, the update
413
+ // would be scheduled for a future render.
414
+ }
415
+ }
416
+
417
+ _createWorkInProgressHook(): Hook {
418
+ if (this._workInProgressHook === null) {
419
+ // This is the first hook in the list
420
+ if (this._firstWorkInProgressHook === null) {
421
+ this._isReRender = false;
422
+ this._firstWorkInProgressHook = this._workInProgressHook = createHook();
423
+ } else {
424
+ // There's already a work-in-progress. Reuse it.
425
+ this._isReRender = true;
426
+ this._workInProgressHook = this._firstWorkInProgressHook;
427
+ }
428
+ } else {
429
+ if (this._workInProgressHook.next === null) {
430
+ this._isReRender = false;
431
+ // Append to the end of the list
432
+ this._workInProgressHook = (this
433
+ ._workInProgressHook: any).next = createHook();
434
+ } else {
435
+ // There's already a work-in-progress. Reuse it.
436
+ this._isReRender = true;
437
+ this._workInProgressHook = this._workInProgressHook.next;
438
+ }
439
+ }
440
+ return this._workInProgressHook;
441
+ }
442
+
443
+ _prepareToUseHooks(componentIdentity: Object): void {
444
+ if (
445
+ this._previousComponentIdentity !== null &&
446
+ this._previousComponentIdentity !== componentIdentity
447
+ ) {
448
+ this._firstWorkInProgressHook = null;
449
+ }
450
+ this._currentlyRenderingComponent = componentIdentity;
451
+ this._previousComponentIdentity = componentIdentity;
452
+ }
453
+
454
+ _finishHooks(element: ReactElement, context: null | Object) {
455
+ if (this._didScheduleRenderPhaseUpdate) {
456
+ // Updates were scheduled during the render phase. They are stored in
457
+ // the `renderPhaseUpdates` map. Call the component again, reusing the
458
+ // work-in-progress hooks and applying the additional updates on top. Keep
459
+ // restarting until no more updates are scheduled.
460
+ this._didScheduleRenderPhaseUpdate = false;
461
+ this._numberOfReRenders += 1;
462
+
463
+ // Start over from the beginning of the list
464
+ this._workInProgressHook = null;
465
+ this._rendering = false;
466
+ this.render(element, context);
467
+ } else {
468
+ this._currentlyRenderingComponent = null;
469
+ this._workInProgressHook = null;
470
+ this._renderPhaseUpdates = null;
471
+ this._numberOfReRenders = 0;
472
+ }
473
}
474
475
getMountedInstance() {
@@ -109,7 +480,7 @@ class ReactShallowRenderer {
480
return this._rendered;
481
}
482
112
- render(element, context = emptyObject) {
483
+ render(element: ReactElement | null, context: null | Object = emptyObject) {
484
invariant(
485
React.isValidElement(element),
486
'ReactShallowRenderer render(): Invalid component element.%s',
@@ -118,6 +489,7 @@ class ReactShallowRenderer {
489
'it by passing it to React.createElement.'
490
: '',
491
);
492
+ element = ((element: any): ReactElement);
493
// Show a special message for host elements since it's a common case.
494
invariant(
495
typeof element.type !== 'string',
@@ -175,11 +547,27 @@ class ReactShallowRenderer {
547
548
this._mountClassComponent(element, this._context);
549
} else {
178
- this._rendered = element.type.call(
179
- undefined,
180
- element.props,
181
- this._context,
182
- );
550
+ if (enableHooks) {
551
+ const prevDispatcher = ReactCurrentDispatcher.current;
552
+ ReactCurrentDispatcher.current = this._dispatcher;
553
+ this._prepareToUseHooks(element.type);
554
+ try {
555
+ this._rendered = element.type.call(
556
+ undefined,
557
+ element.props,
558
+ this._context,
559
+ );
560
+ } finally {
561
+ ReactCurrentDispatcher.current = prevDispatcher;
562
+ }
563
+ this._finishHooks(element, context);
564
+ } else {
565
+ this._rendered = element.type.call(
566
+ undefined,
567
+ element.props,
568
+ this._context,
569
+ );
570
+ }
571
}
572
}
573
@@ -196,6 +584,8 @@ class ReactShallowRenderer {
584
}
585
}
586
587
+ this._firstWorkInProgressHook = null;
588
+ this._previousComponentIdentity = null;
589
this._context = null;
590
this._element = null;
591
this._newState = null;
@@ -203,7 +593,7 @@ class ReactShallowRenderer {
593
this._instance = null;
594
}
595
206
- _mountClassComponent(element, context) {
596
+ _mountClassComponent(element: ReactElement, context: null | Object) {
597
this._instance.context = context;
598
this._instance.props = element.props;
599
this._instance.state = this._instance.state || null;
@@ -240,7 +630,7 @@ class ReactShallowRenderer {
630
// because DOM refs are not available.
631
}
632
243
- _updateClassComponent(element, context) {
633
+ _updateClassComponent(element: ReactElement, context: null | Object) {
634
const {props, type} = element;
635
636
const oldState = this._instance.state || emptyObject;
@@ -310,7 +700,10 @@ class ReactShallowRenderer {
700
// because DOM refs are not available.
701
}
702
313
- _updateStateFromStaticLifecycle(props) {
703
+ _updateStateFromStaticLifecycle(props: Object) {
704
+ if (this._element === null) {
705
+ return;
706
+ }
707
const {type} = this._element;
708
709
if (typeof type.getDerivedStateFromProps === 'function') {
@@ -373,7 +766,7 @@ function shouldConstruct(Component) {
766
}
767
768
function getMaskedContext(contextTypes, unmaskedContext) {
376
- if (!contextTypes) {
769
+ if (!contextTypes || !unmaskedContext) {
770
return emptyObject;
771
}
772
const context = {};
packages/react-test-renderer/src/__tests__/ReactShallowRendererHooks-test.internal.js
new
+320
@@ -0,0 +1,320 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ * @jest-environment node
9
+ */
10
+
11
+'use strict';
12
+
13
+let createRenderer;
14
+let React;
15
+
16
+describe('ReactShallowRenderer with hooks', () => {
17
+ beforeEach(() => {
18
+ jest.resetModules();
19
+ let ReactFeatureFlags = require('shared/ReactFeatureFlags');
20
+ // TODO: Switch this test to non-internal once the flag is on by default.
21
+ ReactFeatureFlags.enableHooks = true;
22
+ createRenderer = require('react-test-renderer/shallow').createRenderer;
23
+ React = require('react');
24
+ });
25
+
26
+ it('should work with useState', () => {
27
+ function SomeComponent({defaultName}) {
28
+ const [name] = React.useState(defaultName);
29
+
30
+ return (
31
+ <div>
32
+ <p>
33
+ Your name is: <span>{name}</span>
34
+ </p>
35
+ </div>
36
+ );
37
+ }
38
+
39
+ const shallowRenderer = createRenderer();
40
+ let result = shallowRenderer.render(
41
+ <SomeComponent defaultName={'Dominic'} />,
42
+ );
43
+
44
+ expect(result).toEqual(
45
+ <div>
46
+ <p>
47
+ Your name is: <span>Dominic</span>
48
+ </p>
49
+ </div>,
50
+ );
51
+
52
+ result = shallowRenderer.render(
53
+ <SomeComponent defaultName={'Should not use this name'} />,
54
+ );
55
+
56
+ expect(result).toEqual(
57
+ <div>
58
+ <p>
59
+ Your name is: <span>Dominic</span>
60
+ </p>
61
+ </div>,
62
+ );
63
+ });
64
+
65
+ it('should work with updating a value from useState', () => {
66
+ function SomeComponent({defaultName}) {
67
+ const [name, updateName] = React.useState(defaultName);
68
+
69
+ if (name !== 'Dan') {
70
+ updateName('Dan');
71
+ }
72
+
73
+ return (
74
+ <div>
75
+ <p>
76
+ Your name is: <span>{name}</span>
77
+ </p>
78
+ </div>
79
+ );
80
+ }
81
+
82
+ const shallowRenderer = createRenderer();
83
+ const result = shallowRenderer.render(
84
+ <SomeComponent defaultName={'Dominic'} />,
85
+ );
86
+
87
+ expect(result).toEqual(
88
+ <div>
89
+ <p>
90
+ Your name is: <span>Dan</span>
91
+ </p>
92
+ </div>,
93
+ );
94
+ });
95
+
96
+ it('should work with useReducer', () => {
97
+ const initialState = {count: 0};
98
+
99
+ function reducer(state, action) {
100
+ switch (action.type) {
101
+ case 'reset':
102
+ return initialState;
103
+ case 'increment':
104
+ return {count: state.count + 1};
105
+ case 'decrement':
106
+ return {count: state.count - 1};
107
+ default:
108
+ return state;
109
+ }
110
+ }
111
+
112
+ function SomeComponent({initialCount}) {
113
+ const [state] = React.useReducer(reducer, {count: initialCount});
114
+
115
+ return (
116
+ <div>
117
+ <p>
118
+ The counter is at: <span>{state.count.toString()}</span>
119
+ </p>
120
+ </div>
121
+ );
122
+ }
123
+
124
+ const shallowRenderer = createRenderer();
125
+ let result = shallowRenderer.render(<SomeComponent initialCount={0} />);
126
+
127
+ expect(result).toEqual(
128
+ <div>
129
+ <p>
130
+ The counter is at: <span>0</span>
131
+ </p>
132
+ </div>,
133
+ );
134
+
135
+ result = shallowRenderer.render(<SomeComponent initialCount={10} />);
136
+
137
+ expect(result).toEqual(
138
+ <div>
139
+ <p>
140
+ The counter is at: <span>0</span>
141
+ </p>
142
+ </div>,
143
+ );
144
+ });
145
+
146
+ it('should work with a dispatched state change for a useReducer', () => {
147
+ const initialState = {count: 0};
148
+
149
+ function reducer(state, action) {
150
+ switch (action.type) {
151
+ case 'reset':
152
+ return initialState;
153
+ case 'increment':
154
+ return {count: state.count + 1};
155
+ case 'decrement':
156
+ return {count: state.count - 1};
157
+ default:
158
+ return state;
159
+ }
160
+ }
161
+
162
+ function SomeComponent({initialCount}) {
163
+ const [state, dispatch] = React.useReducer(reducer, {
164
+ count: initialCount,
165
+ });
166
+
167
+ if (state.count === 0) {
168
+ dispatch({type: 'increment'});
169
+ }
170
+
171
+ return (
172
+ <div>
173
+ <p>
174
+ The counter is at: <span>{state.count.toString()}</span>
175
+ </p>
176
+ </div>
177
+ );
178
+ }
179
+
180
+ const shallowRenderer = createRenderer();
181
+ let result = shallowRenderer.render(<SomeComponent initialCount={0} />);
182
+
183
+ expect(result).toEqual(
184
+ <div>
185
+ <p>
186
+ The counter is at: <span>1</span>
187
+ </p>
188
+ </div>,
189
+ );
190
+ });
191
+
192
+ it('should not trigger effects', () => {
193
+ let effectsCalled = [];
194
+
195
+ function SomeComponent({defaultName}) {
196
+ React.useEffect(() => {
197
+ effectsCalled.push('useEffect');
198
+ });
199
+
200
+ React.useLayoutEffect(() => {
201
+ effectsCalled.push('useEffect');
202
+ });
203
+
204
+ return <div>Hello world</div>;
205
+ }
206
+
207
+ const shallowRenderer = createRenderer();
208
+ shallowRenderer.render(<SomeComponent />);
209
+
210
+ expect(effectsCalled).toEqual([]);
211
+ });
212
+
213
+ it('should work with useRef', () => {
214
+ function SomeComponent() {
215
+ const randomNumberRef = React.useRef({number: Math.random()});
216
+
217
+ return (
218
+ <div>
219
+ <p>The random number is: {randomNumberRef.current.number}</p>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ const shallowRenderer = createRenderer();
225
+ let firstResult = shallowRenderer.render(<SomeComponent />);
226
+ let secondResult = shallowRenderer.render(<SomeComponent />);
227
+
228
+ expect(firstResult).toEqual(secondResult);
229
+ });
230
+
231
+ it('should work with useMemo', () => {
232
+ function SomeComponent() {
233
+ const randomNumber = React.useMemo(() => {
234
+ return {number: Math.random()};
235
+ }, []);
236
+
237
+ return (
238
+ <div>
239
+ <p>The random number is: {randomNumber.number}</p>
240
+ </div>
241
+ );
242
+ }
243
+
244
+ const shallowRenderer = createRenderer();
245
+ let firstResult = shallowRenderer.render(<SomeComponent />);
246
+ let secondResult = shallowRenderer.render(<SomeComponent />);
247
+
248
+ expect(firstResult).toEqual(secondResult);
249
+ });
250
+
251
+ it('should work with useContext', () => {
252
+ const SomeContext = React.createContext('default');
253
+
254
+ function SomeComponent() {
255
+ const value = React.useContext(SomeContext);
256
+
257
+ return (
258
+ <div>
259
+ <p>{value}</p>
260
+ </div>
261
+ );
262
+ }
263
+
264
+ const shallowRenderer = createRenderer();
265
+ let result = shallowRenderer.render(<SomeComponent />);
266
+
267
+ expect(result).toEqual(
268
+ <div>
269
+ <p>default</p>
270
+ </div>,
271
+ );
272
+ });
273
+
274
+ it('should not leak state when component type changes', () => {
275
+ function SomeComponent({defaultName}) {
276
+ const [name] = React.useState(defaultName);
277
+
278
+ return (
279
+ <div>
280
+ <p>
281
+ Your name is: <span>{name}</span>
282
+ </p>
283
+ </div>
284
+ );
285
+ }
286
+
287
+ function SomeOtherComponent({defaultName}) {
288
+ const [name] = React.useState(defaultName);
289
+
290
+ return (
291
+ <div>
292
+ <p>
293
+ Your name is: <span>{name}</span>
294
+ </p>
295
+ </div>
296
+ );
297
+ }
298
+
299
+ const shallowRenderer = createRenderer();
300
+ let result = shallowRenderer.render(
301
+ <SomeComponent defaultName={'Dominic'} />,
302
+ );
303
+ expect(result).toEqual(
304
+ <div>
305
+ <p>
306
+ Your name is: <span>Dominic</span>
307
+ </p>
308
+ </div>,
309
+ );
310
+
311
+ result = shallowRenderer.render(<SomeOtherComponent defaultName={'Dan'} />);
312
+ expect(result).toEqual(
313
+ <div>
314
+ <p>
315
+ Your name is: <span>Dan</span>
316
+ </p>
317
+ </div>,
318
+ );
319
+ });
320
+});