Skip to content

Commit 8cbed5f

Browse files
authored
Fix crash where a synchronous effect render unmounts the tree (#5026)
1 parent 4ac7204 commit 8cbed5f

File tree

2 files changed

+35
-5
lines changed

2 files changed

+35
-5
lines changed

hooks/src/index.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,13 +447,14 @@ export function useId() {
447447
function flushAfterPaintEffects() {
448448
let component;
449449
while ((component = afterPaintEffects.shift())) {
450-
if (!component._parentDom || !component.__hooks) continue;
450+
const hooks = component.__hooks;
451+
if (!component._parentDom || !hooks) continue;
451452
try {
452-
component.__hooks._pendingEffects.some(invokeCleanup);
453-
component.__hooks._pendingEffects.some(invokeEffect);
454-
component.__hooks._pendingEffects = [];
453+
hooks._pendingEffects.some(invokeCleanup);
454+
hooks._pendingEffects.some(invokeEffect);
455+
hooks._pendingEffects = [];
455456
} catch (e) {
456-
component.__hooks._pendingEffects = [];
457+
hooks._pendingEffects = [];
457458
options._catchError(e, component._vnode);
458459
}
459460
}

hooks/test/browser/useEffect.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,4 +636,33 @@ describe('useEffect', () => {
636636
expect(calls.length).to.equal(1);
637637
expect(calls).to.deep.equal(['doing effecthi']);
638638
});
639+
640+
it('should not crash when effect throws and component is unmounted by render(null) during flush', () => {
641+
// In flushAfterPaintEffects():
642+
// 1. Guard checks component.__hooks — truthy, passes
643+
// 2. invokeEffect runs the effect callback
644+
// 3. The callback calls render(null, scratch) which unmounts the tree
645+
// → options.unmount sets component.__hooks = undefined
646+
// 4. Resetting the hooks array to an empty array would throw an error
647+
let setVal;
648+
649+
function App() {
650+
const [val, _setVal] = useState(0);
651+
setVal = _setVal;
652+
useEffect(() => {
653+
if (val === 1) {
654+
render(null, scratch);
655+
}
656+
}, [val]);
657+
return <div>val: {val}</div>;
658+
}
659+
660+
act(() => {
661+
render(<App />, scratch);
662+
});
663+
664+
act(() => {
665+
setVal(1);
666+
});
667+
});
639668
});

0 commit comments

Comments
 (0)