lostpebble / pullstate

Simple state stores using immer and React hooks - re-use parts of your state by pulling it anywhere you like!

Home Page:https://lostpebble.github.io/pullstate

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Can't perform a React state update on an unmounted component. When switching view with react route

hostlund opened this issue · comments

I'm trying to use PullState in an app with a few react routes and get the error " Can't perform a React state update on an unmounted component." when I use some kind of callback to update the store. Small example inlined. Switch view and press the Test 1, 2 or 3 link. In the console you get (Can't perform a React state update on an unmounted component.) Seems to be "setUpdateTrigger((val) => val + 1);" in useStoreState that triggers the warning. Are we initializing the store the wrong way or should this work?

setUpdateTrigger((val) => val + 1);

store.js

import { Store } from 'pullstate';

export const UIStore = new Store({
  clickTimes: 0
});

Quick example, based on create react app and added react router

import React from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import { useStoreState } from 'pullstate';
import { UIStore } from './store';

export default function BasicExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/dashboard">Dashboard</Link>
          </li>
        </ul>

        <hr />

        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

// You can think of these components as "pages"
// in your app.

function Home() {
    const stateNR = useStoreState(UIStore, (s) => s.clickTimes);
  const updateNumber = () =>  {
    UIStore.update((s) => {
      // eslint-disable-next-line no-param-reassign
      s.clickTimes = s.clickTimes +1
    });
  }
  return (
    <div>
      <h2>Home</h2>
      <a href="#" onClick={(e) => { e.preventDefault(); updateNumber()}}>Test 1</a>
      <br />
      {stateNR}
    </div>
  );
}

function About() {
  const stateNR = useStoreState(UIStore, (s) => s.clickTimes);
  const updateNumber2 = () =>  {
    UIStore.update((s) => {
      // eslint-disable-next-line no-param-reassign
      s.clickTimes = s.clickTimes +1
    });
  }
  return (
    <div>
      <h2>About</h2>
      <a href="#" onClick={(e) => { e.preventDefault(); updateNumber2()}}>Test 2</a>
      <br />
      {stateNR}
    </div>
  );
}

function Dashboard() {
  const stateNR = useStoreState(UIStore, (s) => s.clickTimes);
  const updateNumber3 = () =>  {
    UIStore.update((s) => {
      // eslint-disable-next-line no-param-reassign
      s.clickTimes = s.clickTimes +1
    });
  }
  return (
    <div>
      <h2>Dashboard</h2>
      <a href="#" onClick={(e) => { e.preventDefault(); updateNumber3()}}>Test 3</a>
      <br />
      {stateNR}
    </div>
  );
}

Hi @hostlund ,

Thanks for bringing this to my attention. I've looked into it, and reproduced what is happening in a Codesandbox here: https://codesandbox.io/s/react-and-pullstate-state-update-leak-cxbky

The strange thing is, in my dev environment I'm not getting these errors at all. Only in the sandbox.


Okay, I just figured out what the difference was between the environments. Its because of <React.StrictMode> wrapping your app. I think I've run into similar issues with strict mode before... It tends to run some things twice which can cause some unintended issues with some libraries. See Apollo as well: apollographql/apollo-client#6209

I don't really know what to do, because as far as I can see the pullstate code is doing everything expected for React components when it comes to mounting and unmounting, and I've never got those warnings about Pullstate popping without StrictMode turned on (but I still get warnings where there obviously are violations of the rule).

You can see the relevant code here:

seEffect(() => {
    updateRef.current.shouldUpdate = true;

    return () => {
      updateRef.current.shouldUpdate = false;
      store._removeUpdateListener(updateRef.current.onStoreUpdate!);
    };
  }, []);

That ref.current.shouldUpdate is a flag that is instantly turned off when the component unmounts, so it should never ever run any state updates after that:

if (updateRef.current.shouldUpdate) {
    updateRef.current.currentSubState = nextSubState;
    setUpdateTrigger((val) => val + 1);
}

It is something to look into in more depth, so I'll keep an eye on it, but I have my suspicions that its a false non-issue caused by StrictMode that we can probably safely ignore it.

I've looked a bit into it now and noticed this issue, which could be a clue: facebook/react#17193
It mentions that useRef state might not be persisting properly in strict mode.

Thanks for the clarification @lostpebble

Disabling strict mode was one thing I didn't try. It seems to work without any errors if I remove strict mode so I guess it due to that React issue. I will close this issue now.