@@ -179,6 +179,159 @@ describe('memo', () => {
179
179
expect ( ReactNoop . getChildren ( ) ) . toEqual ( [ span ( 'Count: 1' ) ] ) ;
180
180
} ) ;
181
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
+
182
335
it ( 'accepts custom comparison function' , async ( ) => {
183
336
function Counter ( { count} ) {
184
337
return < Text text = { count } /> ;
0 commit comments