Skip to content
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

Fix cloneElement using string ref w no owner #28797

Merged
merged 8 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions packages/react/src/__tests__/ReactElementClone-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,55 @@ describe('ReactElementClone', () => {

const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => root.render(<Grandparent />));
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
if (gate(flags => flags.enableRefAsProp && flags.disableStringRefs)) {
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
} else if (
gate(flags => !flags.enableRefAsProp && !flags.disableStringRefs)
) {
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
} else if (
gate(flags => flags.enableRefAsProp && !flags.disableStringRefs)
) {
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
Comment on lines +286 to +289
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm pretty sure this isn't running...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, not by default. thanks CI

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, it's getting hit, you have to run with the www variant:

yarn test-www --variant=true

Tested locally:

Screenshot 2024-04-09 at 3 01 39 PM

} else {
// Not going to bother testing every possible combination.
}
});

it('should steal the ref if a new string ref is specified', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dupe test name

Suggested change
it('should steal the ref if a new string ref is specified', async () => {
it('should steal the ref if a new string ref is specified without an owner', async () => {

if (gate(flags => !flags.enableRefAsProp && !flags.disableStringRefs)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of inlining this, you can add this above the test:

// @gate !flags.enableRefAsProp && !flags.disableStringRefs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, even fixing this to remove the "flags." from each flag, it causes failures in all the other flag combinations, which expect the test to fail

await expect(async () => {
// create an element without an owner
const element = React.createElement('div', {id: 'some-id'});
class Parent extends React.Component {
render() {
return <Child>{element}</Child>;
}
}
let child;
class Child extends React.Component {
render() {
child = this;
const clone = React.cloneElement(this.props.children, {
ref: 'xyz',
});
return <div>{clone}</div>;
}
}

const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => root.render(<Parent />));
expect(child.refs.xyz.tagName).toBe('DIV');
}).toErrorDev([
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before the fix this failed, hitting the case where there was no owner during string ref conversion

'Warning: Component "Child" contains the string ref "xyz". Support for ' +
'string refs will be removed in a future major release. We recommend ' +
'using useRef() or createRef() instead. Learn more about using refs ' +
'safely here: https://react.dev/link/strict-mode-string-ref',
]);
}
});

it('should overwrite props', async () => {
Expand Down Expand Up @@ -371,6 +418,15 @@ describe('ReactElementClone', () => {
) {
expect(clone.ref).toBe(element.ref);
expect(clone.props).toEqual({foo: 'ef'});
} else if (
gate(flags => flags.enableRefAsProp && !flags.disableStringRefs)
) {
expect(() => {
expect(clone.ref).toBe(element.ref);
}).toErrorDev('Accessing element.ref was removed in React 19', {
withoutStack: true,
});
expect(clone.props).toEqual({foo: 'ef', ref: element.ref});
Copy link
Member

@rickhanlonii rickhanlonii Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josephsavona since enableRefAsProp is on, the props will include the ref, so I updated the test.

} else {
// Not going to bother testing every possible combination.
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/jsx/ReactJSXElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -866,14 +866,14 @@ export function cloneElement(element, config, children) {

if (config != null) {
if (hasValidRef(config)) {
owner = ReactSharedInternals.owner;
if (!enableRefAsProp) {
// Silently steal the ref from the parent.
ref = config.ref;
if (!disableStringRefs) {
ref = coerceStringRef(ref, owner, element.type);
}
}
owner = ReactSharedInternals.owner;
}
if (hasValidKey(config)) {
if (__DEV__) {
Expand Down