main
js 162 lines 4.72 KB
Raw
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 });