From 00a2557234b0cf2fc3b44af8bf373b57fed77d3d Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 2 Jun 2025 17:49:22 +0200 Subject: [PATCH 1/6] feat: add custom RefAttributes interface which is used within ForwardRefComponent to mitigate breaking changes shipped as patch release in @types/react@18.2.61 --- .../etc/react-utilities.api.md | 8 ++++++- .../react-utilities/src/compose/types.ts | 10 +++++++-- .../react-utilities/src/index.ts | 2 +- .../react-utilities/src/utils/types.ts | 22 +++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-utilities/etc/react-utilities.api.md b/packages/react-components/react-utilities/etc/react-utilities.api.md index 70e8390f3ef24..70291d8f45118 100644 --- a/packages/react-components/react-utilities/etc/react-utilities.api.md +++ b/packages/react-components/react-utilities/etc/react-utilities.api.md @@ -63,7 +63,7 @@ export type FluentTriggerComponent = { }; // @public -export type ForwardRefComponent = React_2.ForwardRefExoticComponent>>; +export type ForwardRefComponent = React_2.ForwardRefExoticComponent>>; // @public export function getEventClientCoords(event: TouchOrMouseEvent): { @@ -190,6 +190,12 @@ export interface PriorityQueue { // @public (undocumented) export type ReactTouchOrMouseEvent = React_2.MouseEvent | React_2.TouchEvent; +// @public +export interface RefAttributes extends React_2.Attributes { + // (undocumented) + ref?: React_2.Ref | undefined; +} + // @public export type RefObjectFunction = React_2.RefObject & ((value: T | null) => void); diff --git a/packages/react-components/react-utilities/src/compose/types.ts b/packages/react-components/react-utilities/src/compose/types.ts index 277b29455a4f9..b280ba3e14a26 100644 --- a/packages/react-components/react-utilities/src/compose/types.ts +++ b/packages/react-components/react-utilities/src/compose/types.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { SLOT_CLASS_NAME_PROP_SYMBOL, SLOT_ELEMENT_TYPE_SYMBOL, SLOT_RENDER_FUNCTION_SYMBOL } from './constants'; -import { DistributiveOmit, ReplaceNullWithUndefined } from '../utils/types'; +import { DistributiveOmit, RefAttributes, ReplaceNullWithUndefined } from '../utils/types'; export type SlotRenderFunction = ( Component: React.ElementType, @@ -216,9 +216,15 @@ export type InferredElementRefType = ObscureEventName extends keyof Props /** * Return type for `React.forwardRef`, including inference of the proper typing for the ref. + * + * @remarks + * {@link React.RefAttributes} is {@link https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/69756 | leaking string references} into `forwardRef` components + * after introducing {@link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68720 | Breaking Change as Patch}, which shipped in `@types/react@18.2.61` + * - `forwardRef` component do not support string refs. + * - this uses custom `RefAttributes` which is compatible with all React versions. */ export type ForwardRefComponent = React.ForwardRefExoticComponent< - Props & React.RefAttributes> + Props & RefAttributes> >; // A definition like this would also work, but typescript is more likely to unnecessarily expand // the props type with this version (and it's likely much more expensive to evaluate) diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts index 76b147ac229fb..5d03ecc98e773 100644 --- a/packages/react-components/react-utilities/src/index.ts +++ b/packages/react-components/react-utilities/src/index.ts @@ -73,7 +73,7 @@ export { createPriorityQueue, } from './utils/index'; -export type { DistributiveOmit, UnionToIntersection } from './utils/types'; +export type { DistributiveOmit, UnionToIntersection, RefAttributes } from './utils/types'; export type { PriorityQueue } from './utils/priorityQueue'; diff --git a/packages/react-components/react-utilities/src/utils/types.ts b/packages/react-components/react-utilities/src/utils/types.ts index 3d0f96236b358..93e6744511121 100644 --- a/packages/react-components/react-utilities/src/utils/types.ts +++ b/packages/react-components/react-utilities/src/utils/types.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; /** * Helper type that works similar to Omit, * but when modifying an union type it will distribute the omission to all the union members. @@ -31,3 +32,24 @@ export type UnionToIntersection = (U extends unknown ? (x: U) => U : never) e * If type T includes `null`, remove it and add `undefined` instead. */ export type ReplaceNullWithUndefined = T extends null ? Exclude | undefined : T; + +/** + * This type should be used in place of `React.RefAttributes` in all components that specify `ref` prop. + * + * If user is using React 18 types `>=18.2.61`, they will run into type issues of incompatible refs, using this type mitigates this issues across react type versions. + * + * @remarks + * + * React 18 types introduced Breaking Changes to the `RefAttributes` interface as patch release. + * These changes were release in `@types/react@18.2.61` (replacing ref with `LegacyRef`): + * - {@link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68720 | PR } + * - {@link https://app.unpkg.com/@types/react@18.2.61/files/index.d.ts | shipped definitions } + * + * + * In React 19 types this was "reverted" back to the original `Ref` type. + * In order to maintain compatibility with React 17,18,19, we are forced to use our own version of `RefAttributes`. + * + */ +export interface RefAttributes extends React.Attributes { + ref?: React.Ref | undefined; +} From ff344ef2b54201e9b11a1aa56f656806237ec72e Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 2 Jun 2025 17:55:37 +0200 Subject: [PATCH 2/6] change file --- ...act-utilities-66a3d122-bdde-45be-a7b3-d6ae2b28a1d6.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-utilities-66a3d122-bdde-45be-a7b3-d6ae2b28a1d6.json diff --git a/change/@fluentui-react-utilities-66a3d122-bdde-45be-a7b3-d6ae2b28a1d6.json b/change/@fluentui-react-utilities-66a3d122-bdde-45be-a7b3-d6ae2b28a1d6.json new file mode 100644 index 0000000000000..613156a3a20b8 --- /dev/null +++ b/change/@fluentui-react-utilities-66a3d122-bdde-45be-a7b3-d6ae2b28a1d6.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add custom RefAttributes interface which is used within ForwardRefComponent to mitigate breaking changes shipped as patch release in @types/react@18.2.61", + "packageName": "@fluentui/react-utilities", + "email": "martinhochel@microsoft.com", + "dependentChangeType": "patch" +} From 9297bfbf770875bb6b689967ffea728d8facabaf Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 2 Jun 2025 18:41:49 +0200 Subject: [PATCH 3/6] fix(eslint-plugin): add valid tsdoc tags to propagate when using react config --- packages/eslint-plugin/src/configs/react.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/eslint-plugin/src/configs/react.js b/packages/eslint-plugin/src/configs/react.js index ad66e8190d60b..2d58c4295cb6c 100644 --- a/packages/eslint-plugin/src/configs/react.js +++ b/packages/eslint-plugin/src/configs/react.js @@ -23,6 +23,8 @@ module.exports = { 'jsdoc/check-tag-names': [ 'error', { + // Allow TSDoc tags + definedTags: ['remarks'], jsxTags: true, }, ], From 813b1fb3818b3c3cb11c5434b8cd8d6258b35809 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Tue, 3 Jun 2025 14:27:17 +0200 Subject: [PATCH 4/6] docs: udpate jsdoc --- .../react-components/react-utilities/src/compose/types.ts | 4 ++-- packages/react-components/react-utilities/src/utils/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-components/react-utilities/src/compose/types.ts b/packages/react-components/react-utilities/src/compose/types.ts index b280ba3e14a26..9f60fa7ddd8a4 100644 --- a/packages/react-components/react-utilities/src/compose/types.ts +++ b/packages/react-components/react-utilities/src/compose/types.ts @@ -219,9 +219,9 @@ export type InferredElementRefType = ObscureEventName extends keyof Props * * @remarks * {@link React.RefAttributes} is {@link https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/69756 | leaking string references} into `forwardRef` components - * after introducing {@link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68720 | Breaking Change as Patch}, which shipped in `@types/react@18.2.61` + * after introducing {@link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68720 | RefAttributes Type Extension}, which shipped in `@types/react@18.2.61` * - `forwardRef` component do not support string refs. - * - this uses custom `RefAttributes` which is compatible with all React versions. + * - uses custom `RefAttributes` which is compatible with all React versions enforcing no `string` allowance. */ export type ForwardRefComponent = React.ForwardRefExoticComponent< Props & RefAttributes> diff --git a/packages/react-components/react-utilities/src/utils/types.ts b/packages/react-components/react-utilities/src/utils/types.ts index 93e6744511121..e5b738f4ec594 100644 --- a/packages/react-components/react-utilities/src/utils/types.ts +++ b/packages/react-components/react-utilities/src/utils/types.ts @@ -40,8 +40,8 @@ export type ReplaceNullWithUndefined = T extends null ? Exclude | un * * @remarks * - * React 18 types introduced Breaking Changes to the `RefAttributes` interface as patch release. - * These changes were release in `@types/react@18.2.61` (replacing ref with `LegacyRef`): + * React 18 types introduced Type Expansion Change to the `RefAttributes` interface as patch release. + * These changes were released in `@types/react@18.2.61` (replacing ref with `LegacyRef`, which leaks `string` into the union type, causing breaking changes between v8/v9 libraries): * - {@link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68720 | PR } * - {@link https://app.unpkg.com/@types/react@18.2.61/files/index.d.ts | shipped definitions } * From 8e0057922c859c62c0daf7418e2ae03ca46a89ee Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 6 Jun 2025 10:36:01 +0200 Subject: [PATCH 5/6] refactor(react-utilities): move RefAttributes to compose domain where it belongs and add test case --- .../react-utilities/src/compose/index.ts | 1 + .../react-utilities/src/compose/types.test.ts | 46 +++++++++++++++++++ .../react-utilities/src/compose/types.ts | 23 +++++++++- .../react-utilities/src/index.ts | 3 +- .../react-utilities/src/utils/types.ts | 22 --------- 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/packages/react-components/react-utilities/src/compose/index.ts b/packages/react-components/react-utilities/src/compose/index.ts index 9dd0db0086066..756a54c8a9cf0 100644 --- a/packages/react-components/react-utilities/src/compose/index.ts +++ b/packages/react-components/react-utilities/src/compose/index.ts @@ -8,6 +8,7 @@ export type { EventHandler, ExtractSlotProps, ForwardRefComponent, + RefAttributes, InferredElementRefType, IsSingleton, PropsWithoutChildren, diff --git a/packages/react-components/react-utilities/src/compose/types.test.ts b/packages/react-components/react-utilities/src/compose/types.test.ts index b0cd6e7eaf590..5154e9061bc88 100644 --- a/packages/react-components/react-utilities/src/compose/types.test.ts +++ b/packages/react-components/react-utilities/src/compose/types.test.ts @@ -54,6 +54,52 @@ describe(`types`, () => { componentEl = { greeting: 123, who: false }; expect(componentEl).toBeDefined(); + + // + // v9 ForwardRefComponent + RefAttributes with React 18.2.61 types issue + // + + /** + * @types/react@18.2.61 introduced a change in the `RefAttributes` type to include `LegacyRef` + */ + interface RefAttributesAfterTypesReact18_2_61 extends React.Attributes { + ref?: React.LegacyRef; + } + + /** + * + * previous change affects forwardRef api + */ + function forwardRefAfterTypesReact18_2_61( + render: React.ForwardRefRenderFunction, + ): React.ForwardRefExoticComponent & RefAttributesAfterTypesReact18_2_61> { + return null as unknown as React.ForwardRefExoticComponent< + React.PropsWithoutRef

& RefAttributesAfterTypesReact18_2_61 + >; + } + + type ButtonProps = Types.ComponentProps<{ + root: NonNullable>; + + icon?: Types.Slot<'span'>; + }>; + const Button = (_props => { + return null; + }) as Types.ForwardRefComponent; + + const Example = forwardRefAfterTypesReact18_2_61((props: { hello?: string }, _ref) => { + const wrong = React.createElement( + Button, + // @ts-expect-error - Type 'LegacyRef | undefined' is not assignable to type 'Ref | undefined'. -> Type 'string' is not assignable to type 'Ref | undefined'. + { + ...(props as RefAttributesAfterTypesReact18_2_61), + }, + ); + const correct = React.createElement(Button, { ...(props as Types.RefAttributes) }); + + return React.createElement(React.Fragment, null, wrong, correct); + }); + console.log(Example); }); }); }); diff --git a/packages/react-components/react-utilities/src/compose/types.ts b/packages/react-components/react-utilities/src/compose/types.ts index 9f60fa7ddd8a4..c4194d28ad34a 100644 --- a/packages/react-components/react-utilities/src/compose/types.ts +++ b/packages/react-components/react-utilities/src/compose/types.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { SLOT_CLASS_NAME_PROP_SYMBOL, SLOT_ELEMENT_TYPE_SYMBOL, SLOT_RENDER_FUNCTION_SYMBOL } from './constants'; -import { DistributiveOmit, RefAttributes, ReplaceNullWithUndefined } from '../utils/types'; +import type { DistributiveOmit, ReplaceNullWithUndefined } from '../utils/types'; export type SlotRenderFunction = ( Component: React.ElementType, @@ -295,3 +295,24 @@ export type EventHandler> = ( ev: React.SyntheticEvent | Event, data: TData, ) => void; + +/** + * This type should be used in place of `React.RefAttributes` in all components that specify `ref` prop. + * + * If user is using React 18 types `>=18.2.61`, they will run into type issues of incompatible refs, using this type mitigates this issues across react type versions. + * + * @remarks + * + * React 18 types introduced Type Expansion Change to the `RefAttributes` interface as patch release. + * These changes were released in `@types/react@18.2.61` (replacing ref with `LegacyRef`, which leaks `string` into the union type, causing breaking changes between v8/v9 libraries): + * - {@link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68720 | PR } + * - {@link https://app.unpkg.com/@types/react@18.2.61/files/index.d.ts | shipped definitions } + * + * + * In React 19 types this was "reverted" back to the original `Ref` type. + * In order to maintain compatibility with React 17,18,19, we are forced to use our own version of `RefAttributes`. + * + */ +export interface RefAttributes extends React.Attributes { + ref?: React.Ref | undefined; +} diff --git a/packages/react-components/react-utilities/src/index.ts b/packages/react-components/react-utilities/src/index.ts index 5d03ecc98e773..b0909d2611103 100644 --- a/packages/react-components/react-utilities/src/index.ts +++ b/packages/react-components/react-utilities/src/index.ts @@ -20,6 +20,7 @@ export type { ComponentProps, ComponentState, ForwardRefComponent, + RefAttributes, // eslint-disable-next-line @typescript-eslint/no-deprecated ResolveShorthandFunction, // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -73,7 +74,7 @@ export { createPriorityQueue, } from './utils/index'; -export type { DistributiveOmit, UnionToIntersection, RefAttributes } from './utils/types'; +export type { DistributiveOmit, UnionToIntersection } from './utils/types'; export type { PriorityQueue } from './utils/priorityQueue'; diff --git a/packages/react-components/react-utilities/src/utils/types.ts b/packages/react-components/react-utilities/src/utils/types.ts index e5b738f4ec594..3d0f96236b358 100644 --- a/packages/react-components/react-utilities/src/utils/types.ts +++ b/packages/react-components/react-utilities/src/utils/types.ts @@ -1,4 +1,3 @@ -import * as React from 'react'; /** * Helper type that works similar to Omit, * but when modifying an union type it will distribute the omission to all the union members. @@ -32,24 +31,3 @@ export type UnionToIntersection = (U extends unknown ? (x: U) => U : never) e * If type T includes `null`, remove it and add `undefined` instead. */ export type ReplaceNullWithUndefined = T extends null ? Exclude | undefined : T; - -/** - * This type should be used in place of `React.RefAttributes` in all components that specify `ref` prop. - * - * If user is using React 18 types `>=18.2.61`, they will run into type issues of incompatible refs, using this type mitigates this issues across react type versions. - * - * @remarks - * - * React 18 types introduced Type Expansion Change to the `RefAttributes` interface as patch release. - * These changes were released in `@types/react@18.2.61` (replacing ref with `LegacyRef`, which leaks `string` into the union type, causing breaking changes between v8/v9 libraries): - * - {@link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68720 | PR } - * - {@link https://app.unpkg.com/@types/react@18.2.61/files/index.d.ts | shipped definitions } - * - * - * In React 19 types this was "reverted" back to the original `Ref` type. - * In order to maintain compatibility with React 17,18,19, we are forced to use our own version of `RefAttributes`. - * - */ -export interface RefAttributes extends React.Attributes { - ref?: React.Ref | undefined; -} From 33d0ef5f023593934de4d902f23243e71fae3862 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 6 Jun 2025 10:58:55 +0200 Subject: [PATCH 6/6] fixup! refactor(react-utilities): move RefAttributes to compose domain where it belongs and add test case --- .../react-utilities/src/compose/types.test.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/react-components/react-utilities/src/compose/types.test.ts b/packages/react-components/react-utilities/src/compose/types.test.ts index 5154e9061bc88..7207d5478c735 100644 --- a/packages/react-components/react-utilities/src/compose/types.test.ts +++ b/packages/react-components/react-utilities/src/compose/types.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import * as React from 'react'; import type * as Types from './types'; @@ -59,17 +60,12 @@ describe(`types`, () => { // v9 ForwardRefComponent + RefAttributes with React 18.2.61 types issue // - /** - * @types/react@18.2.61 introduced a change in the `RefAttributes` type to include `LegacyRef` - */ + // @types/react@18.2.61 introduced a change in the `RefAttributes` type to include `LegacyRef` interface RefAttributesAfterTypesReact18_2_61 extends React.Attributes { ref?: React.LegacyRef; } - /** - * - * previous change affects forwardRef api - */ + // previous change affects forwardRef api function forwardRefAfterTypesReact18_2_61( render: React.ForwardRefRenderFunction, ): React.ForwardRefExoticComponent & RefAttributesAfterTypesReact18_2_61> {