| 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 | }); |