@@ -386,6 +386,156 @@ describe('useOnyx', () => {
386386 expect ( result . current [ 0 ] ) . toEqual ( 'id - test_id, name - test_name - selector changed' ) ;
387387 expect ( result . current [ 1 ] . status ) . toEqual ( 'loaded' ) ;
388388 } ) ;
389+
390+ it ( 'should memoize selector output and return same reference when input unchanged' , async ( ) => {
391+ Onyx . set ( ONYXKEYS . TEST_KEY , { id : 'test_id' , name : 'test_name' , count : 1 } ) ;
392+
393+ const { result} = renderHook ( ( ) =>
394+ useOnyx ( ONYXKEYS . TEST_KEY , {
395+ // @ts -expect-error bypass
396+ selector : ( entry : OnyxEntry < { id : string ; name : string ; count : number } > ) => ( {
397+ id : entry ?. id ,
398+ name : entry ?. name ,
399+ } ) ,
400+ } ) ,
401+ ) ;
402+
403+ await act ( async ( ) => waitForPromisesToResolve ( ) ) ;
404+
405+ const firstResult = result . current [ 0 ] ;
406+
407+ // Trigger another render without changing the data
408+ await act ( async ( ) => waitForPromisesToResolve ( ) ) ;
409+
410+ // Should return the exact same reference due to memoization
411+ expect ( result . current [ 0 ] ) . toBe ( firstResult ) ;
412+ } ) ;
413+
414+ it ( 'should return new reference when selector input changes' , async ( ) => {
415+ Onyx . set ( ONYXKEYS . TEST_KEY , { id : 'test_id' , name : 'test_name' } ) ;
416+
417+ const { result} = renderHook ( ( ) =>
418+ useOnyx ( ONYXKEYS . TEST_KEY , {
419+ // @ts -expect-error bypass
420+ selector : ( entry : OnyxEntry < { id : string ; name : string } > ) => ( {
421+ id : entry ?. id ,
422+ name : entry ?. name ,
423+ } ) ,
424+ } ) ,
425+ ) ;
426+
427+ await act ( async ( ) => waitForPromisesToResolve ( ) ) ;
428+
429+ const firstResult = result . current [ 0 ] ;
430+
431+ // Change the data
432+ await act ( async ( ) => Onyx . merge ( ONYXKEYS . TEST_KEY , { id : 'changed_id' } ) ) ;
433+
434+ // Should return a new reference since data changed
435+ expect ( result . current [ 0 ] ) . not . toBe ( firstResult ) ;
436+ expect ( result . current [ 0 ] ) . toEqual ( { id : 'changed_id' , name : 'test_name' } ) ;
437+ } ) ;
438+
439+ it ( 'should memoize selector output using deep equality check' , async ( ) => {
440+ let selectorCallCount = 0 ;
441+
442+ Onyx . set ( ONYXKEYS . TEST_KEY , { id : 'test_id' , name : 'test_name' } ) ;
443+
444+ const { result} = renderHook ( ( ) =>
445+ useOnyx ( ONYXKEYS . TEST_KEY , {
446+ // @ts -expect-error bypass
447+ selector : ( entry : OnyxEntry < { id : string ; name : string } > ) => {
448+ selectorCallCount ++ ;
449+ return { id : entry ?. id , name : entry ?. name } ;
450+ } ,
451+ } ) ,
452+ ) ;
453+
454+ await act ( async ( ) => waitForPromisesToResolve ( ) ) ;
455+
456+ const firstResult = result . current [ 0 ] ;
457+ const initialCallCount = selectorCallCount ;
458+
459+ // Add a property that will change object reference but keep selected data same
460+ await act ( async ( ) => Onyx . merge ( ONYXKEYS . TEST_KEY , { extraProp : 'new' } ) ) ;
461+
462+ // Selector should be called again due to input object reference change
463+ expect ( selectorCallCount ) . toBe ( initialCallCount + 1 ) ;
464+ // But output should be the same reference due to deep equality check in memoized selector
465+ expect ( result . current [ 0 ] ) . toBe ( firstResult ) ;
466+ } ) ;
467+
468+ it ( 'should use reference equality for memoized selectors instead of deep equality' , async ( ) => {
469+ // This test verifies the optimization where memoized selectors use reference equality
470+ const complexObject = {
471+ nested : {
472+ array : [ 1 , 2 , 3 ] ,
473+ object : { prop : 'value' } ,
474+ } ,
475+ } ;
476+
477+ Onyx . set ( ONYXKEYS . TEST_KEY , complexObject ) ;
478+
479+ const { result} = renderHook ( ( ) =>
480+ useOnyx ( ONYXKEYS . TEST_KEY , {
481+ // @ts -expect-error bypass
482+ selector : ( entry : OnyxEntry < typeof complexObject > ) => entry ?. nested ,
483+ } ) ,
484+ ) ;
485+
486+ await act ( async ( ) => waitForPromisesToResolve ( ) ) ;
487+
488+ const firstResult = result . current [ 0 ] ;
489+
490+ // Set the exact same object reference
491+ await act ( async ( ) => Onyx . set ( ONYXKEYS . TEST_KEY , complexObject ) ) ;
492+
493+ // Should return same reference due to memoization
494+ expect ( result . current [ 0 ] ) . toBe ( firstResult ) ;
495+
496+ // Set different object with same content
497+ const differentObjectSameContent = {
498+ nested : {
499+ array : [ 1 , 2 , 3 ] ,
500+ object : { prop : 'value' } ,
501+ } ,
502+ } ;
503+
504+ await act ( async ( ) => Onyx . set ( ONYXKEYS . TEST_KEY , differentObjectSameContent ) ) ;
505+
506+ // Should return same reference due to deep equality in memoized selector
507+ expect ( result . current [ 0 ] ) . toBe ( firstResult ) ;
508+ } ) ;
509+
510+ it ( 'should memoize primitive selector results correctly' , async ( ) => {
511+ Onyx . set ( ONYXKEYS . TEST_KEY , { count : 5 , name : 'test' } ) ;
512+
513+ const { result} = renderHook ( ( ) =>
514+ useOnyx ( ONYXKEYS . TEST_KEY , {
515+ // @ts -expect-error bypass
516+ selector : ( entry : OnyxEntry < { count : number ; name : string } > ) => entry ?. count || 0 ,
517+ } ) ,
518+ ) ;
519+
520+ await act ( async ( ) => waitForPromisesToResolve ( ) ) ;
521+
522+ const firstResult = result . current [ 0 ] ;
523+ expect ( firstResult ) . toBe ( 5 ) ;
524+
525+ // Change unrelated property
526+ await act ( async ( ) => Onyx . merge ( ONYXKEYS . TEST_KEY , { name : 'changed' } ) ) ;
527+
528+ // Should return the same primitive value (number 5)
529+ expect ( result . current [ 0 ] ) . toBe ( firstResult ) ;
530+ expect ( result . current [ 0 ] ) . toBe ( 5 ) ;
531+
532+ // Change the selected property
533+ await act ( async ( ) => Onyx . merge ( ONYXKEYS . TEST_KEY , { count : 10 } ) ) ;
534+
535+ // Should return new value
536+ expect ( result . current [ 0 ] ) . not . toBe ( firstResult ) ;
537+ expect ( result . current [ 0 ] ) . toBe ( 10 ) ;
538+ } ) ;
389539 } ) ;
390540
391541 describe ( 'allowStaleData' , ( ) => {
0 commit comments