-
Notifications
You must be signed in to change notification settings - Fork 47k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC #30: React.forwardRef implementation #12346
Changes from 7 commits
9bd0fdb
753dba7
56138d2
60efe32
166d236
0d565b5
508d52e
d5a196d
3583bf6
7c3d823
2661583
6185838
6d7d1ef
5321365
557cd79
04ec720
1b1deda
b91b3a3
c038d89
5b5b625
2a7b3c8
33bb141
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
/** | ||
* Copyright (c) 2013-present, Facebook, Inc. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @emails react-core | ||
*/ | ||
|
||
'use strict'; | ||
|
||
describe('forwardRef', () => { | ||
let React; | ||
let ReactFeatureFlags; | ||
let ReactNoop; | ||
|
||
function normalizeCodeLocInfo(str) { | ||
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)'); | ||
} | ||
|
||
beforeEach(() => { | ||
jest.resetModules(); | ||
ReactFeatureFlags = require('shared/ReactFeatureFlags'); | ||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this necessary for this test? If not you can remove the internal file name suffix. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without it, the number of yields vary between dev and prod :( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it. Okay. |
||
React = require('react'); | ||
ReactNoop = require('react-noop-renderer'); | ||
}); | ||
|
||
it('should work without a ref to be forwarded', () => { | ||
class Child extends React.Component { | ||
render() { | ||
ReactNoop.yield(this.props.value); | ||
return null; | ||
} | ||
} | ||
|
||
function Wrapper(props) { | ||
return <Child {...props} ref={props.forwardedRef} />; | ||
} | ||
|
||
const RefForwardingComponent = React.forwardRef((props, ref) => ( | ||
<Wrapper {...props} forwardedRef={ref} /> | ||
)); | ||
|
||
ReactNoop.render(<RefForwardingComponent value={123} />); | ||
expect(ReactNoop.flush()).toEqual([123]); | ||
}); | ||
|
||
it('should forward a ref to a chosen component', () => { | ||
class Child extends React.Component { | ||
render() { | ||
ReactNoop.yield(this.props.value); | ||
return null; | ||
} | ||
} | ||
|
||
function Wrapper(props) { | ||
return <Child {...props} ref={props.forwardedRef} />; | ||
} | ||
|
||
const RefForwardingComponent = React.forwardRef((props, ref) => ( | ||
<Wrapper {...props} forwardedRef={ref} /> | ||
)); | ||
|
||
const ref = React.createRef(); | ||
|
||
ReactNoop.render(<RefForwardingComponent ref={ref} value={123} />); | ||
expect(ReactNoop.flush()).toEqual([123]); | ||
expect(ref.value instanceof Child).toBe(true); | ||
}); | ||
|
||
it('should maintain ref through updates', () => { | ||
class Child extends React.Component { | ||
render() { | ||
ReactNoop.yield(this.props.value); | ||
return null; | ||
} | ||
} | ||
|
||
function Wrapper(props) { | ||
return <Child {...props} ref={props.forwardedRef} />; | ||
} | ||
|
||
const RefForwardingComponent = React.forwardRef((props, ref) => ( | ||
<Wrapper {...props} forwardedRef={ref} /> | ||
)); | ||
|
||
let setRefCount = 0; | ||
let ref; | ||
|
||
const setRef = r => { | ||
setRefCount++; | ||
ref = r; | ||
}; | ||
|
||
ReactNoop.render(<RefForwardingComponent ref={setRef} value={123} />); | ||
expect(ReactNoop.flush()).toEqual([123]); | ||
expect(ref instanceof Child).toBe(true); | ||
expect(setRefCount).toBe(1); | ||
ReactNoop.render(<RefForwardingComponent ref={setRef} value={456} />); | ||
expect(ReactNoop.flush()).toEqual([456]); | ||
expect(ref instanceof Child).toBe(true); | ||
expect(setRefCount).toBe(1); | ||
}); | ||
|
||
it('should not break lifecycle error handling', () => { | ||
class ErrorBoundary extends React.Component { | ||
state = {error: null}; | ||
componentDidCatch(error) { | ||
ReactNoop.yield('ErrorBoundary.componentDidCatch'); | ||
this.setState({error}); | ||
} | ||
render() { | ||
if (this.state.error) { | ||
ReactNoop.yield('ErrorBoundary.render: catch'); | ||
return null; | ||
} | ||
ReactNoop.yield('ErrorBoundary.render: try'); | ||
return this.props.children; | ||
} | ||
} | ||
|
||
class BadRender extends React.Component { | ||
render() { | ||
ReactNoop.yield('BadRender throw'); | ||
throw new Error('oops!'); | ||
} | ||
} | ||
|
||
function Wrapper(props) { | ||
ReactNoop.yield('Wrapper'); | ||
return <BadRender {...props} ref={props.forwardedRef} />; | ||
} | ||
|
||
const RefForwardingComponent = React.forwardRef((props, ref) => ( | ||
<Wrapper {...props} forwardedRef={ref} /> | ||
)); | ||
|
||
const ref = React.createRef(); | ||
|
||
ReactNoop.render( | ||
<ErrorBoundary> | ||
<RefForwardingComponent ref={ref} /> | ||
</ErrorBoundary>, | ||
); | ||
expect(ReactNoop.flush()).toEqual([ | ||
'ErrorBoundary.render: try', | ||
'Wrapper', | ||
'BadRender throw', | ||
'ErrorBoundary.componentDidCatch', | ||
'ErrorBoundary.render: catch', | ||
]); | ||
expect(ref.value).toBe(null); | ||
}); | ||
|
||
it('should support rendering null', () => { | ||
const RefForwardingComponent = React.forwardRef((props, ref) => null); | ||
|
||
const ref = React.createRef(); | ||
|
||
ReactNoop.render(<RefForwardingComponent ref={ref} />); | ||
ReactNoop.flush(); | ||
expect(ref.value).toBe(null); | ||
}); | ||
|
||
it('should warn if not provided a callback during creation', () => { | ||
expect(() => React.forwardRef(undefined)).toWarnDev( | ||
'forwardRef requires a render function but was given undefined.', | ||
); | ||
expect(() => React.forwardRef(null)).toWarnDev( | ||
'forwardRef requires a render function but was given null.', | ||
); | ||
expect(() => React.forwardRef('foo')).toWarnDev( | ||
'forwardRef requires a render function but was given string.', | ||
); | ||
}); | ||
|
||
it('should error with a callstack if rendered without a function', () => { | ||
let RefForwardingComponent; | ||
expect(() => { | ||
RefForwardingComponent = React.forwardRef(); | ||
}).toWarnDev( | ||
'forwardRef requires a render function but was given undefined.', | ||
); | ||
|
||
ReactNoop.render( | ||
<div> | ||
<RefForwardingComponent /> | ||
</div>, | ||
); | ||
|
||
let caughtError; | ||
try { | ||
ReactNoop.flush(); | ||
} catch (error) { | ||
caughtError = error; | ||
} | ||
expect(caughtError).toBeDefined(); | ||
expect(normalizeCodeLocInfo(caughtError.message)).toBe( | ||
'forwardRef requires a render function but was given undefined.' + | ||
(__DEV__ ? '\n in div (at **)' : ''), | ||
); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/** | ||
* Copyright (c) 2014-present, Facebook, Inc. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import {REACT_USE_REF_TYPE} from 'shared/ReactSymbols'; | ||
|
||
import warning from 'fbjs/lib/warning'; | ||
|
||
export default function forwardRef<Props, ElementType: React$ElementType>( | ||
renderProp: (props: Props, ref: React$ElementRef<ElementType>) => React$Node, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not really a render prop since it's not passed at render time nor a prop. It's more like, just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion. I like |
||
) { | ||
warning( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
typeof renderProp === 'function', | ||
'forwardRef requires a render function but was given %s.', | ||
renderProp === null ? 'null' : typeof renderProp, | ||
); | ||
|
||
return { | ||
$$typeof: REACT_USE_REF_TYPE, | ||
renderProp, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow why are these things so easy with Fiber
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know! 😁