From ab6648cb5afd388bad9029a83478720841a861ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A5=E1=86=AB?= =?UTF-8?q?=E1=84=80=E1=85=B2?= Date: Mon, 24 Aug 2026 12:43:45 +0900 Subject: [PATCH] fix(react): allow key attribute on components created by with --- .changeset/fix-with-key-attribute.md | 5 +++++ packages/react/src/ClientOnly.test-d.tsx | 22 +++++++++++++++++++ packages/react/src/ClientOnly.tsx | 2 +- packages/react/src/Delay.test-d.tsx | 19 ++++++++++++++++ packages/react/src/Delay.tsx | 2 +- packages/react/src/ErrorBoundary.test-d.tsx | 22 +++++++++++++++++++ packages/react/src/ErrorBoundary.tsx | 2 +- .../react/src/ErrorBoundaryGroup.test-d.tsx | 19 ++++++++++++++++ packages/react/src/ErrorBoundaryGroup.tsx | 2 +- packages/react/src/Suspense.test-d.tsx | 22 +++++++++++++++++++ packages/react/src/Suspense.tsx | 2 +- 11 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-with-key-attribute.md create mode 100644 packages/react/src/ClientOnly.test-d.tsx create mode 100644 packages/react/src/Suspense.test-d.tsx diff --git a/.changeset/fix-with-key-attribute.md b/.changeset/fix-with-key-attribute.md new file mode 100644 index 000000000..c2ad6c6ec --- /dev/null +++ b/.changeset/fix-with-key-attribute.md @@ -0,0 +1,5 @@ +--- +'@suspensive/react': patch +--- + +fix(react): allow the React-reserved `key` attribute on components created by `with` diff --git a/packages/react/src/ClientOnly.test-d.tsx b/packages/react/src/ClientOnly.test-d.tsx new file mode 100644 index 000000000..018d58a9e --- /dev/null +++ b/packages/react/src/ClientOnly.test-d.tsx @@ -0,0 +1,22 @@ +import { ClientOnly } from './ClientOnly' + +describe('', () => { + describe('ClientOnly.with', () => { + it('should accept the React-reserved key attribute on components wrapped without props', () => { + const Wrapped = ClientOnly.with({ fallback: null }, () => <>) + + expectTypeOf([, ]).toEqualTypeOf>() + + assertType( + // @ts-expect-error arbitrary props should still be rejected + + ) + }) + + it('should accept the key attribute along with inferred props', () => { + const Wrapped = ClientOnly.with({ fallback: null }, ({ text }: { text: string }) => <>{text}) + + expectTypeOf().toEqualTypeOf() + }) + }) +}) diff --git a/packages/react/src/ClientOnly.tsx b/packages/react/src/ClientOnly.tsx index 31ca1b512..8ee7dd793 100644 --- a/packages/react/src/ClientOnly.tsx +++ b/packages/react/src/ClientOnly.tsx @@ -17,7 +17,7 @@ export const ClientOnly = Object.assign( ({ children, fallback }: ClientOnlyProps) => <>{useIsClient() ? children : fallback}, { displayName: 'ClientOnly', - with: = Record>( + with: = Record>( clientOnlyProps: PropsWithoutChildren, Component: ComponentType ) => diff --git a/packages/react/src/Delay.test-d.tsx b/packages/react/src/Delay.test-d.tsx index 1f367bbf4..e4574da4f 100644 --- a/packages/react/src/Delay.test-d.tsx +++ b/packages/react/src/Delay.test-d.tsx @@ -39,4 +39,23 @@ describe('', () => { ).toEqualTypeOf() }) + + describe('Delay.with', () => { + it('should accept the React-reserved key attribute on components wrapped without props', () => { + const Wrapped = Delay.with({ ms: 1000 }, () => <>) + + expectTypeOf([, ]).toEqualTypeOf>() + + assertType( + // @ts-expect-error arbitrary props should still be rejected + + ) + }) + + it('should accept the key attribute along with inferred props', () => { + const Wrapped = Delay.with({ ms: 1000 }, ({ text }: { text: string }) => <>{text}) + + expectTypeOf().toEqualTypeOf() + }) + }) }) diff --git a/packages/react/src/Delay.tsx b/packages/react/src/Delay.tsx index 500d3c437..0fefa8069 100644 --- a/packages/react/src/Delay.tsx +++ b/packages/react/src/Delay.tsx @@ -49,7 +49,7 @@ export const Delay = Object.assign( }, { displayName: 'Delay', - with: = Record>( + with: = Record>( delayProps: PropsWithoutChildren, Component: ComponentType ) => diff --git a/packages/react/src/ErrorBoundary.test-d.tsx b/packages/react/src/ErrorBoundary.test-d.tsx index c7b623110..eb25fada8 100644 --- a/packages/react/src/ErrorBoundary.test-d.tsx +++ b/packages/react/src/ErrorBoundary.test-d.tsx @@ -1230,4 +1230,26 @@ describe('', () => { expectTypeOf(example).toEqualTypeOf() }) }) + + describe('ErrorBoundary.with key attribute', () => { + it('should accept the React-reserved key attribute on components wrapped without props', () => { + const Wrapped = ErrorBoundary.with({ fallback: ({ error }) => <>{error.message} }, () => <>) + + expectTypeOf([, ]).toEqualTypeOf>() + + assertType( + // @ts-expect-error arbitrary props should still be rejected + + ) + }) + + it('should accept the key attribute along with inferred props', () => { + const Wrapped = ErrorBoundary.with( + { fallback: ({ error }) => <>{error.message} }, + ({ text }: { text: string }) => <>{text} + ) + + expectTypeOf().toEqualTypeOf() + }) + }) }) diff --git a/packages/react/src/ErrorBoundary.tsx b/packages/react/src/ErrorBoundary.tsx index 538a4a880..c0fdb27eb 100644 --- a/packages/react/src/ErrorBoundary.tsx +++ b/packages/react/src/ErrorBoundary.tsx @@ -263,7 +263,7 @@ export const ErrorBoundary = Object.assign( { displayName: 'ErrorBoundary', with: < - TProps extends ComponentProps = Record, + TProps extends ComponentProps = Record, TShouldCatch extends ShouldCatch = ShouldCatch, >( errorBoundaryProps: PropsWithoutChildren>, diff --git a/packages/react/src/ErrorBoundaryGroup.test-d.tsx b/packages/react/src/ErrorBoundaryGroup.test-d.tsx index 91ce4f867..80600bb23 100644 --- a/packages/react/src/ErrorBoundaryGroup.test-d.tsx +++ b/packages/react/src/ErrorBoundaryGroup.test-d.tsx @@ -10,4 +10,23 @@ describe('ErrorBoundaryGroup', () => { {() => <>} ).not.toEqualTypeOf() }) + + describe('ErrorBoundaryGroup.with', () => { + it('should accept the React-reserved key attribute on components wrapped without props', () => { + const Wrapped = ErrorBoundaryGroup.with({}, () => <>) + + expectTypeOf([, ]).toEqualTypeOf>() + + assertType( + // @ts-expect-error arbitrary props should still be rejected + + ) + }) + + it('should accept the key attribute along with inferred props', () => { + const Wrapped = ErrorBoundaryGroup.with({}, ({ text }: { text: string }) => <>{text}) + + expectTypeOf().toEqualTypeOf() + }) + }) }) diff --git a/packages/react/src/ErrorBoundaryGroup.tsx b/packages/react/src/ErrorBoundaryGroup.tsx index 43a2a5966..5bf7ef602 100644 --- a/packages/react/src/ErrorBoundaryGroup.tsx +++ b/packages/react/src/ErrorBoundaryGroup.tsx @@ -54,7 +54,7 @@ export const ErrorBoundaryGroup = Object.assign( }, { displayName: 'ErrorBoundaryGroup', - with: = Record>( + with: = Record>( errorBoundaryGroupProps: PropsWithoutChildren, Component: ComponentType ) => diff --git a/packages/react/src/Suspense.test-d.tsx b/packages/react/src/Suspense.test-d.tsx new file mode 100644 index 000000000..3a817a888 --- /dev/null +++ b/packages/react/src/Suspense.test-d.tsx @@ -0,0 +1,22 @@ +import { Suspense } from './Suspense' + +describe('', () => { + describe('Suspense.with', () => { + it('should accept the React-reserved key attribute on components wrapped without props', () => { + const Wrapped = Suspense.with({ fallback: null }, () => <>) + + expectTypeOf([, ]).toEqualTypeOf>() + + assertType( + // @ts-expect-error arbitrary props should still be rejected + + ) + }) + + it('should accept the key attribute along with inferred props', () => { + const Wrapped = Suspense.with({ fallback: null }, ({ text }: { text: string }) => <>{text}) + + expectTypeOf().toEqualTypeOf() + }) + }) +}) diff --git a/packages/react/src/Suspense.tsx b/packages/react/src/Suspense.tsx index b2712dca4..f8c5796fb 100644 --- a/packages/react/src/Suspense.tsx +++ b/packages/react/src/Suspense.tsx @@ -31,7 +31,7 @@ export const Suspense = Object.assign( }, { displayName: 'Suspense', - with: = Record>( + with: = Record>( suspenseProps: PropsWithoutChildren, Component: ComponentType ) =>