Can't perform a React state update on an unmounted component
luchsamapparat opened this issue · 3 comments
Hi,
first of all, thanks for providing this library! I have no idea one should write integration tests for Next.js without it 🤔
I've written a couple of tests for a Next.js page component, but even though all tests basically render the same, only one test throws this warning:
console.error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at Link (C:\Users\{project}\node_modules\next\client\link.tsx:131:19)
at LinkIconButton (C:\Users\{project}\libs\ui\src\lib\button.tsx:61:23)
at div
at td
at TCell (C:\Users\{project}\libs\ui\src\lib\table\table.tsx:59:78)
at tr
at TRow (C:\Users\{project}\libs\ui\src\lib\table\table.tsx:37:87)
at AufgabenListItem (C:\Users\{project}\apps\sales-planner\src\components\aufgaben-list\aufgaben-list-item.tsx:18:36)
at printWarning (../../node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (../../node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnAboutUpdateOnUnmountedFiberInDEV (../../node_modules/react-dom/cjs/react-dom.development.js:23914:9)
at scheduleUpdateOnFiber (../../node_modules/react-dom/cjs/react-dom.development.js:21840:5)
at dispatchAction (../../node_modules/react-dom/cjs/react-dom.development.js:16139:5)
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:24:7)
at ../../node_modules/next-router-mock/src/lib/mitt/index.ts:38:9
at Array.map (<anonymous>)
I tried to create a minimal reproduction of the issue, but I really struggle to pinpoint the underlying problem. But I noticed one thing which probably is the cause.
I modified the code of useMemoryRouter
to output some log statements:
// Trigger updates on route changes:
useEffect(() => {
const id = lodash.uniqueId();
console.log(`subscribe: ${id}`);
const handleRouteChange = () => {
console.log(`handleRouteChange: ${id}`);
// Ensure the reference changes each render:
setRouter(MemoryRouter.snapshot(singletonRouter));
};
singletonRouter.events.on("routeChangeComplete", handleRouteChange);
return () => {
console.log(`UNsubscribe: ${id}`);
singletonRouter.events.off("routeChangeComplete", handleRouteChange);
};
}, [singletonRouter]);
Basically, it logs when useMemoryRouter
subscribes and unsubscribes to routeChangeComplete
and also logs every call of handleRouteChange
. And each subscription gets a unique ID.
The interesting thing is this: In the following excerpt of my test log, there is a handleRouteChange
execution for Subscription 7 even though the same subscription has already been unsubscribed. That probably explains why react complains about a state update on an unmounted component.
console.log
UNsubscribe: 7
at ../../node_modules/next-router-mock/src/useMemoryRouter.tsx:28:25
console.log
UNsubscribe: 6
at ../../node_modules/next-router-mock/src/useMemoryRouter.tsx:28:25
console.log
handleRouteChange: 5
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:22:5)
at Array.map (<anonymous>)
console.log
handleRouteChange: 6
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:22:5)
at Array.map (<anonymous>)
console.error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at Link (C:\Users\Marvin\Projekte\tbit\oneplatform\node_modules\next\client\link.tsx:131:19)
at LinkIconButton (C:\Users\Marvin\Projekte\tbit\oneplatform\libs\ui\src\lib\button.tsx:61:23)
at div
at td
at TCell (C:\Users\Marvin\Projekte\tbit\oneplatform\libs\ui\src\lib\table\table.tsx:59:78)
at tr
at TRow (C:\Users\Marvin\Projekte\tbit\oneplatform\libs\ui\src\lib\table\table.tsx:37:87)
at AufgabenListItem (C:\Users\Marvin\Projekte\tbit\oneplatform\apps\sales-planner\src\components\aufgaben-list\aufgaben-list-item.tsx:18:36)
at printWarning (../../node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (../../node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnAboutUpdateOnUnmountedFiberInDEV (../../node_modules/react-dom/cjs/react-dom.development.js:23914:9)
at scheduleUpdateOnFiber (../../node_modules/react-dom/cjs/react-dom.development.js:21840:5)
at dispatchAction (../../node_modules/react-dom/cjs/react-dom.development.js:16139:5)
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:24:7)
at ../../node_modules/next-router-mock/src/lib/mitt/index.ts:38:9
at Array.map (<anonymous>)
console.log
handleRouteChange: 7
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:22:5)
at Array.map (<anonymous>)
console.error
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at AufgabenListItem (C:\Users\Marvin\Projekte\tbit\oneplatform\apps\sales-planner\src\components\aufgaben-list\aufgaben-list-item.tsx:18:36)
at printWarning (../../node_modules/react-dom/cjs/react-dom.development.js:67:30)
at error (../../node_modules/react-dom/cjs/react-dom.development.js:43:5)
at warnAboutUpdateOnUnmountedFiberInDEV (../../node_modules/react-dom/cjs/react-dom.development.js:23914:9)
at scheduleUpdateOnFiber (../../node_modules/react-dom/cjs/react-dom.development.js:21840:5)
at dispatchAction (../../node_modules/react-dom/cjs/react-dom.development.js:16139:5)
at handleRouteChange (../../node_modules/next-router-mock/src/useMemoryRouter.tsx:24:7)
at ../../node_modules/next-router-mock/src/lib/mitt/index.ts:38:9
at Array.map (<anonymous>)
It seems that there's a short window where the MittEmitter
behind singletonRouter.events
still executes some event handlers that shouldn't be there anymore at that point.
I validated this with this dirty hack which actually solves the issue:
useEffect(() => {
let isUnsubscribed = false;
const handleRouteChange = () => {
if (!isUnsubscribed) {
// Ensure the reference changes each render:
setRouter(MemoryRouter.snapshot(singletonRouter));
}
};
singletonRouter.events.on("routeChangeComplete", handleRouteChange);
return () => {
isUnsubscribed = true;
singletonRouter.events.off("routeChangeComplete", handleRouteChange);
}
}, [singletonRouter]);
Im aware that it's unfortunate, that I cannot provide a reproduction for this issue, but the project I'm working on is proprietary and my attempts to extract the issue have failed so far.
I can totally see how this could be a problem. While it might be hard to repro, I'll see what I can do ... and I don't think it would be too hard to utilize this isUnsubscribed
logic, that's exactly how I'd solve it! I'll see what I can do.
Pushed a PR to fix this: #26
Can you review and approve if it looks good? :) Thanks
Thanks for the quick response!
It works like a charm 👍