React prevent re-fetch when url params are unchanged - reactjs

I have an ExperienceList component that fetches all items on mount. The functionality I am trying to achieve will occur when the user clicks into one particular experience, then navigates back to the ExperienceList. Currently, a fetch is made each time.
I want to do a simple check to see if the cityname from url params matches the cityname found in redux store. If it's the same, bypass the fetch. If not, do it.
Here are 2 approaches that I have tried, without luck:
First Approach
useEffect(() => {
if (props.match.params.cityname !== list[0].city) {
readList(apiRequestParams)
}
}, [])
With the above approach, I get the console error :
Cannot read property 'city' of undefined.
I am not 100% sure on why list is unavailable, since I am able to log it to the console.
Here is the Second approach:
useEffect(() => {
readList(apiRequestParams)
}, [props.match.params.cityname])
This approach does not work either, but I am more baffled as to why this is. As is my understanding, the variables appearing in the dependencies array of useEffect will cause the function to execute if and only if they are different between function calls. In this case, I can guarantee that props.match.params.cityname is the same between 2 different renders, yet a fetch is made each time.
Any insight on how I can optimize this process?

The function readlist is actually the dep of useEffect, you have to tell the useEffect. or you can change it to the cityname param.
const [cityName, setcityName] = useState('cityname');
useEffect(() => {
function getFetchUrl() {
return 'https://...' + cityName;
}
async function fetchData() {
const result = await readList(getFetchUrl());
setData(result.data);
}
fetchData();
}, [cityName]);
or
const fetchData = useCallback(() => {
readList("https://" + cityName);
}, [cityName]);
useEffect(() => {
fetchData()
},[fetchData])

Related

ReactJS array from usestate is still empty after setTimeout

please I'm solving one problem (just learning purposes). I'm using useState() hook and then, after some timeout I want add next items into array from remote fetch.
My code snippet look like:
const [tasks, setTasks] = useState([]);
const url = 'https://pokeapi.co/api/v2/pokemon?offset=0&limit=5';
// asnynchronous call. Yes, it's possible to use axios as well
const fetchData = async () => {
try {
let tasksArray = [];
await fetch(url)
.then((response) => response.json())
.then((data) => {
data.results.map((task, index) => {
// first letter uppercase
const taskName = task.name.charAt(0).toUpperCase() + task.name.slice(1);
tasksArray.push({ id: index, name: taskName });
});
});
console.log('Added tasks:' + tasks.length);
setTasks(_.isEmpty(tasks) ? tasksArray : [...tasks, tasksArray]);
} catch (error) {
console.log('error', error);
}
};
useEffect(() => {
// Add additional example tasks from api after 5 seconds with
// fetch fetchData promise
setTimeout(fetchData, 5000);
}, []);
Code works fine with useEffect() hook. But in async function my array is empty when I add some tasks within five seconds and it will be replaced by fetched data and one empty
I added Butter and Milk within 5 seconds to my Shopping list
But after timeout my tasks array will be replaced by remote fetched data.
And as you can see, tasks array lenght is 0 (like console.log() says)
Please, can you exmplain me, why my tasks array is empty if there exists 2 items before 5 seconds.
Of course, I'm adding my tasks to the list normally after hit Enter and handleSubmit
const handleSubmit = (e) => {
e.preventDefault();
//creating new task
setTasks([
{
id: [...tasks].length === 0 ? 0 : Math.max(...tasks.map((task) => task.id)) + 1,
name: newTask,
isFinished: false,
},
...tasks,
]);
setNewTask('');
}
Thanks for help or explain. It the problem that useEffect is called after rendering? Of this causing async behavior?
I could not understand your code fully correctly but my guess is
the fetchData function you have declared might refer to the tasks at the time of declaration.
so every time you call fetchData you might not see the changed tasks state...
if this is the case try using useCallback with the dependency tasks...
what useCallback does is it stores the function in memory and if smth in dependency changes the function's logic changes to dependencies you declared.
If you have used eslint, calling a function inside useEffect will give you error similar to below
The ‘fetchOrg’ function makes the dependencies of useEffect Hook (at line 6) change on every render. Move it inside the useEffect callback. Alternatively, wrap the ‘fetchOrg’ definition into its own useCallback() Hook
Your code is confusing. You can place everything inside an useEffect, and I believe the thing you are trying to achieve is a long poll query. (for that you use setInterval() and you have to clear the function in useEffect
my solution for you:
const [tasks, setTasks] = useState([]);
const url = 'https://pokeapi.co/api/v2/pokemon?offset=0&limit=5';
const request = {method: GET, Headers: {"Content-type":"application/json"}}
useEffect(() => {
function fetchData(){
fetch(url, request).then(res => res.json().then(data => {
data.results.map((task, index) => {
const taskName = task.name.charAt(0).toUpperCase() + task.name.slice(1);
setTasks((prevState) => ([...prevState,{ id: index, name: taskName }]));
});
})
}
const interval = setInterval(() => {
fetchData();
}, 5000)
return () => clearInterval(interval)
}, []);
please do not forget two things:
This approach is only good when you have 5 to 10 simultaneous clients connected since it is not performance effective on the backend, I would recommend instead an alternative based on realtime communication (with the listening for new events.
please do not forget to specify the {method, headers, body) in the fetch function

React useState without undefined values

I'm novice with React and the state concept. So I fetch data from my database with axios. I put everything in a state (accomodation) and then I need to catch a field (country) into datas and use it into other function.
I succeed to catch everything but when I try to test it with a 'console.log' it appears that the two first result returns empty/undefined before having a result. Because the fact the two first attempts are empty/undefined, the other function doesn't work.
Anyone could help me please :)
Here are my code :
const { id } = useParams()
const [accomodation, setAccomodation] = useState('')
const getAcc = async () => {
const data = await axios.get(
`${process.env.REACT_APP_API_URL}api/v1/accomodations/${id}`
)
setAccomodation(data.data.data.accomodation)
}
useEffect(() => {
getAcc()
}, [])
const country = accomodation.country
console.log(country)
To go over what is happening here:
const [accomodation, setAccomodation] = useState('') the accomodation object is set to empty string.
useEffect(() => { getAcc() }, []) getAcc() is called upon render.
const country = accomodation.country; console.log(country) we are still waiting for getAcc() to finish, meanwhile, country is set to the 'country' property of an empty string which is underfined. undefined is printed.
setAccomodation(data.data.data.accomodation) getAcc() finally finishes, and accommodation is hopefully set to the object you intended.
What happens now?.. Nothing. Because you have not done anything to act on a state update. So one of the answers already posted here is on the right track. You need to act on the update of accommodation state.
This is how you trigger an effect on state update:
useEffect(() => {
if (accommodation.country) {
console.log(accomodation.country);
}
}, [accomodation])
Note that this useEffect() will be invoked whenever accommodation changes from its previous state. If the desired effect is not achieved it may be that accommodation is not the object you intended.

Redux: Why does my useEffect() keeps rerendering its value on every page rerender

I am learning react-redux.
I got the following problem:
I make two async api calls (with redux-thunk):
the first one to fetch country names (in one object, ex: {countries: [{...}, ...]}.
Those country names I use afterwards to make a second api call, to get all the soccer leagues, that are in those countrys (sometimes, there are none, so I get a null). In this case, the call is made with each countryName separatly. I make out of the results an array.
This arrays length is 255m out of which I filter out the null values and map the leagues names.
After I click on a League's name, a page is rendered ({Link} from "react-router-dom";).
NOW my problem occurs
When I click, to get back to my home page (<Link to={"/"} >), both useEffect() are making an api call again. Why?
Here is the code for my useEffect():
const dispatch = useDispatch();
const selectAllCountries = useSelector(state => state.allCountries);
const selectAllLeagues = useSelector(state => state.allLeagues);
useEffect(() => {
dispatch(allCountries());
}, [dispatch]);
useEffect(() => {
if(!_.isEmpty(selectAllCountries.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data]);
I tried to make a custom hook and put the useEffect() in there:
const useCountries = getCountries => {useEffect(() => {
dispatch(getCountries());
},[getCountries])}
useCountries(allCountries);
as suggested here:
React hooks: dispatch action from useEffect
But it didnt help.
Will be greatful for any help.
ANSWER:
in "./actions/.../allLeagues.js
...
import _ from "lodash";
export const allLeagues = (country) => async (dispatch, getState) => {
if (!_.isEmpty(getState().allLeagues) && !_.isEmpty(getState().allLeagues.data)) {
return Promise.resolve();
} else {
try {
...
}
}
}
Question, that was also helpfull:
Fetching data from store if exists or call API otherwise in React
(take look at answer about getStore())
As mentioned in a comment above, the homepage unmounts when you click to go to a new page. When you go back, the page re-mounts and the effect runs again, triggering another API call. You can prevent the API call by checking whether or not the values already exist in your store. I personally like to do this in the action creator, but you could do it in the effect as well.
Checking state in the action creator:
function allLeagues(countryName) {
return (dispatch, getState) => {
// Call `getState` and check whether `allLeagues` has been populated yet.
const { allLeagues } = getState();
if (allLeagues && allLeagues.data && allLeagues.data.length) {
// You already have the data, no need to make the API call.
return Promise.resolve();
}
// No data, make the API call...
};
}
Checking state in the effect:
useEffect(() => {
// Check whether the league data is set or not.
if(!_.isEmpty(selectAllCountries.data) && _.isEmpty(selectAllLeagues.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data, selectAllLeagues.data]);

React useEffect infinite loop fetching data from an api

Hi I'm trying to make a twitter clone app. I am using React on the client side and Express on the server side and PostgreSQL as my database. So here's the problem, I'm trying to use the useEffect like this:
const [tweets, setTweets] = useState([]);
const getTweets = async () => {
const res = await api.get("/posts", {
headers: { token: localStorage.token },
});
setTweets(res.data);
};
useEffect(() => {
getTweets();
}, [tweets]);
I have no idea why it's looping infinite times, am I using it correctly though? I want the tweets to be updated every time I post a tweet. It's working fine but it's running infinite times. I just want it to re-render if a tweet got posted.
Here's my server code for getting all the posts:
async all(request: Request, response: Response, next: NextFunction) {
return this.postRepository.find({
relations: ["user"],
order: {
createdAt: "DESC",
},
});
}
The problem is every time you change the tweets it executes useEffect and changes the tweets and so long and so forth, so it's natural that it loops infinitely, the solution is to add a trigger that you set to true when a tweet gets posted, so the solution would be like this
const [tweets, setTweets] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const getTweets = async () => {
const res = await api.get("/posts", {
headers: { token: localStorage.token },
});
setTweets(res.data);
};
useEffect(() => {
getTweets();
setIsFetching(false);
}, [isFetching]);
and set some logic to use setIsFetching(true) in order to execute the useEffect
PS: if you use an empty array in useEffect, it would execute only when the component is mounted (at the start)
useEffect(() => {
getTweets();
}, [tweets]); // [tweets means that hook works every time 'tweets' state changes]
so your getTweets function set tweets => as tweets are changed hook works again => call getTweets => ... = infinite loop
if you want to download tweets, use empty array instead - hook will work once then
Pass empty array as a second arg for calling it once otherwise for changing it on every tweet change it will re-trigger, so whenever state will change only then it will be re-rendered like Tarukami explained. One thing you can do is check the length like mentioned below so not to compare the whole object but just the length
useEffect(() => {
getTweets();
}, [tweets.length]);
This might raise an error react-hooks/exhaustive-deps lint error (that's a bypass you can use it).
But if you want more tighter check you can compare the ids on each re-render (create a hash/key/id from all element in the array and compare them on each render) like so [tweet id here]) // Only re-subscribe if id changes

React hook missing dependency

I'm hoping someone can explain to me the correct usage of React hook in this instance, as I can't seem to find away around it.
The following is my code
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// This is a trick so that the debounce doesn't run on initial page load
// we use a ref, and set it to true, then set it to false after
const firstUpdate = React.useRef(true);
const UserSearchTimer = React.useRef()
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
function _debounceSearch() {
clearTimeout(UserSearchTimer.current);
UserSearchTimer.current = setTimeout( async () => {
_getUsers();
}, DEBOUNCE_TIMER);
}
async function _getUsers(query = {}) {
if(type) query.type = type;
if(search) query.search = search;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
So essentially I have a table in which i am displaying users, when the page changes, or the perPage, or the order, or the type changes, i want to requery my user list so i have a useEffect for that case.
Now generally I would put the _getUsers() function into that useEffect, but the only problem is that i have another useEffect which is used for when my user starts searching in the searchbox.
I don't want to requery my user list with each and every single letter my user types into the box, but instead I want to use a debouncer that will fire after the user has stopped typing.
So naturally i would create a useEffect, that would watch the value search, everytime search changes, i would call my _debounceSearch function.
Now my problem is that i can't seem to get rid of the React dependency warning because i'm missing _getUsers function in my first useEffect dependencies, which is being used by my _debounceSearch fn, and in my second useEffect i'm missing _debounceSearch in my second useEffect dependencies.
How could i rewrite this the "correct" way, so that I won't end up with React warning about missing dependencies?
Thanks in advance!
I would setup a state variable to hold debounced search string, and use it in effect for fetching users.
Assuming your component gets the query params as props, it would something like this:
function Component({page, perPage, order, type, search}) {
const [debouncedSearch, setDebouncedSearch] = useState(search);
const debounceTimer = useRef(null);
// debounce
useEffect(() => {
if(debounceTime.current) {
clearTimeout(UserSearchTimer.current);
}
debounceTime.current = setTimeout(() => setDebouncedSearch(search), DEBOUNCE_DELAY);
}, [search]);
// fetch
useEffect(() => {
async function _getUsers(query = {}) {
if(type) query.type = type;
if(debouncedSearch) query.search = debouncedSearch;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
_getUsers();
}, [page, perPage, order, type, debouncedSearch]);
}
On initial render, debounce effect will setup a debounce timer... but it is okay.
After debounce delay, it will set deboucedSearch state to same value.
As deboucedSearch has not changed, ferch effect will not run, so no wasted fetch.
Subsequently, on change of any query param except search, fetch effect will run immediately.
On change of search param, fetch effect will run after debouncing.
Ideally though, debouncing should be done at <input /> of search param.
Small issue with doing debouncing in fetching component is that every change in search will go through debouncing, even if it is happening through means other than typing in text box, say e.g. clicking on links of pre-configured searches.
The rule around hook dependencies is pretty simple and straight forward: if the hook function use or refer to any variables from the scope of the component, you should consider to add it into the dependency list (https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies).
With your code, there are couple of things you should be aware of:
1.With the first _getUsers useEffect:
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// Correctly it should be:
useEffect(() => {
_getUsers()
}, [_getUsers])
Also, your _getUsers function is currently recreated every single time the component is rerendered, you can consider to use React.useCallback to memoize it.
2.The second useEffect
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
// Correctly it should be
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [firstUpdate, _debounceSearch])

Resources