Skip to content

Commit 5652282

Browse files
authored
fix(react): Memoize wrapped component to prevent rerenders (#17230)
1 parent 47e5c6c commit 5652282

File tree

3 files changed

+119
-3
lines changed

3 files changed

+119
-3
lines changed

packages/react-router/test/client/react-exports.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe('Re-exports from React SDK', () => {
7676
});
7777

7878
expect(WrappedComponent).toBeDefined();
79-
expect(typeof WrappedComponent).toBe('function');
79+
expect(typeof WrappedComponent).toBe('object');
8080
expect(WrappedComponent.displayName).toBe('errorBoundary(TestComponent)');
8181

8282
const { getByText } = render(React.createElement(WrappedComponent));

packages/react/src/errorboundary.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,11 @@ function withErrorBoundary<P extends Record<string, any>>(
222222
): React.FC<P> {
223223
const componentDisplayName = WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT;
224224

225-
const Wrapped: React.FC<P> = (props: P) => (
225+
const Wrapped = React.memo((props: P) => (
226226
<ErrorBoundary {...errorBoundaryOptions}>
227227
<WrappedComponent {...props} />
228228
</ErrorBoundary>
229-
);
229+
)) as unknown as React.FC<P>;
230230

231231
Wrapped.displayName = `errorBoundary(${componentDisplayName})`;
232232

packages/react/test/errorboundary.test.tsx

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,122 @@ describe('withErrorBoundary', () => {
9696
const Component = withErrorBoundary(() => <h1>Hello World</h1>, { fallback: <h1>fallback</h1> });
9797
expect(Component.displayName).toBe(`errorBoundary(${UNKNOWN_COMPONENT})`);
9898
});
99+
100+
it('does not rerender when props are identical', () => {
101+
let renderCount = 0;
102+
const TestComponent = ({ title }: { title: string }) => {
103+
renderCount++;
104+
return <h1>{title}</h1>;
105+
};
106+
107+
const WrappedComponent = withErrorBoundary(TestComponent, { fallback: <h1>fallback</h1> });
108+
const { rerender } = render(<WrappedComponent title="test" />);
109+
110+
expect(renderCount).toBe(1);
111+
112+
// Rerender with identical props - should not cause TestComponent to rerender
113+
rerender(<WrappedComponent title="test" />);
114+
expect(renderCount).toBe(1);
115+
116+
// Rerender with different props - should cause TestComponent to rerender
117+
rerender(<WrappedComponent title="different" />);
118+
expect(renderCount).toBe(2);
119+
});
120+
121+
it('does not rerender when complex props are identical', () => {
122+
let renderCount = 0;
123+
const TestComponent = ({ data }: { data: { id: number; name: string } }) => {
124+
renderCount++;
125+
return <h1>{data.name}</h1>;
126+
};
127+
128+
const WrappedComponent = withErrorBoundary(TestComponent, { fallback: <h1>fallback</h1> });
129+
const props = { data: { id: 1, name: 'test' } };
130+
const { rerender } = render(<WrappedComponent {...props} />);
131+
132+
expect(renderCount).toBe(1);
133+
134+
// Rerender with same object reference - should not cause TestComponent to rerender
135+
rerender(<WrappedComponent {...props} />);
136+
expect(renderCount).toBe(1);
137+
138+
// Rerender with different object but same values - should cause rerender
139+
rerender(<WrappedComponent data={{ id: 1, name: 'test' }} />);
140+
expect(renderCount).toBe(2);
141+
142+
// Rerender with different values - should cause rerender
143+
rerender(<WrappedComponent data={{ id: 2, name: 'different' }} />);
144+
expect(renderCount).toBe(3);
145+
});
146+
147+
it('does not rerender when errorBoundaryOptions are the same', () => {
148+
let renderCount = 0;
149+
const TestComponent = ({ title }: { title: string }) => {
150+
renderCount++;
151+
return <h1>{title}</h1>;
152+
};
153+
154+
const errorBoundaryOptions = { fallback: <h1>fallback</h1> };
155+
const WrappedComponent = withErrorBoundary(TestComponent, errorBoundaryOptions);
156+
const { rerender } = render(<WrappedComponent title="test" />);
157+
158+
expect(renderCount).toBe(1);
159+
160+
// Rerender with identical props - should not cause TestComponent to rerender
161+
rerender(<WrappedComponent title="test" />);
162+
expect(renderCount).toBe(1);
163+
});
164+
165+
it('preserves function component behavior with React.memo', () => {
166+
const TestComponent = ({ title }: { title: string }) => <h1>{title}</h1>;
167+
const WrappedComponent = withErrorBoundary(TestComponent, { fallback: <h1>fallback</h1> });
168+
169+
expect(WrappedComponent).toBeDefined();
170+
expect(typeof WrappedComponent).toBe('object');
171+
expect(WrappedComponent.displayName).toBe('errorBoundary(TestComponent)');
172+
173+
const { container } = render(<WrappedComponent title="test" />);
174+
expect(container.innerHTML).toContain('test');
175+
});
176+
177+
it('does not rerender parent component unnecessarily', () => {
178+
let parentRenderCount = 0;
179+
let childRenderCount = 0;
180+
181+
const ChildComponent = ({ value }: { value: number }) => {
182+
childRenderCount++;
183+
return <div>Child: {value}</div>;
184+
};
185+
186+
const WrappedChild = withErrorBoundary(ChildComponent, { fallback: <div>Error</div> });
187+
188+
const ParentComponent = ({ childValue, otherProp }: { childValue: number; otherProp: string }) => {
189+
parentRenderCount++;
190+
return (
191+
<div>
192+
<div>Parent: {otherProp}</div>
193+
<WrappedChild value={childValue} />
194+
</div>
195+
);
196+
};
197+
198+
const { rerender } = render(<ParentComponent childValue={1} otherProp="test" />);
199+
200+
expect(parentRenderCount).toBe(1);
201+
expect(childRenderCount).toBe(1);
202+
203+
// Change otherProp but keep childValue the same
204+
rerender(<ParentComponent childValue={1} otherProp="changed" />);
205+
206+
expect(parentRenderCount).toBe(2); // Parent should rerender
207+
expect(childRenderCount).toBe(1); // Child should NOT rerender due to memo
208+
209+
// Change childValue
210+
rerender(<ParentComponent childValue={2} otherProp="changed" />);
211+
212+
expect(parentRenderCount).toBe(3); // Parent should rerender
213+
expect(childRenderCount).toBe(2); // Child should rerender due to changed props
214+
});
99215
});
100216

101217
describe('ErrorBoundary', () => {

0 commit comments

Comments
 (0)