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

React 19 compatibility #2934

Merged
merged 27 commits into from
Jun 10, 2024
Merged

React 19 compatibility #2934

merged 27 commits into from
Jun 10, 2024

Conversation

vladmoroz
Copy link
Contributor

@vladmoroz vladmoroz commented Jun 4, 2024

  • Access ref from props when possible
  • Remove propTypes usage
  • Replace our ComponentPropsWithoutRef type with the built-in React type

Handy link:
https://react.dev/blog/2024/04/25/react-19-upgrade-guide

Comment on lines -62 to -83
Accordion.propTypes = {
type(props) {
const value = props.value || props.defaultValue;
if (props.type && !['single', 'multiple'].includes(props.type)) {
return new Error(
'Invalid prop `type` supplied to `Accordion`. Expected one of `single | multiple`.'
);
}
if (props.type === 'multiple' && typeof value === 'string') {
return new Error(
'Invalid prop `type` supplied to `Accordion`. Expected `single` when `defaultValue` or `value` is type `string`.'
);
}
if (props.type === 'single' && Array.isArray(value)) {
return new Error(
'Invalid prop `type` supplied to `Accordion`. Expected `multiple` when `defaultValue` or `value` is type `string[]`.'
);
}
return null;
},
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Prop types have been deprecated for a while and are now completely removed
https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-deprecated-react-apis

Using TypeScript already handles this

Comment on lines +40 to +49
if ((maxProp || maxProp === 0) && !isValidMaxNumber(maxProp)) {
console.error(getInvalidMaxError(`${maxProp}`, 'Progress'));
}

const max = isValidMaxNumber(maxProp) ? maxProp : DEFAULT_MAX;

if (valueProp !== null && !isValidValueNumber(valueProp, max)) {
console.error(getInvalidValueError(`${valueProp}`, 'Progress'));
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relocated the logic from propTypes that TypeScript can't help with

Comment on lines -50 to -71
Separator.propTypes = {
orientation(props, propName, componentName) {
const propValue = props[propName];
const strVal = String(propValue);
if (propValue && !isValidOrientation(propValue)) {
return new Error(getInvalidOrientationError(strVal, componentName));
}
return null;
},
};

/* -----------------------------------------------------------------------------------------------*/

// Split this out for clearer readability of the error message.
function getInvalidOrientationError(value: string, componentName: string) {
return `Invalid prop \`orientation\` of value \`${value}\` supplied to \`${componentName}\`, expected one of:
- horizontal
- vertical

Defaulting to \`${DEFAULT_ORIENTATION}\`.`;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TypeScript already handles this

Comment on lines +84 to +90

if (!label.trim()) {
console.error(
`Invalid prop \`label\` supplied to \`${PROVIDER_NAME}\`. Expected non-empty \`string\`.`
);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relocated from propTypes

Comment on lines +762 to +769

if (!altText.trim()) {
console.error(
`Invalid prop \`altText\` supplied to \`${ACTION_NAME}\`. Expected non-empty \`string\`.`
);
return null;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Relocated from propTypes

Comment on lines 146 to 164
// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`
// After React 19 accessing `element.ref` does the opposite
// https://github.com/facebook/react/pull/28348
//
// Access the ref using the method that doesn't yield a warning
function getElementRef(element: React.ReactElement) {
// Pre React 19 there's a getter on `element.props.ref` that throws a warning when attempting to access it.
// This is safe to rely on. (As in... obviously, old React versions won't change).
// https://github.com/facebook/react/blob/408258268edb5acdfdbf77bc6e0b0dc6396c0e6f/packages/react/src/jsx/ReactJSXElement.js#L89-L99
const getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;
const hasPropWarning = getter && 'isReactWarning' in getter && getter.isReactWarning;

if (hasPropWarning) {
return (element as any).ref;
}

return element.props.ref;
}

Copy link
Contributor Author

@vladmoroz vladmoroz Jun 4, 2024

Choose a reason for hiding this comment

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

Ref access instance 1 of 2

A bit messy checking for the getter presence, but a naïve attempt to access either element.props.ref or element.ref directly like in #2811 yields a warning pre React 19

Comment on lines -24 to -32
// Temporary while we await merge of this fix:
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/55396
// prettier-ignore
type PropsWithoutRef<P> = P extends any ? ('ref' extends keyof P ? Pick<P, Exclude<keyof P, 'ref'>> : P) : P;
type ComponentPropsWithoutRef<T extends React.ElementType> = PropsWithoutRef<
React.ComponentProps<T>
>;

type Primitives = { [E in typeof NODES[number]]: PrimitiveForwardRefComponent<E> };
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This stuff is integrated into @types/react

Comment on lines 126 to 144
// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`
// After React 19 accessing `element.ref` does the opposite
// https://github.com/facebook/react/pull/28348
//
// Access the ref using the method that doesn't yield a warning
function getElementRef(element: React.ReactElement) {
// Pre React 19 there's a getter on `element.props.ref` that throws a warning when attempting to access it.
// This is safe to rely on. (As in... obviously, old React versions won't change).
// https://github.com/facebook/react/blob/408258268edb5acdfdbf77bc6e0b0dc6396c0e6f/packages/react/src/jsx/ReactJSXElement.js#L89-L99
const getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;
const hasPropWarning = getter && 'isReactWarning' in getter && getter.isReactWarning;

if (hasPropWarning) {
return (element as any).ref;
}

return element.props.ref;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ref access instance 2 of 2

Comment on lines +67 to +68
// @ts-ignore
ref: forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting the ref property has always yielded a type error, just didn't break the build before

Choose a reason for hiding this comment

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

I wonder what's the future of the Slot component as React considers cloneElement a legacy api. 🤔

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 wonder what's the future of the Slot component as React considers cloneElement a legacy api. 🤔

We'll see—I haven't seen a good alternative there, and "legacy" doesn't mean it's deprecated yet. I think it will stay around for a while.

package.json Outdated Show resolved Hide resolved
@eps1lon eps1lon mentioned this pull request Jun 4, 2024
22 tasks
@vladmoroz vladmoroz force-pushed the vlad/react-upgrades branch 5 times, most recently from 810b590 to d8f2071 Compare June 4, 2024 17:00
@vladmoroz vladmoroz force-pushed the vlad/react-upgrades branch from 80a94d7 to f1f02eb Compare June 5, 2024 08:51
// https://github.com/facebook/react/pull/28348
//
// Access the ref using the method that doesn't yield a warning.
function getElementRef(element: React.ReactElement) {
Copy link

@lucasmotta lucasmotta Jun 10, 2024

Choose a reason for hiding this comment

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

🤯

Should we extract this to a utility and reuse in other places instead of having the duplicated code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would have to be a separate package—I'd say we can keep it like this for now and extract it later if there's 3+ uses. I think two copy-pastes is OK for now.

Comment on lines +67 to +68
// @ts-ignore
ref: forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef,

Choose a reason for hiding this comment

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

I wonder what's the future of the Slot component as React considers cloneElement a legacy api. 🤔

// https://github.com/facebook/react/pull/28348
//
// Access the ref using the method that doesn't yield a warning.
function getElementRef(element: React.ReactElement) {

Choose a reason for hiding this comment

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

Same method as before, let's extract to a utility.

Copy link

@oliviertassinari oliviertassinari Jul 3, 2024

Choose a reason for hiding this comment

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

It looks like we could save bundle size here. How about going with

return React.version.split('.')[0] >= '19' ? element.props.ref : element.ref;

or

return element.props.propertyIsEnumerable('ref') ? element.props.ref : element.ref;

for this method? It seems to pass the tests.

@vladmoroz vladmoroz merged commit 06de2d4 into main Jun 10, 2024
5 checks passed
@vladmoroz vladmoroz deleted the vlad/react-upgrades branch June 10, 2024 16:02
@CarlosZiegler
Copy link

Amazing work, how can I test ? some RC ?

@vladmoroz
Copy link
Contributor Author

@CarlosZiegler yes, the latest RC versions include this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
7 participants