main
js 388 lines 11.8 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 describe('forwardRef', () => {
13 let React;
14 let ReactNoop;
15 let waitForAll;
16 let assertConsoleErrorDev;
17
18 beforeEach(() => {
19 jest.resetModules();
20 React = require('react');
21 ReactNoop = require('react-noop-renderer');
22
23 const InternalTestUtils = require('internal-test-utils');
24 waitForAll = InternalTestUtils.waitForAll;
25 assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
26 });
27
28 it('should update refs when switching between children', async () => {
29 function FunctionComponent({forwardedRef, setRefOnDiv}) {
30 return (
31 <section>
32 <div ref={setRefOnDiv ? forwardedRef : null}>First</div>
33 <span ref={setRefOnDiv ? null : forwardedRef}>Second</span>
34 </section>
35 );
36 }
37
38 const RefForwardingComponent = React.forwardRef((props, ref) => (
39 <FunctionComponent {...props} forwardedRef={ref} />
40 ));
41
42 const ref = React.createRef();
43
44 ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={true} />);
45 await waitForAll([]);
46 expect(ref.current.type).toBe('div');
47
48 ReactNoop.render(<RefForwardingComponent ref={ref} setRefOnDiv={false} />);
49 await waitForAll([]);
50 expect(ref.current.type).toBe('span');
51 });
52
53 it('should support rendering null', async () => {
54 const RefForwardingComponent = React.forwardRef((props, ref) => null);
55
56 const ref = React.createRef();
57
58 ReactNoop.render(<RefForwardingComponent ref={ref} />);
59 await waitForAll([]);
60 expect(ref.current).toBe(null);
61 });
62
63 it('should support rendering null for multiple children', async () => {
64 const RefForwardingComponent = React.forwardRef((props, ref) => null);
65
66 const ref = React.createRef();
67
68 ReactNoop.render(
69 <div>
70 <div />
71 <RefForwardingComponent ref={ref} />
72 <div />
73 </div>,
74 );
75 await waitForAll([]);
76 expect(ref.current).toBe(null);
77 });
78
79 it('should warn if not provided a callback during creation', () => {
80 React.forwardRef(undefined);
81 assertConsoleErrorDev([
82 'forwardRef requires a render function but was given undefined.',
83 ]);
84
85 React.forwardRef(null);
86 assertConsoleErrorDev([
87 'forwardRef requires a render function but was given null.',
88 ]);
89
90 React.forwardRef('foo');
91 assertConsoleErrorDev([
92 'forwardRef requires a render function but was given string.',
93 ]);
94 });
95
96 it('should warn if no render function is provided', () => {
97 React.forwardRef();
98 assertConsoleErrorDev([
99 'forwardRef requires a render function but was given undefined.',
100 ]);
101 });
102
103 it('should warn if the render function provided has defaultProps attributes', () => {
104 function renderWithDefaultProps(props, ref) {
105 return null;
106 }
107 renderWithDefaultProps.defaultProps = {};
108
109 React.forwardRef(renderWithDefaultProps);
110 assertConsoleErrorDev([
111 'forwardRef render functions do not support defaultProps. ' +
112 'Did you accidentally pass a React component?',
113 ]);
114 });
115
116 it('should not warn if the render function provided does not use any parameter', () => {
117 React.forwardRef(function arityOfZero() {
118 return <div ref={arguments[1]} />;
119 });
120 });
121
122 it('should warn if the render function provided does not use the forwarded ref parameter', () => {
123 const arityOfOne = props => <div {...props} />;
124
125 React.forwardRef(arityOfOne);
126 assertConsoleErrorDev([
127 'forwardRef render functions accept exactly two parameters: props and ref. ' +
128 'Did you forget to use the ref parameter?',
129 ]);
130 });
131
132 it('should not warn if the render function provided use exactly two parameters', () => {
133 const arityOfTwo = (props, ref) => <div {...props} ref={ref} />;
134 React.forwardRef(arityOfTwo);
135 });
136
137 it('should warn if the render function provided expects to use more than two parameters', () => {
138 const arityOfThree = (props, ref, x) => <div {...props} ref={ref} x={x} />;
139
140 React.forwardRef(arityOfThree);
141 assertConsoleErrorDev([
142 'forwardRef render functions accept exactly two parameters: props and ref. ' +
143 'Any additional parameter will be undefined.',
144 ]);
145 });
146
147 it('should skip forwardRef in the stack if neither displayName nor name are present', async () => {
148 const RefForwardingComponent = React.forwardRef(function (props, ref) {
149 return [<span />];
150 });
151 ReactNoop.render(
152 <p>
153 <RefForwardingComponent />
154 </p>,
155 );
156 await waitForAll([]);
157 assertConsoleErrorDev([
158 'Each child in a list should have a unique "key" prop.' +
159 '\n\nCheck the top-level render call using <ForwardRef>. It was passed a child from ForwardRef. ' +
160 'See https://react.dev/link/warning-keys for more information.\n' +
161 ' in span (at **)\n' +
162 ' in **/forwardRef-test.js:**:** (at **)',
163 ]);
164 });
165
166 it('should use the inner function name for the stack', async () => {
167 const RefForwardingComponent = React.forwardRef(function Inner(props, ref) {
168 return [<span />];
169 });
170 ReactNoop.render(
171 <p>
172 <RefForwardingComponent />
173 </p>,
174 );
175
176 await waitForAll([]);
177 assertConsoleErrorDev([
178 'Each child in a list should have a unique "key" prop.' +
179 '\n\nCheck the top-level render call using <ForwardRef(Inner)>. It was passed a child from ForwardRef(Inner). ' +
180 'See https://react.dev/link/warning-keys for more information.\n' +
181 ' in span (at **)\n' +
182 ' in Inner (at **)',
183 ]);
184 });
185
186 it('should use the inner name in the stack', async () => {
187 const fn = (props, ref) => {
188 return [<span />];
189 };
190 Object.defineProperty(fn, 'name', {value: 'Inner'});
191 const RefForwardingComponent = React.forwardRef(fn);
192 ReactNoop.render(
193 <p>
194 <RefForwardingComponent />
195 </p>,
196 );
197 await waitForAll([]);
198 assertConsoleErrorDev([
199 'Each child in a list should have a unique "key" prop.' +
200 '\n\nCheck the top-level render call using <ForwardRef(Inner)>. It was passed a child from ForwardRef(Inner). ' +
201 'See https://react.dev/link/warning-keys for more information.\n' +
202 ' in span (at **)\n' +
203 ' in Inner (at **)',
204 ]);
205 });
206
207 it('can use the outer displayName in the stack', async () => {
208 const RefForwardingComponent = React.forwardRef((props, ref) => {
209 return [<span />];
210 });
211 RefForwardingComponent.displayName = 'Outer';
212 ReactNoop.render(
213 <p>
214 <RefForwardingComponent />
215 </p>,
216 );
217 await waitForAll([]);
218 assertConsoleErrorDev([
219 'Each child in a list should have a unique "key" prop.' +
220 '\n\nCheck the top-level render call using <Outer>. It was passed a child from Outer. ' +
221 'See https://react.dev/link/warning-keys for more information.\n' +
222 ' in span (at **)\n' +
223 ' in Outer (at **)',
224 ]);
225 });
226
227 it('should prefer the inner name to the outer displayName in the stack', async () => {
228 const fn = (props, ref) => {
229 return [<span />];
230 };
231 Object.defineProperty(fn, 'name', {value: 'Inner'});
232 const RefForwardingComponent = React.forwardRef(fn);
233 RefForwardingComponent.displayName = 'Outer';
234 ReactNoop.render(
235 <p>
236 <RefForwardingComponent />
237 </p>,
238 );
239 await waitForAll([]);
240 assertConsoleErrorDev([
241 'Each child in a list should have a unique "key" prop.' +
242 '\n\nCheck the top-level render call using <Outer>. It was passed a child from Outer. ' +
243 'See https://react.dev/link/warning-keys for more information.\n' +
244 ' in span (at **)\n' +
245 ' in Inner (at **)',
246 ]);
247 });
248
249 it('should not bailout if forwardRef is not wrapped in memo', async () => {
250 const Component = props => <div {...props} />;
251
252 let renderCount = 0;
253
254 const RefForwardingComponent = React.forwardRef((props, ref) => {
255 renderCount++;
256 return <Component {...props} forwardedRef={ref} />;
257 });
258
259 const ref = React.createRef();
260
261 ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />);
262 await waitForAll([]);
263 expect(renderCount).toBe(1);
264
265 ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />);
266 await waitForAll([]);
267 expect(renderCount).toBe(2);
268 });
269
270 it('should bailout if forwardRef is wrapped in memo', async () => {
271 const Component = props => <div ref={props.forwardedRef} />;
272
273 let renderCount = 0;
274
275 const RefForwardingComponent = React.memo(
276 React.forwardRef((props, ref) => {
277 renderCount++;
278 return <Component {...props} forwardedRef={ref} />;
279 }),
280 );
281
282 const ref = React.createRef();
283
284 ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />);
285 await waitForAll([]);
286 expect(renderCount).toBe(1);
287
288 expect(ref.current.type).toBe('div');
289
290 ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />);
291 await waitForAll([]);
292 expect(renderCount).toBe(1);
293
294 const differentRef = React.createRef();
295
296 ReactNoop.render(
297 <RefForwardingComponent ref={differentRef} optional="foo" />,
298 );
299 await waitForAll([]);
300 expect(renderCount).toBe(2);
301
302 expect(ref.current).toBe(null);
303 expect(differentRef.current.type).toBe('div');
304
305 ReactNoop.render(<RefForwardingComponent ref={ref} optional="bar" />);
306 await waitForAll([]);
307 expect(renderCount).toBe(3);
308 });
309
310 it('should custom memo comparisons to compose', async () => {
311 const Component = props => <div ref={props.forwardedRef} />;
312
313 let renderCount = 0;
314
315 const RefForwardingComponent = React.memo(
316 React.forwardRef((props, ref) => {
317 renderCount++;
318 return <Component {...props} forwardedRef={ref} />;
319 }),
320 (o, p) => o.a === p.a && o.b === p.b,
321 );
322
323 const ref = React.createRef();
324
325 ReactNoop.render(<RefForwardingComponent ref={ref} a="0" b="0" c="1" />);
326 await waitForAll([]);
327 expect(renderCount).toBe(1);
328
329 expect(ref.current.type).toBe('div');
330
331 // Changing either a or b rerenders
332 ReactNoop.render(<RefForwardingComponent ref={ref} a="0" b="1" c="1" />);
333 await waitForAll([]);
334 expect(renderCount).toBe(2);
335
336 // Changing c doesn't rerender
337 ReactNoop.render(<RefForwardingComponent ref={ref} a="0" b="1" c="2" />);
338 await waitForAll([]);
339 expect(renderCount).toBe(2);
340
341 const ComposedMemo = React.memo(
342 RefForwardingComponent,
343 (o, p) => o.a === p.a && o.c === p.c,
344 );
345
346 ReactNoop.render(<ComposedMemo ref={ref} a="0" b="0" c="0" />);
347 await waitForAll([]);
348 expect(renderCount).toBe(3);
349
350 // Changing just b no longer updates
351 ReactNoop.render(<ComposedMemo ref={ref} a="0" b="1" c="0" />);
352 await waitForAll([]);
353 expect(renderCount).toBe(3);
354
355 // Changing just a and c updates
356 ReactNoop.render(<ComposedMemo ref={ref} a="2" b="2" c="2" />);
357 await waitForAll([]);
358 expect(renderCount).toBe(4);
359
360 // Changing just c does not update
361 ReactNoop.render(<ComposedMemo ref={ref} a="2" b="2" c="3" />);
362 await waitForAll([]);
363 expect(renderCount).toBe(4);
364
365 // Changing ref still rerenders
366 const differentRef = React.createRef();
367
368 ReactNoop.render(<ComposedMemo ref={differentRef} a="2" b="2" c="3" />);
369 await waitForAll([]);
370 expect(renderCount).toBe(5);
371
372 expect(ref.current).toBe(null);
373 expect(differentRef.current.type).toBe('div');
374 });
375
376 it('warns on forwardRef(memo(...))', () => {
377 React.forwardRef(
378 React.memo((props, ref) => {
379 return null;
380 }),
381 );
382 assertConsoleErrorDev([
383 'forwardRef requires a render function but received a `memo` ' +
384 'component. Instead of forwardRef(memo(...)), use ' +
385 'memo(forwardRef(...)).',
386 ]);
387 });
388 });