From 835242344c1c122ebd2d3e72436111c8dd6dc891 Mon Sep 17 00:00:00 2001 From: Alberto Blanco Date: Wed, 2 Sep 2026 15:47:14 +0200 Subject: [PATCH 1/2] Infer the wrapped component's ref type in withErrorBoundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HOC was parameterized over `Type extends ComponentClass`, but no argument mentions `Type`, so it can never be inferred and always falls back to its constraint. Every wrapper therefore came out typed `RefAttributes>`, and passing the ref the wrapped component actually takes stops type-checking: Type 'RefObject' is not assignable to type 'Ref>' Parameterize over the component instead, so `Type` is inferred from the argument, and read the props and ref off it with `ComponentProps` and `ComponentRef` — which is also what the runtime already forwards. The existing ref test passes a class component, whose instance happens to satisfy `Component`, which is why this went unnoticed. The added test passes a function component that declares a `ref` prop, the React 19 form, and fails to compile without this change. Co-authored-by: Claude Opus 5 --- lib/utils/withErrorBoundary.test.tsx | 34 +++++++++++++++++++++++++++- lib/utils/withErrorBoundary.ts | 15 ++++++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/lib/utils/withErrorBoundary.test.tsx b/lib/utils/withErrorBoundary.test.tsx index 940be2f8..1cd291eb 100644 --- a/lib/utils/withErrorBoundary.test.tsx +++ b/lib/utils/withErrorBoundary.test.tsx @@ -1,4 +1,10 @@ -import { Component, createRef, type PropsWithChildren } from "react"; +import { + Component, + createRef, + useImperativeHandle, + type PropsWithChildren, + type Ref, +} from "react"; import { createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -78,4 +84,30 @@ describe("withErrorBoundary", () => { expect(ref.current).not.toBeNull(); expect(typeof ref.current?.test).toBe("function"); }); + + it("should forward refs to a function component that takes a ref prop", () => { + type Handle = { test: () => void }; + + function Inner({ foo, ref }: { foo: string; ref?: Ref }) { + useImperativeHandle(ref, () => ({ + test() { + // No-op + }, + })); + return foo; + } + + const Wrapped = withErrorBoundary(Inner, { + fallback:
Error
, + }); + + const ref = createRef(); + + act(() => { + root.render(); + }); + + expect(ref.current).not.toBeNull(); + expect(typeof ref.current?.test).toBe("function"); + }); }); diff --git a/lib/utils/withErrorBoundary.ts b/lib/utils/withErrorBoundary.ts index 2204860f..c9911333 100644 --- a/lib/utils/withErrorBoundary.ts +++ b/lib/utils/withErrorBoundary.ts @@ -1,17 +1,22 @@ import { createElement, forwardRef, - type ComponentClass, + type ComponentProps, + type ComponentRef, type ComponentType, } from "react"; import { ErrorBoundary } from "../components/ErrorBoundary"; import type { ErrorBoundaryProps } from "../types"; export function withErrorBoundary< - Type extends ComponentClass, - Props extends object, ->(Component: ComponentType, errorBoundaryProps: ErrorBoundaryProps) { - const Wrapped = forwardRef, Props>((props, ref) => + // ComponentProps and ComponentRef are themselves constrained by + // JSXElementConstructor, so the parameter they read has to be too. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Type extends ComponentType, +>(Component: Type, errorBoundaryProps: ErrorBoundaryProps) { + type Props = ComponentProps; + + const Wrapped = forwardRef, Props>((props, ref) => createElement( ErrorBoundary, errorBoundaryProps, From 4d65390456c9cf7de76f07d14eadd1811911333b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Sat, 5 Sep 2026 08:49:36 -0400 Subject: [PATCH 2/2] Tweak tests --- lib/utils/withErrorBoundary.test.tsx | 66 +++++++++++++++++++++------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/lib/utils/withErrorBoundary.test.tsx b/lib/utils/withErrorBoundary.test.tsx index 1cd291eb..a4f585e2 100644 --- a/lib/utils/withErrorBoundary.test.tsx +++ b/lib/utils/withErrorBoundary.test.tsx @@ -1,6 +1,7 @@ import { Component, createRef, + forwardRef, useImperativeHandle, type PropsWithChildren, type Ref, @@ -9,6 +10,7 @@ import { createRoot } from "react-dom/client"; import { act } from "react-dom/test-utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { withErrorBoundary } from "./withErrorBoundary"; +import { assert } from "./assert"; describe("withErrorBoundary", () => { let container: HTMLDivElement; @@ -62,42 +64,72 @@ describe("withErrorBoundary", () => { it("should forward refs", () => { type Props = { foo: string }; - class Inner extends Component { - test() { - // No-op + class TestClassComponent extends Component { + getFoo() { + return this.props.foo; } render() { - return this.props.foo; + return null; } } - const Wrapped = withErrorBoundary(Inner, { + const Wrapped = withErrorBoundary(TestClassComponent, { fallback:
Error
, }); - const ref = createRef(); + const ref = createRef(); act(() => { root.render(); }); - expect(ref.current).not.toBeNull(); - expect(typeof ref.current?.test).toBe("function"); + assert(ref.current !== null); + expect(ref.current.getFoo()).toBe("abc"); }); it("should forward refs to a function component that takes a ref prop", () => { - type Handle = { test: () => void }; - - function Inner({ foo, ref }: { foo: string; ref?: Ref }) { + type Handle = { getFoo: () => string }; + + function TestFunctionComponent({ + foo, + ref, + }: { + foo: string; + ref?: Ref; + }) { useImperativeHandle(ref, () => ({ - test() { - // No-op - }, + getFoo: () => foo, })); return foo; } - const Wrapped = withErrorBoundary(Inner, { + const Wrapped = withErrorBoundary(TestFunctionComponent, { + fallback:
Error
, + }); + + const ref = createRef(); + + act(() => { + root.render(); + }); + + assert(ref.current !== null); + expect(ref.current.getFoo()).toBe("abc"); + }); + + it("should forward refs to a function using forwardRef", () => { + type Handle = { getFoo: () => string }; + + const TestForwardRefComponent = forwardRef( + ({ foo }: { foo: string }, ref: Ref) => { + useImperativeHandle(ref, () => ({ + getFoo: () => foo, + })); + return null; + }, + ); + + const Wrapped = withErrorBoundary(TestForwardRefComponent, { fallback:
Error
, }); @@ -107,7 +139,7 @@ describe("withErrorBoundary", () => { root.render(); }); - expect(ref.current).not.toBeNull(); - expect(typeof ref.current?.test).toBe("function"); + assert(ref.current !== null); + expect(ref.current.getFoo()).toBe("abc"); }); });