How to Preserve State When Component Is Unmounted and Then Mounted Again React Native
React state update on an unmounted component
October 23, 2019 - nine min read
If yous are a react developer, there is a good chance that you faced this warning at least once:
Alarm: Can't perform a React state update on an unmounted component. This is a no-op, only it indicates a retentivity leak in your application. To gear up, abolish all subscriptions and asynchronous tasks in a useEffect cleanup role.
In order to understand how to fix this warning, nosotros need to understand why it is happening. Nosotros volition need to reproduce information technology in a consequent style.
⚠️ Note that in this article i use react hooks, if you are using react class components yous may see in the alarm a reference to componentWillUnmount instead of the useEffect cleanup part.
Reproduce the alert
👀 I've uploaded a starter repo to github so you won't have to copy paste the code. Yous can clone and run information technology locally or use the import feature of codesandbox.io
If we look at the warning again, we tin see that there are 2 main parts playing a role here:
- A React state update
- An unmounted component
In club to create these, we will build this simple drop-down with asynchronous data fetching
State updates
function Pets ( ) { const [pets, dispatch] = useReducer (petsReducer, initialState) ; const onChange = ( { target } ) => { acceleration ( { type: "PET_SELECTED" , payload: target.value } ) ; } ; useEffect ( ( ) => { if (pets.selectedPet) { dispatch ( { type: "FETCH_PET" } ) ; getPet (pets.selectedPet) . and then ( data => { dispatch ( { type: "FETCH_PET_SUCCESS" , payload: data } ) ; } ) ; } else { acceleration ( { blazon: "RESET" } ) ; } } , [pets.selectedPet] ) ; render ( <div > <select value = {pets.selectedPet} onChange = {onChange} > <option value = " " > Select a pet </pick > <selection value = "cats" > Cats </option > <option value = "dogs" > Dogs </pick > </select > {pets.loading && <div > Loading... </div > } {pets.petData && < Pet { ... pets . petData } /> } </div > ) ; } Here nosotros take the Pets component, it uses the useReducer hook to store some state. Lets run into the petsReducer and the initial state:
const initialState = { loading: imitation , selectedPet: "" , petData: null } function petsReducer ( country, activity ) { switch (action.type) { case "PET_SELECTED" : { return { ...country, selectedPet: activity.payload } ; } case "FETCH_PET" : { return { ...state, loading: truthful , petData: null } ; } case "FETCH_PET_SUCCESS" : { return { ...state, loading: simulated , petData: action.payload } ; } case "RESET" : { return initialState; } default : throw new Fault ( ` Non supported action ${action.type} ` ) ; } } Equally yous tin see theres zilch special hither, a simple reducer that manage our state.
The Pets component as well apply the useEffect hook for some side furnishings similar fetching the data of our selected pet, we invoke the getPet office which returns a Promise and we dispatch the FETCH_PET_SUCCESS action with the returned information equally the payload to update our state.
Note that getPet is non really hit a server end-point, its just a part that simulate a server phone call. This is how information technology looks like:
const petsDB = { dogs: { name: "Dogs" , voice: "Woof!" , avatar: "🐶" } , cats: { proper name: "Cats" , vocalism: "Miauuu" , avatar: "🐱" } } ; consign function getPet ( type ) { render new Promise ( resolve => { // simulate a fetch call setTimeout ( ( ) => { resolve (petsDB[type] ) ; } , 1000 ) ; } ) ; } As you run across, its nothing only a setTimeout inside a Promise .
Our App is basically just rendering this Pets component:
part App ( ) { return ( <div > < Pets /> </div > ) ; } Ok first part of our trouble is accomplished, thats the React state update , now we need to create the 2nd part - An unmounted component .
Un-mounting a component
This is relatively piece of cake to accomplish using a country and a conditional rendering, we volition store a boolean flag at the App level and we will render the <Pets /> component accordingly while using a toggle button.
function App ( ) { const [showPets, setShowPets] = useState ( truthful ) ; const toggle = ( ) => { setShowPets ( country => !state) ; } ; return ( <div > <push button onClick = {toggle} > {showPets ? "hide" : "show" } </button > {showPets && < Pets /> } </div > ) ; } This is how our application should wait like
Reproduction
Ok, at present that we have both conditions for the alarm to appear lets try it. If we expect again at the warning:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but information technology indicates a memory leak in your application. To set, abolish all subscriptions and asynchronous tasks in a useEffect cleanup role.
Lets focus on this line here:
React state update on an unmounted component
If we select a pet, we know that information technology will have our getPet at least ane second to render our data. Afterwards our information is returned we are updating the land, if we will un-mount the Pet component before that 1 2d (before our information is received) we will trigger an update on an unmounted component.
So this is how you do information technology: *If you tin can't get in with a 1 second delay, try to increase the timeOut in the getPet part.
OK this is office one of our task, now nosotros need to set it.
The fix
You may exist surprised but the fix for this issue is actually the like shooting fish in a barrel part. React is providing a clear and a very helpful bulletin, with a guidance to the solution:
To ready, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Well, we may not exactly subscribing to anything hither, but we do have an asynchronous tasks, call back the getPet asynchronous function:
function Pets ( ) { const [pets, dispatch] = useReducer (petsReducer, initialState) ; const onChange = ( { target } ) => { dispatch ( { type: "PET_SELECTED" , payload: target.value } ) ; } ; useEffect ( ( ) => { if (pets.selectedPet) { dispatch ( { blazon: "FETCH_PET" } ) ; getPet (pets.selectedPet) . then ( data => { dispatch ( { type: "FETCH_PET_SUCCESS" , payload: information } ) ; } ) ; } else { dispatch ( { type: "RESET" } ) ; } } , [pets.selectedPet] ) ; render ( <div > <select value = {pets.selectedPet} onChange = {onChange} > <selection value = " " > Select a pet </option > <option value = "cats" > Cats </option > <option value = "dogs" > Dogs </choice > </select > {pets.loading && <div > Loading... </div > } {pets.petData && < Pet { ... pets . petData } /> } </div > ) ; } So basically we just need to Non update the state in the callback if the component is non mounted already.
function Pets ( ) { const [pets, dispatch] = useReducer (petsReducer, initialState) ; const onChange = ( { target } ) => { acceleration ( { type: "PET_SELECTED" , payload: target.value } ) ; } ; useEffect ( ( ) => { let mounted = true ; if (pets.selectedPet) { dispatch ( { type: "FETCH_PET" } ) ; getPet (pets.selectedPet) . then ( data => { if (mounted) { acceleration ( { blazon: "FETCH_PET_SUCCESS" , payload: data } ) ; } } ) ; } else { dispatch ( { type: "RESET" } ) ; } return ( ) => mounted = simulated ; } , [pets.selectedPet] ) ; return ( <div > <select value = {pets.selectedPet} onChange = {onChange} > <option value = " " > Select a pet </choice > <pick value = "cats" > Cats </pick > <option value = "dogs" > Dogs </choice > </select > {pets.loading && <div > Loading... </div > } {pets.petData && < Pet { ... pets . petData } /> } </div > ) ; } Every fourth dimension our effect volition run, we are setting a local variable mounted to true, we ready information technology to false on the cleanup function of the upshot (like suggested by react). And most importantly, nosotros are updating the state if and merely if that value is true, that is if the component is un-mounted meaning our variable is set to false, it wont enter the if cake.
And then this is it, we are no longer receiving the warning:
Bonus Tip
We set a local variable within the useEffect scope, if we want to re-use this variable inside some other useEffect we can use useRef, which is sort of a none rendering country for components.
For example:
part Pets ( ) { const [pets, dispatch] = useReducer (petsReducer, initialState) ; const isMountedRef = useRef ( null ) ; const onChange = ( { target } ) => { dispatch ( { type: "PET_SELECTED" , payload: target.value } ) ; } ; useEffect ( ( ) => { isMountedRef.current = truthful ; if (pets.selectedPet) { acceleration ( { blazon: "FETCH_PET" } ) ; getPet (pets.selectedPet) . then ( data => { if (isMountedRef.current) { dispatch ( { type: "FETCH_PET_SUCCESS" , payload: data } ) ; } } ) ; } else { acceleration ( { type: "RESET" } ) ; } render ( ) => isMountedRef.current = fake ; } , [pets.selectedPet] ) ; useEffect ( ( ) => { // we can access isMountedRef.current here as well } ) return ( <div > <select value = {pets.selectedPet} onChange = {onChange} > <option value = " " > Select a pet </selection > <option value = "cats" > Cats </option > <option value = "dogs" > Dogs </choice > </select > {pets.loading && <div > Loading... </div > } {pets.petData && < Pet { ... pets . petData } /> } </div > ) ; } The great thing almost hooks is that we can extract this tiny logic to a custom hook and reuse it across components. 1 possible implementation tin can exist something like this:
function useIsMountedRef ( ) { const isMountedRef = useRef ( null ) ; useEffect ( ( ) => { isMountedRef.electric current = truthful ; render ( ) => isMountedRef.electric current = false ; } ) ; return isMountedRef; } function Pets ( ) { const [pets, dispatch] = useReducer (petsReducer, initialState) ; const isMountedRef = useIsMountedRef ( ) ; const onChange = ( { target } ) => { dispatch ( { type: "PET_SELECTED" , payload: target.value } ) ; } ; useEffect ( ( ) => { if (pets.selectedPet) { dispatch ( { type: "FETCH_PET" } ) ; getPet (pets.selectedPet) . and so ( data => { if (isMountedRef.current) { dispatch ( { type: "FETCH_PET_SUCCESS" , payload: data } ) ; } } ) ; } else { dispatch ( { type: "RESET" } ) ; } } , [pets.selectedPet, isMountedRef] ) ; return ( <div > <select value = {pets.selectedPet} onChange = {onChange} > <option value = " " > Select a pet </selection > <option value = "cats" > Cats </option > <pick value = "dogs" > Dogs </option > </select > {pets.loading && <div > Loading... </div > } {pets.petData && < Pet { ... pets . petData } /> } </div > ) ; } Custom useEffect
If nosotros want to get real crazy with our hooks, nosotros can create our own custom useEffect (or useLayoutEffect) which volition provide us the "current status" of the effect:
part useAbortableEffect ( effect, dependencies ) { const condition = { } ; // mutable status object useEffect ( ( ) => { status.aborted = false ; // laissez passer the mutable object to the effect callback // store the returned value for cleanup const cleanUpFn = effect (condition) ; return ( ) => { // mutate the object to betoken the consumer // this result is cleaning up condition.aborted = truthful ; if ( typeof cleanUpFn === "office" ) { // run the cleanup role cleanUpFn ( ) ; } } ; } , [ ...dependencies] ) ; } And nosotros will use it in our Pet component like this:
useAbortableEffect ( ( condition ) => { if (pets.selectedPet) { dispatch ( { type: "FETCH_PET" } ) ; getPet (pets.selectedPet) . then ( information => { if ( !condition.aborted) { dispatch ( { type: "FETCH_PET_SUCCESS" , payload: data } ) ; } } ) ; } else { dispatch ( { type: "RESET" } ) ; } } , [pets.selectedPet] ) ; Note how our custom issue callback now accepts a status argument which is an object that contains an aborted boolean property. If it is fix to true, that means our effect got cleaned and re-run (which ways our dependencies are changed or the component was un-mounted).
I kind of like this pattern and i wish react useEffect would get usa this behavior out of the box. I even created an RFC on the react repo for this if y'all want to comment, better it or only upward-vote with a like and then it will take a better chance to get implemented..
Wrapping up
We saw how a elementary component with an asynchronous state update may yield this common alarm, think nigh all those components you have with a similar case. Make sure yous check if the component is actually mounted before you perform a state update.
Hope you found this article helpful, if you have a dissimilar approach or whatsoever suggestions i would honey to hear almost them, you can tweet or DM me @sag1v. 🤓
Source: https://www.debuggr.io/react-update-unmounted-component/
Post a Comment for "How to Preserve State When Component Is Unmounted and Then Mounted Again React Native"