setState in useEffect not update - javascript

Got an input and want to pass value to handler:
const [term, setTerm] = useState('');
<Input type="text" onBlur={(e)=>handleFilter(e, 'params')} />
const handleFilter = async(e, params) => {
//... api call and etc
setTerm(e.target.value); // update term
console.log(term) // return none-updated value! but I need fresh value
// send this value to another api
}
I want to make an search filter function, for ex. if I enter a, console return empty, then I enter b console return a ! it means term not update immediately, then I used useEffect but inside the useEffect I got new value, but inside handleFilter function still console return prev value.
useEffect(() => {
getApi()
.then(data => {
console.log(data)
})
console.log(term) // works fine, return new value
setTerm(term) // update term
}, [term])
I tried this but no success:
setTerm({...term, e.target.value});
Any solution? I'm new to react hook.

You can check this answer here.
This is because react's state update is async. You can't rely on its update right after calling setState. Put your effects (code that is run after a state is updated) in a useEffect hook.
const handleFilter = async(e, params) => {
//... api call and etc
setTerm(e.target.value); // update term
}
React.useEffect(() => {
console.log(term) // return none-updated value! but I need fresh value
// send this value to another api
}, [term]);

setTerm is async and will update the term on the next render cycle.
it is not updated immediately for the current render cycle.
you can store the current value in a ref if you are curious what is happening behind the scenes
const termRef = React.useRef(term);
termRef.current = term;
const yourHandler = () => {
setTimeout(() => console.log(termRef.current), 0);
}

If we are going ahead with useEffect with an API call, please ensure to include async await scenario to ensure that setState updates after the data is fetched.
useEffect(async () => {
const data = await getApi()
console.log(term) // works fine, return new value
setTerm(term) // update term
}, [term])

I could be wrong on this one, but could this have something to do with you calling an async function from a normal event. This might cause some type of delay.
There is also the fast that useEffect is treated differently in React than a normal function, since it's integrated into React.
It could also be related to the [term] trigger in the useEffect but the event in your handleFilter isn't treated the same

Related

Why useEffect is not called when I get a null value response from the api?

What im tryin to do is to get data from the backend. To do that I'm using axios.get().
I have created an useEffect without a dependency, because I want to call it every time.
The useEffect works, but when I get a null value as a response it stops, and if I add a new value (from postman) to test if the list will be updated but it won't. Only if I refresh the screen it will start getting the data list every second.
useEffect(() => {
console.log('called');
axios
.get(`API`)
.then((response) => {
setDataList(response.data.data); //null or data
})
.catch((error) => console.log('error', error));
});
How can I make the useEffect work even when I get a null value from response ?
By not putting a dependency array in place, you get the same effect as not using useEffect in the first place.
Every time the component renders, the Ajax request is triggered.
Your problem is that, all else being equal, the only time it will re-render is if setDataList(response.data.data) sets a different value.
If it errors, then it isn't setting any value so it won't trigger a re-render.
You need to decide what you really want to trigger a new request. For example, a timer.
(Untested code);
useEffect(() => {
let timeOut = null;
let controller = null;
const makeRequest = async () => {
controller = new AbortController();
const response = await axios.get(`API`, {
signal: controller.signal
});
const data = response.data?.data;
if (data) setDataList(data);
setTimeout(makeRequest, 5000); // Wait 5 seconds then poll again
};
makeRequest(); // Start the timer.
// Clean up on unmount
return () => {
clearTimeout(timeOut);
controller.abort();
};
}, [ /* You need a dependency array to stop multiple timers being created! */ ]);

Export a global variable using asynchronous (async, await) JavaScript

Ok the goal is to be equal a global scope variable to an inner information I need and then export it. But I am not able to do it (I keep getting undefined even though when I console out data, I get the info I want). I have looked at several documentations and other peoples questions NodeJS Async/Await Exporting a Variable , but it still doesn't answer my question. I know I have to use asynchronous JavaScript (async, await) but because I fairly new to JS. NOTE: GETAPRODUCTAPI is a SpringBoot API and updateClick() is called inside another method.
The global variable called dataToExport I want to export and it equals data (then(data)).
export var dataToExport;
const updateClick = () => {
const editBtns = getQSelectorAll(".edit");
editBtns.forEach((btn) => {
btn.addEventListener("click", (e) => {
const currentClicked = e.currentTarget.dataset.editid;
const api = GETAPRODUCTAPI + currentClicked;
fetch(api).then((response) => {
return response.json();
}).then((data) => {
console.log(data); // this works fine
// TODO
dataToExport = data
});
});
});
};
console.log(dataToExport); // undefined output
This works but you have to ensure that reading of dataToExport happens after the asynchronous code that actually sets the variable.
That is, through whatever means necessary, you would want to wait to console.log(dataToExport) until after all of the following have occurred:
updateClick is called and the event listener has been bound to the buttons
The button(s) are actually clicked (programmatically or by user interaction)
The fetch(es) are sent out
The response to the fetch(es) are received
The response json is parsed and assigned to dataToExport.
As is you're trying to read dataToExport before any of those steps have occurred and therefore the variable has not been set yet. You can periodically poll to see if the variable has been assigned (e.g. setTimeout or setInterval) or otherwise have an event that triggers a check of this variable.
Based on #arcyqwerty explanation, I was able to figure out a logic. I created two global scopes a variable status and a function changedStatus() . Equalled status to null and then changedStatus() whose job is to check when status changes from null to something else (event that triggers a check of this variable). NOTE: I renamed dataToExport to status
let status = null;
let changedStatus = (value) => {
status = value;
if (status != null) {
console.log(status);
}
}
const updateClick = () => {
const editBtns = getQSelectorAll(".edit");
editBtns.forEach((btn) => {
btn.addEventListener("click", (e) => {
const currentClicked = e.currentTarget.dataset.editid;
const api = GETAPRODUCTAPI + currentClicked;
fetch(api).then((response) => {
return response.json();
}).then((data) => {
// TODO
changedStatus(data);
});
});
});
};
And as far as exporting dataToExport just like #Bergi said it doesn't make sense to export data before user clicks so instead I used localStorage. This example helped for better understanding.

useeffect infinite loop even though state data is not changing

My program goes into an infinite loop constantly calling useEffect() everytime I start the app. I have one state that I don't think is changing other than in the retrieveItemStatus() function so I'm confused on why its going into a loop like it is.
const App = () => {
var items;
const [itemStatuses, updateStatuses] = useState({});
const retrieveItemStatus = async () => {
var tempStatuses;
try {
const value = await AsyncStorage.getItem("#item_Statuses");
if (value !== null) {
tempStatuses = await JSON.parse(value);
//console.log("123456");
} else {
tempStatuses = await JSON.parse(
JSON.stringify(require("../default-statuses.json"))
);
}
updateStatuses(tempStatuses);
} catch (error) {}
};
retrieveItemStatus();
useEffect(() => {
const copyData = async () => {
const itemsCopy = [];
const coll = await collection(db, "Items");
const querySnapshots = await getDocs(coll);
const docsArr = querySnapshots.docs;
docsArr.map((doc) => {
var data = doc.data();
if (itemStatuses[data.name] === "locked") return;
itemsCopy.push(data);
});
items = itemsCopy;
//getItems([...itemsCopy]);
};
copyData();
}, [itemStatuses]);
return (
<View style={styles.container}>
<Text>temp.......</Text>
</View>
);
};
It has nothing to do with useEffect. You're calling retrieveItemStatus unconditionally every time your component function is called to render the componennt. retrieveItemStatus calls updateStatuses which changes state. You see your useEffect callback get run repeatedly as a side-effect of that, because your useEffect callback has itemStatuses as a dependency.
I assume you only need the itemStatuses to get fetched once. If so, put the call in a useEffect callback with an empty dependency array:
useEffect(retrieveItemStatus, []);
Also, you have (note the ***):
const App = () => {
var items // ***
// ...
useEffect(() => {
const copyData = async () => {
// ...
items = itemsCopy; // ***
// ...
};
copyData();
}, [itemStatuses]);
};
That won't work, by the time you assign to items from the callback, anything you might have been trying to do with items will already have just used undefined (the value it gets when you don't give it one). If you need items to be retained, either put it in state (if you use it for rendering) or in a ref (if you don't).
In a comment you said:
Ok so I put retrieveItemStatus() call inside useEffect and removed the dependency which fixed the looping. But now there is an issue where itemStatuses state doesn't get updated before copyData() is called and itemStatuses is needed.. so it doesn't do anything until I manually refresh/render the whole thing again.
If copyData relies on the result from retrieveItemStatus, then put the calls to each of them in the same useEffect, not calling copyData until you get the results from retrieveItemStatus. Something along the lines of the below, though you'll need to tweak it of course as I don't have all the details (I've also made some other comments and changes in there I've flagged up):
// *** There's no need to recreate this function on every render, just
// have it return the information
const retrieveItemStatus = async () => {
try {
let tempStatuses; // *** Declare variables in the innermost scope you can
const value = await AsyncStorage.getItem("#item_Statuses");
if (value !== null) {
tempStatuses = await JSON.parse(value);
//console.log("123456");
} else {
// *** stringify + parse isn't a good way to copy an object,
// see your options at:
// https://stackoverflow.com/questions/122102/
tempStatuses = await JSON.parse(JSON.stringify(require("../default-statuses.json")));
}
return tempStatuses;
} catch (error) {
// *** Not even a `console.error` to tell you something went wrong?
}
};
// *** Similarly, just pass `itemStatuses` into this function
const copyData = async (itemStatuses) => {
const coll = await collection(db, "Items");
const querySnapshots = await getDocs(coll);
const docsArr = querySnapshots.docs;
// *** Your previous code was using `map` just as a loop,
// throwing away the array it creates. That's an anti-
// pattern, see my post here:
// https://thenewtoys.dev/blog/2021/04/17/misusing-map/
// Instead, let's just use a loop:
// (Alternatively, you could use `filter` to filter out
// the locked items, and then `map` to build `itemsCopy`,
// but that loops through twice rather than just once.)
const itemsCopy = []; // *** I moved this closer to where
// it's actually filled in
for (const doc of docsArr) {
const data = doc.data();
if (itemStatuses[data.name] !== "locked") {
itemsCopy.push(data);
}
}
//getItems([...itemsCopy]); // *** ?
return itemsCopy;
};
const App = () => {
// *** A new `items` is created on each render, you can't just
// assign to it. You have to make it a member of state (or use
// a ref if it's not used for rendering.)
const [items, setItems] = useState(null);
const [itemStatuses, setItemStatuses] = useState({});
// *** ^−−−−− the standard convention is `setXyz`.
// You don't have to follow convention, but it makes it easier
// for other people to read and maintain your code if you do.
useEffect(() => {
(async () => {
const newStatuses = await retrieveItemStatus();
const newItems = await copyData(newStatuses);
// *** Do you need `itemStatuses` to be in state at all? If it's
// only used for calling `copyData`, there's no need.
setItemStatuses(newStatuses);
setItems(newItems);
})().catch((error) => {
console.error(error);
});
}, []);
// *** You didn't show what you're using here, so it's hard to be
// sure what needs to be in state and what doesn't.
// Only put `items` or `itemStatuses` in state if you use them for
// rendering.
return (
<View style={styles.container}>
<Text>temp.......</Text>
</View>
);
};
Here are those links as links:
What is the most efficient way to deep clone an object in JavaScript?
Misusing map (on my blog)
#T.J. Crowder's answer is correct but I want to clarify a small point, so you understand why your useEffect didn't stop running.
As you you know, when the dependency in dependency array changes, useEffect runs. But the data in your itemStatuses doesn't change right(according to you)? So why does useEffect re-runs? Let's look at the below example:
const obj1 = {};
const obj2 = {};
const obj3 = {a:2};
const obj4 = {a:2};
console.log(obj1 === obj2)
console.log(obj3 === obj4)
console.log(obj3.a === obj4.a)
As you can see javascript doesn't think that an empty object is strictly equal to another empty object. It is because these objects refer to the different locations in memory regardless of their content.
So that's why every time retrieveItemStatus ran, it updated itemStatuses. Then because itemStatuses got updated(although the value is same), useEffect triggered a re-render, so again everything started all over.
Main reason is in calling retrieveItemStatus(); without any event.
If you want to call this function when page loading, you should like this.
...
useEffect(() => {
retrieveItemStatus()
}, [])
...
This will fix loop issue.

js only - run a function only once

I'm fetching some data from firebase and would like to run async/await function (to fetch data) only once upon the first page load. I'm used to React and lifecycle methods / hooks doing it but this little project is just too small to use React. I just need to run this function once, fetch the data, save it to a variable and do not make any further calls to firebase api in the same session.
async function getEntries() {
const snapshot = await firebase.firestore().collection('riders').get()
// Do my thing with the data, etc.
// console.log(snapshot.docs.map(doc => doc.data()));
}
Is there any js-only way of running this function only once when the page loads?
If you call a function just once, why do you need the function at all?
const snapshot = await firebase.firestore().collection('riders').get()
// Do my thing with the data, etc.
// console.log(snapshot.docs.map(doc => doc.data()));
This top level await only works in modules, and it blocks all depending modules to load. If that is not necessary (they don't depend on the data), or if you don't want write a module, you can wrap the code in an async IIFE, and store the returned promise in a variable:
const dataPromise = (async function() {
//...
return data;
})();
While the data is loading, you might want to show some loading icon or so. That can easily be done with the following hook:
function usePromise(p) {
const [state, setState] = useState(null);
useEffect(() => { p.then(setState); }, []);
return state;
}
// Inside a component:
const data = usePromise(dataPromise);
if(data === null)
return <Loading />;
// show data
Yes. You can use Self Invoking (self executing) Functions. Syntax is like:
(function(){})();
The last parentheses are for running function. the function is anonymous.
You can Implement it this way:
(async function () {
const snapshot = await firebase.firestore().collection('riders').get()
})();
in this way you can never call this function again and it will run only once.
Tutorial: https://blog.mgechev.com/2012/08/29/self-invoking-functions-in-javascript-or-immediately-invoked-function-expression/
And The question you asked is somehow duplicate and answered here: Function in JavaScript that can be called only once
What you are looking for is memoization of the function result. There are several libraries to supporting including react.
Theres also a handmade pattern you can use by changing the function implementation after it's called once, accoring to JavaScript: The Good Parts
async function getEntries() {
const snapshot = await firebase.firestore().collection('riders').get()
// Do my thing with the data, etc.
// console.log(snapshot.docs.map(doc => doc.data()));
getEntries = async function(){
return snapshot
}
return snapshot
}
I think you can load it with the load method when the page is first loaded and then set it to cookie or local stroge. You can check this value on next page loads. You can do this quickly using jQuery.
$(window).load(function() {
var item = localStorage.getItem('test');
if(item != null){
// your code
}
else {
localStorage.setItem('test', 1);
}
});
The simplest way is to make a global variable like:
let isCalled = false;
and in the function body do:
if(isCalled) return;
//the stuff the function would do
isCalled = true;
//Assign isCalled to true before using a return statement as it will make the program discard the lines below it.

React Hooks, setState not working with an async .map call

So, I'm trying to change a state in my component by getting a list of users to call an api.get to get data from these users and add on an new array with the following code:
function MembersList(props) {
const [membersList, setMembersList] = useState(props.members);
const [devs, setDevs] = useState([]);
useEffect(() => {
let arr = membersList.map((dev) => {
return dev.login;
});
handleDevs(arr);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [membersList]);
function handleDevs(membersArr) {
membersArr.map(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
/** If i console.log data here i actualy get the data from the user
but i think theres something wrong with that setDevs([...devs, data]) call
**/
setDevs([...devs, data]);
});
}
but the devs state always return an empty array, what can I do to get it to have the actual users data on it?
The issue you were having is because you were setting devs based on the data from the original render every time due to the closure around handleDevs function. I believe this should help take care of the issues you were having by using the callback method of using setDevs.
This also takes care of some issues with the dependency arrays and staleness in the useEffect hook. Typically using // eslint-disable-next-line react-hooks/exhaustive-deps should be your last resort.
function MembersList(props) {
// this isn't needed unless you are using it separately
const [membersList, setMembersList] = useState(props.members);
const [devs, setDevs] = useState([]);
useEffect(() => {
let arr = membersList.map((dev) => dev.login);
arr.forEach(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
setDevs((devs) => [...devs, data]);
})
}, [membersList]);
}
You need to understand how React works behind scenes.
In short, it saves all the "sets" until it finishes the cycle and just after that actually update each state.
I think that why you do not see the current state updated.
For better understanding read this post: Medium article
The issue is that setDevs uses devs which is the version of devs when handleDevs is defined. Therefore setDevs will really only incorporate the data from the last time setDevs is called.
To fix this you can use the callback version of setDevs, like so:
setDevs(prevDevs => [...prevDevs, data])
Also since you are not trying to create a new array, using map is not semantically the best loop choice. Consider using a regular for loop or a forEach loop instead.
you call setDevs in the async execution loop. Here is the updated handleDevs function.
function handleDevs(membersArr) {
const arr = membersArr.map(async (dev) => {
let { data } = await api.get(`/users/${dev}`);
return data;
/** If i console.log data here i actualy get the data from the user
but i think theres something wrong with that setDevs([...devs, data]) call
**/
});
setDevs([...devs, ...arr]);
}

Categories