Chris Morris
Chris Morris

Reputation: 983

React can't perform state update on unmounted component

I'm using the following method to control my header from other components. However I'm getting the old "can't perform a react state update on unmounted component" error when changing page

export const store = {
    state: {},
    setState(value) {
        this.state = value;
        this.setters.forEach(setter => setter(this.state));
    },
    setters: []
};
store.setState = store.setState.bind(store);

export function useStore() {
    const [ state, set ] = useState(store.state);
    if (!store.setters.includes(set)) {
        store.setters.push(set);
    }

    return [ state, store.setState ];
}

My header then uses it to set a class and control if it needs to be black on white or white on black

const Header = () => {
    const [type] = useStore();
    render( ... do stuff )
};

And my components on page import useStore and then call setType based on a number of factors, certain layouts are one type, some others, some vary depending on API calls so there are a lot of different Components that need to call the function to set the headers state.

const Flexible = (props) => {
    const [type, setType] = useStore();
    if( type !== 'dark ){ setType('dark') }
    ... do stuff
};

The header its self is always on page, is before and outside the router and never unmounts.

This all works perfectly fine and sets the headers sate. However when I change page with React Router I get the can't set state error. I can't see why I would get this error. I first thought that the Component might be trying to run again with react router so I moved the code to set the headers state into a useEffect that only runs on initialisation but that didn't help.

Upvotes: 1

Views: 591

Answers (2)

Josep Vidal
Josep Vidal

Reputation: 2661

This error is pretty straightforward it means that you are mutating the state (calling setState) in a component that is not mounted.

This mostly happens with promises, you call a promise, then when its resolved you update the state, but if you switch the page before it resolves, when the promise is resolved it still tries to update the state of a component that now its not mounted.

The easy and "ugly" solution, is to use some parameter that you control in componentWillUnmout to check if you still need to update the state or not like this:

var mounted = false;
componentWillMount(){
 mounted = true
}
componentWillUnmount(){
 mounted = false
}
// then in the promise
// blabla
promise().then(response => {
 if(mounted) this.setState();
})

Upvotes: 1

Nicholas Tower
Nicholas Tower

Reputation: 84902

You only ever add to the setters, never remove. So when a component unmounts, it will remain in the setters, and the next time some other part of the app tries to set the state, all the setters get called, including the setter for the unmounted component. This then results in the error you're seeing.

You'll need to modify your custom hook to make use of useEffect, so that you can have teardown logic when unmounting. Something like this:

export function useStore() {
  const [ state, set ] = useState(store.state);
  useEffect(() => {
    store.setters.push(set);
    return () => {
      const i = store.setters.indexOf(set);
      if (i > -1) {
        store.setters.splice(i, 1);
      }
    }      
  }, []);

  return [ state, store.setState ];
}

Upvotes: 1

Related Questions