Add ref callback test for cleanup fn vs null call (#28895)
Used this test scenario to clarify how callback refs work when detached based on the availability of a cleanup function to update documentation in https://github.com/reactjs/react.dev/pull/6770 Checking it in for additional test coverage and test-based documentation
Jack Pope committed
Apr 23, 2024 at 12:13 UTC
d4e78c42a94be027b4dc7ed2659a5fddfbf9bd4e
1 file changed
+97
packages/react-dom/src/__tests__/refs-test.js
+97
@@ -706,4 +706,101 @@ describe('refs return clean up function', () => {
706
expect(setup).toHaveBeenCalledTimes(1);
707
expect(cleanUp).toHaveBeenCalledTimes(1);
708
});
709
+
710
+ it('handles detaching refs with either cleanup function or null argument', async () => {
711
+ const container = document.createElement('div');
712
+ const cleanUp = jest.fn();
713
+ const setup = jest.fn();
714
+ const setup2 = jest.fn();
715
+ const nullHandler = jest.fn();
716
+
717
+ function _onRefChangeWithCleanup(_ref) {
718
+ if (_ref) {
719
+ setup(_ref.id);
720
+ } else {
721
+ nullHandler();
722
+ }
723
+ return cleanUp;
724
+ }
725
+
726
+ function _onRefChangeWithoutCleanup(_ref) {
727
+ if (_ref) {
728
+ setup2(_ref.id);
729
+ } else {
730
+ nullHandler();
731
+ }
732
+ }
733
+
734
+ const root = ReactDOMClient.createRoot(container);
735
+ await act(() => {
736
+ root.render(<div id="test-div" ref={_onRefChangeWithCleanup} />);
737
+ });
738
+
739
+ expect(setup).toBeCalledWith('test-div');
740
+ expect(setup).toHaveBeenCalledTimes(1);
741
+ expect(cleanUp).toHaveBeenCalledTimes(0);
742
+
743
+ await act(() => {
744
+ root.render(<div id="test-div2" ref={_onRefChangeWithoutCleanup} />);
745
+ });
746
+
747
+ // Existing setup call was not called again
748
+ expect(setup).toHaveBeenCalledTimes(1);
749
+ // No null call because cleanup is returned
750
+ expect(nullHandler).toHaveBeenCalledTimes(0);
751
+ // Now we have a cleanup
752
+ expect(cleanUp).toHaveBeenCalledTimes(1);
753
+
754
+ // New ref is setup
755
+ expect(setup2).toBeCalledWith('test-div2');
756
+ expect(setup2).toHaveBeenCalledTimes(1);
757
+
758
+ // Now, render with the original ref again
759
+ await act(() => {
760
+ root.render(<div id="test-div3" ref={_onRefChangeWithCleanup} />);
761
+ });
762
+
763
+ // Setup was not called again
764
+ expect(setup2).toBeCalledWith('test-div2');
765
+ expect(setup2).toHaveBeenCalledTimes(1);
766
+
767
+ // Null handler hit because no cleanup is returned
768
+ expect(nullHandler).toHaveBeenCalledTimes(1);
769
+
770
+ // Original setup hit one more time
771
+ expect(setup).toHaveBeenCalledTimes(2);
772
+ });
773
+
774
+ it('calls cleanup function on unmount', async () => {
775
+ const container = document.createElement('div');
776
+ const cleanUp = jest.fn();
777
+ const setup = jest.fn();
778
+ const nullHandler = jest.fn();
779
+
780
+ function _onRefChangeWithCleanup(_ref) {
781
+ if (_ref) {
782
+ setup(_ref.id);
783
+ } else {
784
+ nullHandler();
785
+ }
786
+ return cleanUp;
787
+ }
788
+
789
+ const root = ReactDOMClient.createRoot(container);
790
+ await act(() => {
791
+ root.render(<div id="test-div" ref={_onRefChangeWithCleanup} />);
792
+ });
793
+
794
+ expect(setup).toHaveBeenCalledTimes(1);
795
+ expect(cleanUp).toHaveBeenCalledTimes(0);
796
+ expect(nullHandler).toHaveBeenCalledTimes(0);
797
+
798
+ root.unmount();
799
+
800
+ expect(setup).toHaveBeenCalledTimes(1);
801
+ // Now cleanup has been called
802
+ expect(cleanUp).toHaveBeenCalledTimes(1);
803
+ // Ref callback never called with null when cleanup is returned
804
+ expect(nullHandler).toHaveBeenCalledTimes(0);
805
+ });
806
});