How to ignore previous async effects when useEffect is called again? - reactjs

I have a simple component that makes an async request when some state changes:
const MyComp = () => {
const [state, setState] = useState();
const [result, setResult] = useState();
useEffect(() => {
fetchResult(state).then(setResult);
}, [state]);
return (
<div>{result}</div>
);
};
The problem is, sometimes the state changes twice in a short lapse of time, and the fetchResult function can take a very different amount of time to resolve according to the state value, so sometimes this happens:
As you can guess, as state now is state2 and not state1 anymore, I would like result to be result2, ignoring the response received in the then of the -obsolete- first effect call.
Is there any clean way to do so?

I would suggest you setup some kind of request cancellation method in the useEffect cleanup function.
For example with axios, it looks like that:
const MyComp = () => {
const [state, setState] = useState();
const [result, setResult] = useState();
useEffect(() => {
const source = axios.CancelToken.source();
fetchResult({state, cancelToken: source.cancelToken }).then(setResult);
return () => {
source.cancel()
}
}, [state]);
return (
<div>{result}</div>
);
};
You have a similar API with fetch called AbortController
What this will do is it will cancel the stale requests if your state changed so only the last one will resolve (and set result).

I've not tested this... but my initial thought would be if you have the state in the response, you could check if the state fetched matches the current state. If not, then the state has changed since the request and you no longer care about the response so don't set it.
useEffect(() => {
fetchResult(state).then((response) => {
response.state === state ? setResult(response.data) : false;
});
}, [state]);
You might also be able to do it by keeping a record of the fetchedState on each request.. and again discard it if it no longer matches.
useEffect(() => {
let fetchedState = state;
fetchResult(fetchedState).then((response) => {
fetchedState === state ? setResult(response) : false;
});
}, [state]);

I've built something like the below in order to only ever use the last result of the last request sent:
const REQUEST_INTERVAL = 2000
const MyComponent = () => {
const [inputState, setInputState] = useState();
const [result, setResult = useState()
const requestIndex = useRef(0)
useEffect(() => {
const thisEffectsRequestIndex = requestIndex.current + 1
requestIndex.current = thisEffectsRequestIndex
setTimeout(() => {
if(thisEffectsRequestIndex === requestIndex.current) {
fetch('http://example.com/movies.json')
.then((response) => {
if(thisEffectsRequestIndex === requestIndex.current) {
setResult(response.json())
}
})
}
})
, REQUEST_INTERVAL)
}, [inputState])
return <div>{result}</div>
}

Related

How to access state inside a listener inside a useEffect?

I understand that without dependencies you cannot access state inside useEffect. An example as such :
const [state, setState] = React.useState('')
const ws = new WebSocket(`ws://localhost:5000`)
React.useEffect(() => {
ws.onopen = () => {
console.log(state)
},[])
In this case state comes back as empty even though it has changed in other parts of the code. To solve this in principle I should add state as dependency to the useEffect hook. However, this will trigger the listener again and I do not want to have 2 active listeners on my websocket or being forced to close and reopen it again as such :
React.useEffect(() => {
ws.onopen = () => {
console.log(state)
},[state])
What is good practice when it comes to accessing state inside a listener that sits inside useEffect hook?
IF you need to re-render the component when the state changes try this:
const [state, setState] = React.useState('');
const stateRef = React.useRef(state);
React.useEffect(() => {
stateRef.current = state;
}, [state])
const ws = React.useMemo(() => {
const newWS = new WebSocket(`ws://localhost:5000`);
newWS.onopen = () => {
console.log(stateRef.current);
}
return newWS;
}, []);
This way you create the ws only once, and it will use the state reference which will be up to date because of the useEffect.
If you don't need to re-render the component when the state updates you can remove const [state, setState] = React.useState(''); and the useEffect and just update the stateRef like this when you need.
Like this:
const stateRef = React.useRef(null);
const ws = React.useMemo(() => {
const newWS = new WebSocket(`ws://localhost:5000`);
newWS.onopen = () => {
console.log(stateRef.current);
}
return newWS;
}, []);
// Update the state ref when you need:
stateRef.current = newState;
Best Practice get data in listeners.[UPDATED]!
const [socketData , setSocketData] = useState(null);
useEffect(() => {
websocet.open( (data) => {
setSocketData(data);
})
},[])
//second useEffect to check socketData
useEffect(() => {
if(socketData){
// access to data which come from websock et
}
},[socketData])

useState initial value don't use directly the value

I have an initial state that I never use directly in the code, only inside another set value state
Only a scratch example:
interface PersonProps {}
const Person: React.FC<PersonProps> = () => {
const [name, setName] = useState<string>("")
const [todayYear, setTodayYear] = useState<string>("")
const [birthYear, setBirthYear] = useState<string>("")
const [age, setAge] = useState<string>("")
const getPerson = async () => {
try {
const response = await getPersonRequest()
const data = await response.data
setName(data.name)
setTodayYear(data.today_year)
setBirthYear(data.future_year)
setAge(data.todayYear - data.birthYear)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getPerson()
})
return (
<h1>{name}</h1>
<h2>{age}</h2>
)
}
export default Person
In this case as you can see I will never use "todayYear" and "birthYear" on UI, so code give a warning
todayYear is assigned a value but never used
What can I do to fix this and/or ignore this warning?
If you don't use them for rendering, there's no reason to have them in your state:
const Person: React.FC<PersonProps> = () => {
const [name, setName] = useState<string>("")
const [age, setAge] = useState<string>("")
const getPerson = async () => {
try {
const response = await getPersonRequest()
const data = await response.data
setName(data.name)
setAge(data.todayYear - data.birthYear)
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getPerson()
})
return (
<h1>{name}</h1>
<h2>{age}</h2>
)
}
Side note: In most cases, you can leave off the type argument to useState wen you're providing an intial value. There's no difference between:
const [name, setName] = useState<string>("")
and
const [name, setName] = useState("")
TypeScript will infer the type from the argument. You only need to be explicit when inference can't work, such as if you have useState<Thingy | null>(null).
As this other answer points out, unless you want your code to run every time your component re-renders (which would cause an infinite render loop), you need to specify a dependency array. In this case, probably an empty one if you only want to get the person information once.
Also, since it's possible for your component to be unmounted before the async action occurs, you should cancel your person request if it unmounts (or at least disregard the result if unmounted):
const Person: React.FC<PersonProps> = () => {
const [name, setName] = useState<string>("");
const [age, setAge] = useState<string>("");
const getPerson = async () => {
const response = await getPersonRequest();
const data = await response.data;
return data;
};
useEffect(() => {
getPerson()
.then(data => {
setName(data.name)
setAge(data.todayYear - data.birthYear)
})
.catch(error => {
if (/*error is not a cancellation*/) {
// (Probably better to show this to the user in some way)
console.log(error);
}
});
return () => {
// Cancel the request here if you can
};
}, []);
return (
<h1>{name}</h1>
<h2>{age}</h2>
);
};
If it's not possible to cancel the getPersonRequest, the fallback is a flag:
const Person: React.FC<PersonProps> = () => {
const [name, setName] = useState<string>("");
const [age, setAge] = useState<string>("");
const getPerson = async () => {
const response = await getPersonRequest();
const data = await response.data;
return data;
};
useEffect(() => {
let mounted = true;
getPerson()
.then(data => {
if (mounted) {
setName(data.name)
setAge(data.todayYear - data.birthYear)
}
})
.catch(error => {
// (Probably better to show this to the user in some way)
console.log(error);
});
return () => {
mounted = false;
};
}, []);
return (
<h1>{name}</h1>
<h2>{age}</h2>
);
};
I also would like to mention one more thing. It's not related to your question but I think it's important enough to talk about it.
you need to explicitly state your dependencies for useEffect
In your case, you have the following code
useEffect(() => {
getPerson()
})
it should be written as follow if you want to trigger this only one time when a component is rendered
useEffect(() => {
getPerson()
}, [])
or if you want to trigger your side effect as a result of something that has changed
useEffect(() => {
getPerson()
}, [name])
If this is not clear for I suggest read the following article using the effect hook

How to change the parameter of hook in react (fetch hook)

My fetch hook:
import { useEffect, useState } from "react";
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [fetchedData, setFetchedData] = useState(initialData);
useEffect(() => {
let unmounted = false;
const handleFetchResponse = response => {
if (unmounted) return initialData;
setHasError(!response.ok);
setIsLoading(false);
return response.ok && response.json ? response.json() : initialData;
};
const fetchData = () => {
setIsLoading(true);
return fetch(url, { credentials: 'include' })
.then(handleFetchResponse)
.catch(handleFetchResponse);
};
if (initialUrl && !unmounted)
fetchData().then(data => !unmounted && setFetchedData(data));
return () => {
unmounted = true;
};
}, [url]);
return { isLoading, hasError, setUrl, data: fetchedData };
};
I call this hook in a function like so:
//states
const [assignments, setAssignments] = useState([])
const [submissions, setSubmissions] = useState([])
const [bulk_edit, setBulk_edit] = useState(false)
const [ENDPOINT_URL, set_ENDPOINT_URL] = useState('https://jsonplaceholder.typicode.com/comments?postId=1')
let url = 'https://jsonplaceholder.typicode.com/comments?postId=1';
const { data, isLoading, hasError } = useOurApi(ENDPOINT_URL, []);
My question is how can I call this instance of userOurAPI with a different URL. I have tried calling it within a function where I need it but we can't call hooks within functions, so I am not sure how to pass it new url to get new data. I don't want to have many instances of userOurAPI because that is not DRY. Or is this not possible? New to hooks, so go easy on me!
In order to change the URL such that the component updates and fetches new data, you create a set function that changes the URL and you make sure that the useEffect() is run again on the change of URL. Return your setter function for URL so that you can use it outside of the first instance of your hook. In my code, you will see that I return a setUrl, I can use that to update fetch! Silly of me not to notice, but hopefully this will help someone.
You could do it the way you chose to, but there are other ways of working around such a problem.
One other way would be to always re-fetch whenever the URL changes, without an explicit setter returned from the hook. This would look something like this:
export const useOurApi = (url, initialData) => { // URL passed directly through removes the need for a specific internal url `useState`
// const [url, setUrl] = useState(initialUrl); // No longer used
// ...
useEffect(() => {
// Handle fetch
}, [url]);
return { isLoading, hasError, data: fetchedData }; // No more `setUrl`
};
This may not always be what you want though, sometimes you may not want to re-fetch all the data on every url change, for example if the URL is empty, you may not want to update the url. In that case you could just add a useEffect to the useOurApi custom hook to update the internal url and re-fetch:
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
// ...
useEffect(() => {
// Handle fetch
}, [url]);
useEffect(() => {
// ... do some permutation to the URL or validate it
setUrl(initialUrl);
}, [initialUrl]);
return { isLoading, hasError, data: fetchedData }; // No more `setUrl`
};
If you still sometimes want to re-fetch the data, unrelated to the URL, you could output some function from the hook to trigger the data fetching. Something like this:
export const useOurApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const [fetchedData, setFetchedData] = useState(initialData);
const refetch = useCallback(() => {
// Your fetch logic
}, [url]);
useEffect(() => {
refetch(); // In case you still want to automatically refetch the data on url change
}, [url]);
return { isLoading, hasError, refetch, data: fetchedData };
};
Now you can call refetch whenever you want to trigger the re-fetching. You may still want to be able to internally change the url, but this gives you another a bit more flexible access to the fetching and when it occurs.
you confuse the difference between simple function and function component
Function Component are not just simple function. It means that the have to return a component or a html tag
I think you should turn four function to simple function like so
export const useOurApi = (initialUrl, initialData) => {
let url = initialUrl, fetchedData = initialData,
isLoading= true, hasError = false, unmounted = false;
const handleFetchResponse = response => {
if (unmounted) return initialData;
hasError = !response.ok;
isLoading = false;
return response.ok && response.json ? response.json() : initialData;
};
const fetchData = () => {
isLoading = true;
return fetch(url, { credentials: 'include' })
.then(handleFetchResponse)
.catch(handleFetchResponse);
};
if (initialUrl && !unmounted)
fetchData().then(data => {
if(!unmounted) fetchedData =data;
unmounted = true;
});
return { isLoading, hasError, url, data: fetchedData };
};

Infinite loop in useEffect when setting state

I've got a question about useEffect and useState inside of it.
I am building a component:
const [id, setId] = useState(0);
const [currencies, setCurrencies] = useState([]);
...
useEffect(()=> {
const getCurrentCurrency = async () => {
const response = await fetch(`https://api.exchangeratesapi.io/latest?base=GBP`);
const data = await response.json();
const currencyArray = [];
const {EUR:euro ,CHF:franc, USD: dolar} = data.rates;
currencyArray.push(euro, dolar/franc,1/dolar);
console.log("currencyArray", currencyArray);
setCurrencies(currencies => [...currencies, currencyArray]);
}
getCurrentCurrency();
}, [id, currencies.length]);
Which is used for making a new API request when only id change. I need to every time that ID change make a new request with new data. In my case now I have infinite loop. I try to use dependencies but it doesn't work as I expected.
You changing a value (currencies.length), which the useEffect depends on ([id, currencies.length]), on every call.
Therefore you cause an infinite loop.
useEffect(() => {
const getCurrentCurrency = async () => {
// ...
currencyArray.push(euro, dolar / franc, 1 / dolar);
// v The length is changed on every call
setCurrencies(currencies => [...currencies, currencyArray]);
};
getCurrentCurrency();
// v Will called on every length change
}, [id,currencies.length]);
You don't need currencies.length as a dependency when you using a functional useState, currencies => [...currencies, currencyArray]
useEffect(() => {
const getCurrentCurrency = async () => {
...
}
getCurrentCurrency();
}, [id]);
Moreover, as it seems an exchange application, you might one to use an interval for fetching the currency:
useEffect(() => {
const interval = setInterval(getCurrency, 5000);
return () => clearInterval(interval);
}, []);
you can just call the useEffect cb one the component mounted:
useEffect(()=>{
//your code
// no need for checking for the updates it they are inside the component
}, []);

react hook cant perform setstate in an unmounted component

const SideAd = () => {
const [sideAd, setSideAd] = useState([])
useEffect(() => {
query()
}, [])
const query = async () => {
const res = await getSideAd()
Array.isArray(res) && setSideAd(res)
}
return (
<div className="sideAdComponent">
</div>
)
}
I just want make a request at mounted and then set a new state. But instead I got error react hook cant perform setstate in an unmounted component
There are 2 things you may do here.
First is fighting race conditions in useEffect with using cleanup function(return value in useEffect's callback)
const SideAd = () => {
const [sideAd, setSideAd] = useState([])
useEffect(() => {
const isMounted = true;
getSideAd().then(res => Array.isArray && isMounted && setSideAd(res))
return () => {isMounted = false;}
}, [])
return (
<div className="sideAdComponent">
</div>
)
}
Check nice article at hackernoon for more detailed explanation. To my mind this approach is even better then cancelling request(because it does not require you modify requester's code).
Another thing: maybe it's better to check for a reason why your component unmounted before response comes. Probably there is legit reason for that(say, you've navigated away while response has not come yet).
But it's also possible there is some HOC that declares component constructor inline and that causes all subtree to be re-created instead of updating. So it's worth spending some time on investigation - because there may be hidden bugs you have not found yet(example).
You can write a custom hook called useIsMounted:
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => isMounted.current = false;
}, []);
return isMounted;
};
const SideAd = () => {
const [sideAd, setSideAd] = useState([]);
useEffect(() => {
query();
}, []);
const mounted = useIsMounted();
const query = async () => {
const res = await getSideAd();
Array.isArray(res) && mounted.current && setSideAd(res);
};
return <div className="sideAdComponent"></div>;
};

Resources