scottrippey/next-router-mock

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 👍