| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and 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 | */ |
| 9 | |
| 10 | 'use strict'; |
| 11 | |
| 12 | let React; |
| 13 | let Scheduler; |
| 14 | let ReactNoop; |
| 15 | let act; |
| 16 | let assertLog; |
| 17 | |
| 18 | describe('ReactFiberRefs', () => { |
| 19 | beforeEach(() => { |
| 20 | jest.resetModules(); |
| 21 | React = require('react'); |
| 22 | Scheduler = require('scheduler'); |
| 23 | ReactNoop = require('react-noop-renderer'); |
| 24 | act = require('internal-test-utils').act; |
| 25 | assertLog = require('internal-test-utils').assertLog; |
| 26 | }); |
| 27 | |
| 28 | it('ref is attached even if there are no other updates (class)', async () => { |
| 29 | let component; |
| 30 | class Component extends React.Component { |
| 31 | shouldComponentUpdate() { |
| 32 | // This component's output doesn't depend on any props or state |
| 33 | return false; |
| 34 | } |
| 35 | render() { |
| 36 | Scheduler.log('Render'); |
| 37 | component = this; |
| 38 | return 'Hi'; |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | const ref1 = React.createRef(); |
| 43 | const ref2 = React.createRef(); |
| 44 | const root = ReactNoop.createRoot(); |
| 45 | |
| 46 | // Mount with ref1 attached |
| 47 | await act(() => root.render(<Component ref={ref1} />)); |
| 48 | assertLog(['Render']); |
| 49 | expect(root).toMatchRenderedOutput('Hi'); |
| 50 | expect(ref1.current).toBe(component); |
| 51 | // ref2 has no value |
| 52 | expect(ref2.current).toBe(null); |
| 53 | |
| 54 | // Switch to ref2, but don't update anything else. |
| 55 | await act(() => root.render(<Component ref={ref2} />)); |
| 56 | // The component did not re-render because no props changed. |
| 57 | assertLog([]); |
| 58 | expect(root).toMatchRenderedOutput('Hi'); |
| 59 | // But the refs still should have been swapped. |
| 60 | expect(ref1.current).toBe(null); |
| 61 | expect(ref2.current).toBe(component); |
| 62 | }); |
| 63 | |
| 64 | it('ref is attached even if there are no other updates (host component)', async () => { |
| 65 | // This is kind of ailly test because host components never bail out if they |
| 66 | // receive a new element, and there's no way to update a ref without also |
| 67 | // updating the props, but adding it here anyway for symmetry with the |
| 68 | // class case above. |
| 69 | const ref1 = React.createRef(); |
| 70 | const ref2 = React.createRef(); |
| 71 | const root = ReactNoop.createRoot(); |
| 72 | |
| 73 | // Mount with ref1 attached |
| 74 | await act(() => root.render(<div ref={ref1}>Hi</div>)); |
| 75 | expect(root).toMatchRenderedOutput(<div>Hi</div>); |
| 76 | expect(ref1.current).not.toBe(null); |
| 77 | // ref2 has no value |
| 78 | expect(ref2.current).toBe(null); |
| 79 | |
| 80 | // Switch to ref2, but don't update anything else. |
| 81 | await act(() => root.render(<div ref={ref2}>Hi</div>)); |
| 82 | expect(root).toMatchRenderedOutput(<div>Hi</div>); |
| 83 | // But the refs still should have been swapped. |
| 84 | expect(ref1.current).toBe(null); |
| 85 | expect(ref2.current).not.toBe(null); |
| 86 | }); |
| 87 | |
| 88 | it('throw if a string ref is passed to a ref-receiving component', async () => { |
| 89 | let refProp; |
| 90 | function Child({ref}) { |
| 91 | // This component renders successfully because the ref type check does not |
| 92 | // occur until you pass it to a component that accepts refs. |
| 93 | // |
| 94 | // So the div will throw, but not Child. |
| 95 | refProp = ref; |
| 96 | return <div ref={ref} />; |
| 97 | } |
| 98 | |
| 99 | class Owner extends React.Component { |
| 100 | render() { |
| 101 | return <Child ref="child" />; |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | const root = ReactNoop.createRoot(); |
| 106 | await expect(act(() => root.render(<Owner />))).rejects.toThrow( |
| 107 | 'Expected ref to be a function', |
| 108 | ); |
| 109 | expect(refProp).toBe('child'); |
| 110 | }); |
| 111 | |
| 112 | it('strings refs can be codemodded to callback refs', async () => { |
| 113 | let app; |
| 114 | class App extends React.Component { |
| 115 | render() { |
| 116 | app = this; |
| 117 | return ( |
| 118 | <div |
| 119 | prop="Hello!" |
| 120 | ref={el => { |
| 121 | // `refs` used to be a shared frozen object unless/until a string |
| 122 | // ref attached by the reconciler, but it's not anymore so that we |
| 123 | // can codemod string refs to userspace callback refs. |
| 124 | this.refs.div = el; |
| 125 | }} |
| 126 | /> |
| 127 | ); |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | const root = ReactNoop.createRoot(); |
| 132 | await act(() => root.render(<App />)); |
| 133 | expect(app.refs.div.prop).toBe('Hello!'); |
| 134 | }); |
| 135 | |
| 136 | it('class refs are initialized to a frozen shared object', async () => { |
| 137 | const refsCollection = new Set(); |
| 138 | class Component extends React.Component { |
| 139 | constructor(props) { |
| 140 | super(props); |
| 141 | refsCollection.add(this.refs); |
| 142 | } |
| 143 | render() { |
| 144 | return <div />; |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | const root = ReactNoop.createRoot(); |
| 149 | await act(() => |
| 150 | root.render( |
| 151 | <> |
| 152 | <Component /> |
| 153 | <Component /> |
| 154 | </>, |
| 155 | ), |
| 156 | ); |
| 157 | |
| 158 | expect(refsCollection.size).toBe(1); |
| 159 | const refsInstance = Array.from(refsCollection)[0]; |
| 160 | expect(Object.isFrozen(refsInstance)).toBe(__DEV__); |
| 161 | }); |
| 162 | }); |