179
expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
180
});
181
182
+ it('consistent behavior for reusing props object across different function component types', async () => {
183
+ // This test is a bit complicated because it relates to an
184
+ // implementation detail. We don't have strong guarantees that the props
185
+ // object is referentially equal during updates where we can't bail
186
+ // out anyway — like if the props are shallowly equal, but there's a
187
+ // local state or context update in the same batch.
188
+ //
189
+ // However, as a principle, we should aim to make the behavior
190
+ // consistent across different ways of memoizing a component. For
191
+ // example, React.memo has a different internal Fiber layout if you pass
192
+ // a normal function component (SimpleMemoComponent) versus if you pass
193
+ // a different type like forwardRef (MemoComponent). But this is an
194
+ // implementation detail. Wrapping a component in forwardRef (or
195
+ // React.lazy, etc) shouldn't affect whether the props object is reused
196
+ // during a bailout.
197
+ //
198
+ // So this test isn't primarily about asserting a particular behavior
199
+ // for reusing the props object; it's about making sure the behavior
200
+ // is consistent.
201
+
202
+ const {useEffect, useState} = React;
203
+
204
+ let setSimpleMemoStep;
205
+ const SimpleMemo = React.memo(props => {
206
+ const [step, setStep] = useState(0);
207
+ setSimpleMemoStep = setStep;
208
+
209
+ const prevProps = React.useRef(props);
210
+ useEffect(() => {
211
+ if (props !== prevProps.current) {
212
+ prevProps.current = props;
213
+ Scheduler.unstable_yieldValue('Props changed [SimpleMemo]');
214
+ }
215
+ }, [props]);
216
+
217
+ return <Text text={`SimpleMemo [${props.prop}${step}]`} />;
218
+ });
219
+
220
+ let setComplexMemo;
221
+ const ComplexMemo = React.memo(
222
+ React.forwardRef((props, ref) => {
223
+ const [step, setStep] = useState(0);
224
+ setComplexMemo = setStep;
225
+
226
+ const prevProps = React.useRef(props);
227
+ useEffect(() => {
228
+ if (props !== prevProps.current) {
229
+ prevProps.current = props;
230
+ Scheduler.unstable_yieldValue('Props changed [ComplexMemo]');
231
+ }
232
+ }, [props]);
233
+
234
+ return <Text text={`ComplexMemo [${props.prop}${step}]`} />;
235
+ }),
236
+ );
237
+
238
+ let setMemoWithIndirectionStep;
239
+ const MemoWithIndirection = React.memo(props => {
240
+ return <Indirection props={props} />;
241
+ });
242
+ function Indirection({props}) {
243
+ const [step, setStep] = useState(0);
244
+ setMemoWithIndirectionStep = setStep;
245
+
246
+ const prevProps = React.useRef(props);
247
+ useEffect(() => {
248
+ if (props !== prevProps.current) {
249
+ prevProps.current = props;
250
+ Scheduler.unstable_yieldValue(
251
+ 'Props changed [MemoWithIndirection]',
252
+ );
253
+ }
254
+ }, [props]);
255
+
256
+ return <Text text={`MemoWithIndirection [${props.prop}${step}]`} />;
257
+ }
258
+
259
+ function setLocalUpdateOnChildren(step) {
260
+ setSimpleMemoStep(step);
261
+ setMemoWithIndirectionStep(step);
262
+ setComplexMemo(step);
263
+ }
264
+
265
+ function App({prop}) {
266
+ return (
267
+ <>
268
+ <SimpleMemo prop={prop} />
269
+ <ComplexMemo prop={prop} />
270
+ <MemoWithIndirection prop={prop} />
271
+ </>
272
+ );
273
+ }
274
+
275
+ const root = ReactNoop.createRoot();
276
+ await act(async () => {
277
+ root.render(<App prop="A" />);
278
+ });
279
+ expect(Scheduler).toHaveYielded([
280
+ 'SimpleMemo [A0]',
281
+ 'ComplexMemo [A0]',
282
+ 'MemoWithIndirection [A0]',
283
+ ]);
284
+
285
+ // Demonstrate what happens when the props change
286
+ await act(async () => {
287
+ root.render(<App prop="B" />);
288
+ });
289
+ expect(Scheduler).toHaveYielded([
290
+ 'SimpleMemo [B0]',
291
+ 'ComplexMemo [B0]',
292
+ 'MemoWithIndirection [B0]',
293
+ 'Props changed [SimpleMemo]',
294
+ 'Props changed [ComplexMemo]',
295
+ 'Props changed [MemoWithIndirection]',
296
+ ]);
297
+
298
+ // Demonstrate what happens when the prop object changes but there's a
299
+ // bailout because all the individual props are the same.
300
+ await act(async () => {
301
+ root.render(<App prop="B" />);
302
+ });
303
+ // Nothing re-renders
304
+ expect(Scheduler).toHaveYielded([]);
305
+
306
+ // Demonstrate what happens when the prop object changes, it bails out
307
+ // because all the props are the same, but we still render the
308
+ // children because there's a local update in the same batch.
309
+ await act(async () => {
310
+ root.render(<App prop="B" />);
311
+ setLocalUpdateOnChildren(1);
312
+ });
313
+ // The components should re-render with the new local state, but none
314
+ // of the props objects should have changed
315
+ expect(Scheduler).toHaveYielded([
316
+ 'SimpleMemo [B1]',
317
+ 'ComplexMemo [B1]',
318
+ 'MemoWithIndirection [B1]',
319
+ ]);
320
+
321
+ // Do the same thing again. We should still reuse the props object.
322
+ await act(async () => {
323
+ root.render(<App prop="B" />);
324
+ setLocalUpdateOnChildren(2);
325
+ });
326
+ // The components should re-render with the new local state, but none
327
+ // of the props objects should have changed
328
+ expect(Scheduler).toHaveYielded([
329
+ 'SimpleMemo [B2]',
330
+ 'ComplexMemo [B2]',
331
+ 'MemoWithIndirection [B2]',
332
+ ]);
333
+ });
334
+
335
it('accepts custom comparison function', async () => {
336
function Counter({count}) {
337
return <Text text={count} />;