diff --git a/packages/react-router/test/client/react-exports.test.ts b/packages/react-router/test/client/react-exports.test.ts index 02e5b970a21e..cae5ac56208a 100644 --- a/packages/react-router/test/client/react-exports.test.ts +++ b/packages/react-router/test/client/react-exports.test.ts @@ -76,7 +76,7 @@ describe('Re-exports from React SDK', () => { }); expect(WrappedComponent).toBeDefined(); - expect(typeof WrappedComponent).toBe('function'); + expect(typeof WrappedComponent).toBe('object'); expect(WrappedComponent.displayName).toBe('errorBoundary(TestComponent)'); const { getByText } = render(React.createElement(WrappedComponent)); diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index e3f94b441ee6..f37afe961042 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -222,11 +222,11 @@ function withErrorBoundary

>( ): React.FC

{ const componentDisplayName = WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT; - const Wrapped: React.FC

= (props: P) => ( + const Wrapped = React.memo((props: P) => ( - ); + )) as unknown as React.FC

; Wrapped.displayName = `errorBoundary(${componentDisplayName})`; diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 275faa4e2079..5e731cc86b49 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -96,6 +96,122 @@ describe('withErrorBoundary', () => { const Component = withErrorBoundary(() =>

Hello World

, { fallback:

fallback

}); expect(Component.displayName).toBe(`errorBoundary(${UNKNOWN_COMPONENT})`); }); + + it('does not rerender when props are identical', () => { + let renderCount = 0; + const TestComponent = ({ title }: { title: string }) => { + renderCount++; + return

{title}

; + }; + + const WrappedComponent = withErrorBoundary(TestComponent, { fallback:

fallback

}); + const { rerender } = render(); + + expect(renderCount).toBe(1); + + // Rerender with identical props - should not cause TestComponent to rerender + rerender(); + expect(renderCount).toBe(1); + + // Rerender with different props - should cause TestComponent to rerender + rerender(); + expect(renderCount).toBe(2); + }); + + it('does not rerender when complex props are identical', () => { + let renderCount = 0; + const TestComponent = ({ data }: { data: { id: number; name: string } }) => { + renderCount++; + return

{data.name}

; + }; + + const WrappedComponent = withErrorBoundary(TestComponent, { fallback:

fallback

}); + const props = { data: { id: 1, name: 'test' } }; + const { rerender } = render(); + + expect(renderCount).toBe(1); + + // Rerender with same object reference - should not cause TestComponent to rerender + rerender(); + expect(renderCount).toBe(1); + + // Rerender with different object but same values - should cause rerender + rerender(); + expect(renderCount).toBe(2); + + // Rerender with different values - should cause rerender + rerender(); + expect(renderCount).toBe(3); + }); + + it('does not rerender when errorBoundaryOptions are the same', () => { + let renderCount = 0; + const TestComponent = ({ title }: { title: string }) => { + renderCount++; + return

{title}

; + }; + + const errorBoundaryOptions = { fallback:

fallback

}; + const WrappedComponent = withErrorBoundary(TestComponent, errorBoundaryOptions); + const { rerender } = render(); + + expect(renderCount).toBe(1); + + // Rerender with identical props - should not cause TestComponent to rerender + rerender(); + expect(renderCount).toBe(1); + }); + + it('preserves function component behavior with React.memo', () => { + const TestComponent = ({ title }: { title: string }) =>

{title}

; + const WrappedComponent = withErrorBoundary(TestComponent, { fallback:

fallback

}); + + expect(WrappedComponent).toBeDefined(); + expect(typeof WrappedComponent).toBe('object'); + expect(WrappedComponent.displayName).toBe('errorBoundary(TestComponent)'); + + const { container } = render(); + expect(container.innerHTML).toContain('test'); + }); + + it('does not rerender parent component unnecessarily', () => { + let parentRenderCount = 0; + let childRenderCount = 0; + + const ChildComponent = ({ value }: { value: number }) => { + childRenderCount++; + return
Child: {value}
; + }; + + const WrappedChild = withErrorBoundary(ChildComponent, { fallback:
Error
}); + + const ParentComponent = ({ childValue, otherProp }: { childValue: number; otherProp: string }) => { + parentRenderCount++; + return ( +
+
Parent: {otherProp}
+ +
+ ); + }; + + const { rerender } = render(); + + expect(parentRenderCount).toBe(1); + expect(childRenderCount).toBe(1); + + // Change otherProp but keep childValue the same + rerender(); + + expect(parentRenderCount).toBe(2); // Parent should rerender + expect(childRenderCount).toBe(1); // Child should NOT rerender due to memo + + // Change childValue + rerender(); + + expect(parentRenderCount).toBe(3); // Parent should rerender + expect(childRenderCount).toBe(2); // Child should rerender due to changed props + }); }); describe('ErrorBoundary', () => {