Prevent updating an unmounted component during async event handler - reactjs

I have an async event handler that deletes a component, but this component is using state to watch the event handler execution status. The event handler is a mock of deleting an item from a remote database.
The problem is that upon successful deletion, the component is unmounted, so the final state update (to indicate that deletion is done) triggers the error "Can't perform a React state update on an unmounted component".
I understand that it is frequent classical issue, I would like to know what is the best way to solve it.
A sandbox:
The full code for reference:
import React from "react";
export default function App() {
const [fruits, setFruits] = React.useState(["apple", "pear", "peach"]);
return (
<ul>
{fruits.map((fruit) => (
<Row key={fruit} fruit={fruit} setFruits={setFruits} />
))}
</ul>
);
}
function Row({ fruit, setFruits }) {
const [isDeleting, setIsDeleting] = React.useState(false);
const handleDelete = async () => {
setIsDeleting(true);
try {
await deleteFruit(fruit, setFruits);
} catch (error) {
console.log("An error occured");
}
setIsDeleting(false);
};
return (
<li>
{fruit}
<button onClick={handleDelete} disabled={isDeleting}>
X
</button>
</li>
);
}
async function deleteFruit(fruitToDelete, setFruits) {
// mock remote DB
return new Promise((resolve) => {
setTimeout(() => {
setFruits((fruits) => fruits.filter((f) => f !== fruitToDelete));
resolve();
}, 1000);
});
}
I have tried to prevent the issue by recording if the component is mounted with useRef and useEffect. It works, but I find that it is not easily readable. Is there a more explicit method to achieve this behaviour?
In component Row's render function:
const refIsMounted = React.useRef(true);
React.useEffect(() => {
refIsMounted.current = true;
return () => {
refIsMounted.current = false;
};
}, []);
And the async event handler:
if (refIsMounted.current) {
setIsDeleting(false);
}

based on useEffect documents you can return a clean-up function.
Often, effects create resources that need to be cleaned up before the
component leaves the screen, such as a subscription or timer ID. To do
this, the function passed to useEffect may return a clean-up function.
so your code will be like below:
useEffect(()=>{
return ()=>{
setIsDeleting(false);}
},[])

I realized what was the issue: the lifetime of a component that triggers an async event handler is independent of the execution time of this async event handler
So the solution is either:
to put the state modified by the handler in a component which we know will outlive the handler, either higher in the component hierarchy or in Redux.
to use the useRef trick as described above to check whether the triggering component is still in existence
Here I lifted the state in the parent component:

Related

Trying to implement a cleanup in a useEffect to prevent no-op memory leak error

I am trying to update a piece of UI based on a conditional. The conditional is set by a database call in a separate component. It sometimes works, but often doesn't. When it doesn't work, it gets this 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.
I have followed other advice and tried to do a cleanup in a useEffect:
const [isPatched, setIsPatched] = useState<boolean>(false);
useEffect(() => {
x.Log ? setPatched(true) : setPatched(false);
return () => {
setIsPatched(false);
};
}, []);
const setPatched = (patched: boolean) => {
setIsPatched(patched);
};
Other component db call:
useEffect(() => {
if (disabled === true) {
const handle = setTimeout(() => setDisabled(false), 7000);
return () => clearTimeout(handle);
}
}, [disabled]);
function handleClick() {
[...]
const updatePatchedX = async (id: string) => {
//check if patched x already in db
const content = await services.xData.getxContent(id);
const xyToUpdated = content?.p[0] as T;
if (!xToUpdated.log) {
// add log property to indicate it is patched and put in DB
xToUpdated.log = [
{ cId: cId ?? "", uId: uId, appliedAt: Date.now() },
];
if (content) {
await services.xData
.updateOTxBook(id, content, uId)
.then(() => {
console.log("done");
setPatched(true);
setDisabled(true);
});
}
}
};
updatePatchedX(notebookID);
}
The UI is only fixed on refresh - not immediately, as the useEffect is supposed to achieve? Not sure where to go from here. Could be a race condition?
I have experienced this in the past and here's what I learned from it
This is normally caused by this sequence of events:
user clicks button => triggers API call => UI changes and the button gets unmounted => API call finishes and tries to update the state of a component that has been unmounted
If the action could be canceled then the default recommendation of "To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function." makes sense; but in this case the API call cannot be cancelled.
The only solution I've found to this is using a ref to track whether the component has been unmounted by the time the API call completes
Below a snippet with just the relevant changes for simplicity
const mainRef = useRef(null);
function handleClick() {
[...]
const updatePatchedX = async (id: string) => {
...
await services.xData
.updateOTxBook(id, content, uId)
.then(() => {
if (mainRef.current) {
console.log("done");
setPatched(true);
setDisabled(true);
}
});
...
};
updatePatchedX(notebookID);
}
return (
<div ref={mainRef}>.... <button onClick={handleClick}>...</button></div>
);
The above works because when the component gets unmounted the myRef references get emptied but you can still check its value when the API call eventually fulfills and before you use some setState function

What's a good pattern for invoking event handler props in useEffect?

Let's assume a component:
const Foo = ({id, onError}) => {
useEffect(() => {
subscribe(id).catch(error => onError(error));
return () => cleanup(id);
}, [id, onError]);
return <div>...</div>;
}
The idea is simple-- run an effect that subscribes using the current "id". If the subscription fails, invoke an event handler onError that is passed down as a prop.
However, for this to work correctly, the onError prop that's passed down must be referentially stable. In other words, if a consumer of my component tried the following, they may run into problems where the effect is run for each render:
const Parent = () => {
// handleError is recreated for each render, causing the effect to run each time
const handleError = error => {
console.log("error", error);
}
return <Foo id="test" onError={handleError} />
}
Instead, they would need to do something like:
const Parent = () => {
// handleError identity is stable
const handleError = useCallback(error => {
console.log("error", error);
},[]);
return <Foo id="test" onError={handleError} />
}
This works, but I'm somehow unhappy with it. The consumers of Foo need to realize that onError must be stable, which is not something that's plainly obvious unless you look at its underlying implementation. It breaks the component encapsulation and consumers can easily run into problems without realizing it.
Is there a better pattern to manage props like event handlers that may be invoked within useEffect?
You need to remove onError from your dependency list, but still call if it changes. For that you can use a ref, and update it via useEffect on each render.
You can also use optional chaining ?. to avoid invoking the function if it's undefined.
const Foo = ({ id, onError }) => {
const onErrorRef = useRef();
useEffect(() => {
onErrorRef.current = onError;
});
useEffect(() => {
subscribe(id).catch(error => onErrorRef.current?.(error));
return () => cleanup(id);
}, [id]);
return <div>...</div>;
}

In React, how to call function based on const value change

I have 3 components in react. One of them is the parent component and the remaining two are the child components (EziSchedule & EziTransaction), where each component is getting its own data by calling the API. however data to show in child component EziTransaction depends upon what record I click on table in schedule class.
when user click on record in EziSchedule, it goes back to parent component followed by EziTransaction and can print correct id in EziTransaction.
I need to refresh data in EziTransaction component which I am unable to do so. I believe I need to use state changed in order to call getEziTransactionData in EziTransaction component to refresh data but not sure how to do it.
Parent component
const EziTrackerParrent = () =>{
const [data, setData] = useState('schedule');
useEffect(() =>{
},[]);
return (
<div>
<h3>EziSchedule Table</h3>
<EziSchedule change={setData} ></EziSchedule>
<h3>EziTransaction Table</h3>
<EziTransaction data={data}></EziTransaction>
</div>
)
Child Component A - EziSchedule
Capture click event and pass it to parent
const EziSchedule = ({change}) =>{
Child Component B - EziTransaction
Get data from EziSchidule on click event via parent component. I need help here to ensure very time 'data' value changes, It call getEziTransactionData() and refresh html
const EziTransaction = ({data}) =>{
const [eziTransactionData, setEziTransactionData] = useState<IEziTransaction[]>();
useEffect(() => {
getEziTransactionData(); // this method call API to get data
},[]);
const getEziTransactionData = ()=>{ // I need to call this everytime user click on record in EziSchedule???
(async () =>{
try{
const result = //API Call...
setEziTransactionData(result);
........
return(
<div>
<div>I have received "{data}" from parent</div> // this values does change every click in EziSchedule
to trigger getEziTransactionData everytime dataupdates you need to pass data as dependency at useEffect
useEffect(() => {
getEziTransactionData();
},[data]);
You can run the useEffect hook on props/state change by adding that variable in dependency array if the useEffect hook.
In your case you can add data to dependency of useEffect and useEffect will run every time the variable data changes.
If you are using data in your method to make api call, you can make that function a callback and just add that function to the dependency of useEffect, so every time data will change, your callback function will change, which will trigger the useEffect hook.
const EziTransaction = ({data}) => {
const [eziTransactionData, setEziTransactionData] = useState<IEziTransaction[]>();
// wrap this function in a useCallback hook
const getEziTransactionData = useCallback(async () => {
try {
const result = await fetchApiData(data) // assuming you are using `data` in your API Call, if not add it to useEffect dependency and remove from this callback's dependency
setEziTransactionData(result);
} catch (err) {
console.log(err)
}
}, [setEziTransactionData, data]); // set dependency array of callback properly
useEffect(() => {
getEziTransactionData();
},[getEziTransactionData]); // set dependency array properly
return(
<div>
<div>I have received "{data}" from parent</div>
</div>
);
}
in addition to the answer below i would recomend to define you fetch function inside useEffect with data dependency to avoid extra memoization
useEffect(() => {
const getEziTransactionData = async () => {
try {
const result = await fetchApiData(data);
setEziTransactionData(result);
} catch (err) {
console.error(err);
}
});
getEziTransactionData();
}, [data]);

How to trace sources of state changes?

I have following hook in my react app:
const MyPage = React.FC = () => {
const myContext = useContext(MyContext);
useEffect(() => {
console.log(myContext);
}, [myContext]);
}
Effect hook though fires 3 times and that's expected and works as designed. But is there way to trace source of context changes so I could remove unnecessary changes?
1st approach: Don't use an effect hook, log where you are everytime a component updates the value provided by the context
// SomeComponents.js
export default () => {
const myContext = useContext(MyContext)
<button onClick={() => {window.console.log('In SomeComponents.js, onClick handler'); myContext.incrementCount()}>
${myContext.count}
</button>
}
2nd approach
More generally, there is a tool to find how to optimize the performance of your React app. It's called the React Profiler, available since React 16.5.
Failed approach: Throw and catch an error to get the stack
useEffect(() => {
try {
throw new Error('I should be caught')
} catch (e) {
window.console.log(e)
}
}, [state])
However the state was updated, the error has the same stack, so this gives us no clue.

Best practice to prevent state update warning for unmounted component from a handler

It is a common use-case to fetch and display the data from an external API (by using XHR requests) when a certain UI component (e.g. a <button />) is clicked. However, if the component was unmounted in the meantime, the following warning appears in the console:
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.
In fact, the most common solution (approved by #dan-abramov) to avoid the warning seems to keep track of the mount state of the component by using the return function of useEffect to cleanup.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
export default function PhotoList() {
const mounted = useRef(true);
const [photos, setPhotos] = useState(null);
useEffect(() => {
return () => {
mounted.current = false;
};
}, []);
function handleLoadPhotos() {
axios("https://jsonplaceholder.typicode.com/photos").then(res => {
if (mounted.current) {
setPhotos(res.data);
}
});
}
return (
<div>
<button onClick={handleLoadPhotos}>Load photos</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
However, this seems to cause unnecessary overhead to keep track of the mounting state and to check it before every state update. This becomes especially obvious when Observables (where you can unsubscribe) instead of Promises are used.
While you indeed can unsubscribe inside of the useEffect using the cleanup function in a very neat way:
useEffect(() => {
// getPhotos() returns an observable of the photo list
const photos$ = getPhotos().subscribe(setPhotos);
return () => photos$.unsubscribe();
}, []);
The same smart cleanup is not possible within a handler:
function handleLoadPhotos() {
const photos$ = getPhotos().subscribe(setPhotos);
// how to unsubscribe on unmounting?
}
Is there a best practice to avoid the warning without the ugly manual tracking of the mounting state with useRef()? Are there good approaches for that when using Observables?
Problem is that you are trying to fetch data in your component. This is not a good idea since the component could be unmounted and you would face many possible errors.
So that, you should look for other ways.
I always do async operations in redux thunks.
You should avoid your approach. Use redux and redux-thunk if you like. If not, try to find another solution to move async operations outside of your components.
In fact, you should be writing declarative ui components which renders for given props. So that, your data should be outside of your components logic too.
That's an awesome question! This is how I would do it:
First, define a helper function (it's not cheating because it really is a highly reusable function whenever you're dealing with React and observables combined):
import * as React from 'react';
import { Observable } from 'rxjs';
export const useObservable = <Value>(
arg: () => {
observable: Observable<Value>;
value: Value;
},
) => {
const { observable, value } = React.useMemo(arg, []);
const [state, setState] = React.useState<Value>(value);
React.useEffect(() => {
const subscription = observable.subscribe(value => setState(value));
return () => subscription.unsubscribe();
}, []);
return state;
};
Just to help illustrate what this function does, the following component will display the latest value emitted by myObservable:
() => {
const value = useObservable(() => ({
observable: myObservable,
value: 'Nothing emitted yet',
}));
return <span>{value}</span>;
};
Your component will then look like this:
export default function PhotoList() {
const clicksSubject = React.useMemo(() => new Subject<undefined>(), []);
const photos = useObservable(() => ({
observable: clicksSubject.pipe(
switchMap(() => axiosApiCallReturningAnObservable()),
map(res => res.data),
),
value: null,
}));
return (
<div>
<button
onClick={() => {
clicksSubject.next(undefined);
}}
>
Load photos
</button>
{photos && <p>Loaded {photos.length} photos</p>}
</div>
);
}
When the component is dismounted, useObservable unsubs from the observable that was passed to it. This makes sure that we don't at a later point attempt to set the state, and that the data fetching API aborts (or at least gets a chance to abort) the HTTP request.

Resources