Skip to content

Commit 869968b

Browse files
authored
Ensure memoized components re-render after errors (#4880)
1 parent aeb59e0 commit 869968b

File tree

3 files changed

+85
-3
lines changed

3 files changed

+85
-3
lines changed

compat/test/browser/memo.test.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import React, {
1111
memo,
1212
useState
1313
} from 'preact/compat';
14-
import { vi } from 'vitest';
14+
import { vi, expect } from 'vitest';
1515

1616
const h = React.createElement;
1717

@@ -238,4 +238,40 @@ describe('memo()', () => {
238238

239239
expect(Memoized.type).to.equal(Foo);
240240
});
241+
242+
it('should recover and render siblings when memo child throws once', () => {
243+
let causeError = true;
244+
245+
const TestWithMemo = /** @type {any} */ (memo(
246+
/** @type {(props: { n: number }) => any} */ (props => {
247+
const { n } = props;
248+
if (n === 2 && causeError) {
249+
throw new Error('test error');
250+
}
251+
return <p>test {n}</p>;
252+
})
253+
));
254+
255+
class App extends Component {
256+
static getDerivedStateFromError() {
257+
causeError = false;
258+
return {};
259+
}
260+
render() {
261+
return (
262+
<div>
263+
<h1>Example</h1>
264+
<TestWithMemo n={1} />
265+
<TestWithMemo n={2} />
266+
<TestWithMemo n={3} />
267+
</div>
268+
);
269+
}
270+
}
271+
272+
render(<App />, scratch);
273+
rerender();
274+
275+
expect(scratch.textContent).to.equal('Exampletest 1test 2test 3');
276+
});
241277
});

src/diff/catch-error.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
NULL,
44
COMPONENT_DIRTY,
55
COMPONENT_PENDING_ERROR,
6-
COMPONENT_PROCESSING_EXCEPTION
6+
COMPONENT_PROCESSING_EXCEPTION,
7+
COMPONENT_FORCE
78
} from '../constants';
89

910
/**
@@ -28,6 +29,7 @@ export function _catchError(error, vnode, oldVNode, errorInfo) {
2829
(component = vnode._component) &&
2930
!(component._bits & COMPONENT_PROCESSING_EXCEPTION)
3031
) {
32+
component._bits |= COMPONENT_FORCE;
3133
try {
3234
ctor = component.constructor;
3335

test/browser/lifecycles/componentDidCatch.test.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { setupRerender } from 'preact/test-utils';
22
import { createElement, render, Component, Fragment } from 'preact';
33
import { setupScratch, teardown } from '../../_util/helpers';
4-
import { vi } from 'vitest';
4+
import { vi, expect } from 'vitest';
55

66
/** @jsx createElement */
77

@@ -714,5 +714,49 @@ describe('Lifecycle methods', () => {
714714

715715
expect(info).to.deep.equal({});
716716
});
717+
718+
// New test mirroring React example but using shouldComponentUpdate instead of memo
719+
it('should recover and render siblings when child throws once and uses shouldComponentUpdate', () => {
720+
let causeError = true;
721+
722+
const Render = ({ n }) => <p>test {n}</p>;
723+
724+
class TestWithSCU extends Component {
725+
shouldComponentUpdate(nextProps) {
726+
return nextProps.n !== this.props.n;
727+
}
728+
render(props) {
729+
const { n } = props;
730+
if (n === 2 && causeError) {
731+
throw new Error('test error');
732+
}
733+
return <Render n={n} />;
734+
}
735+
}
736+
737+
class App extends Component {
738+
componentDidCatch() {
739+
causeError = false;
740+
this.setState({});
741+
}
742+
render() {
743+
return (
744+
<div>
745+
<h1>Example</h1>
746+
<TestWithSCU n={1} />
747+
<TestWithSCU n={2} />
748+
<TestWithSCU n={3} />
749+
</div>
750+
);
751+
}
752+
}
753+
754+
vi.spyOn(App.prototype, 'componentDidCatch');
755+
render(<App />, scratch);
756+
rerender();
757+
758+
expect(App.prototype.componentDidCatch).toHaveBeenCalledOnce();
759+
expect(scratch.textContent).to.equal('Exampletest 1test 2test 3');
760+
});
717761
});
718762
});

0 commit comments

Comments
 (0)