How to make State Hook asynchronous - javascript

I'm trying to change the value of a counter on user click.
The problem is that the state isn't synchronous, so the counter isn't updating properly and has one step late.
Here is the state:
const [state, setState] = useState({ count: 2, read: true });
const { count, read } = state;
The user first click on an element, call a reset function :
const onReset = e => {
setState({ count: 0, read: false });
getArticles(article.slug, count, count + 2);
};
Then, the user can hit a load more button which increments the counter.
const loadMore = e => {
if (read) {
getArticles(article.slug, count, count + 2)
} else {
getTagArticles(article.slug, count, count + 2);
}
setState({ ...state, count: count + 2 });
};
How can I make it asynchronous so my counter isn't late on updating the value? Should I use global variables instead of a state hook?
Thanks

Related

How do I set the value of a property via a function?

I'm trying to run a function inside a reducer function to calculate a cart total, but the value in the state object is the function and not the result of the function. After render, the result is displayed, but I cannot pass the object to other components (I'm using a context). How do I do it? Here is the code (assume that the values work, because they do).
I've tried setting it to the const to no avail. I've tried an anonymous function that returns the function's total, and it still doesn't work. I've also tried just calling the function.
const reducer = (cart, action) => {
switch(action.type) {
case("ADD_ITEM"):
return {
...cart,
products: {
...cart.products,
[action.payload.product.id]: {...action.payload.product}
},
total: () => (cartTotal)
}
break
case("REMOVE_ITEM"):
delete cart.products[action.payload]
return {
...cart,
products: {
...cart.products
},
total: () => (cartTotal)
}
break
case("CLEAR_CART"):
return {
cart: {
...initialState
}
}
break
}
}
Here is the object:
cart: {
products: [{}],
total: 0
}
Here is the function to return the total:
const cartTotal = () => {
const total = Object.values(cart.products).reduce((prev, curr) => {
const currPrice = (curr.data.on_sale && curr.data.sale_price) ? curr.data.sale_price : curr.data.price
return prev + currPrice
}, 0)
return total.toFixed(2)
}
Right now, I'm passing the method that allows you to calculate the total, but it seems like it is unnecessary, as I'm watching the cart state and updating the value of total each time items are added/removed. How do I set the value of a property inside of the reducer function as the return of another helper function? Thanks!
I don't know why are you storing derived state, well, in state? This should be computed via a selector when reading your state out (and/or passed to a custom Context provider).
If you must store the total in state then you need to call the cartTotal function to be able to store its return value. Unfortunately this will only compute the cart total on the unupdated cart since you are currently in the function that returns the new cart state.
You can factor out the cart update so you have an updated cart products object, and with a small revision of cartTotal it can consume this updated cart products object and compute a total.
Example:
const cartTotal = (products) => {
const total = Object.values(products).reduce((prev, curr) => {
const currPrice = (curr.data.on_sale && curr.data.sale_price) ? curr.data.sale_price : curr.data.price;
return prev + currPrice;
}, 0);
return total.toFixed(2);
};
Cases
case "ADD_ITEM": {
const { payload } = action;
const products = {
...cart.products,
[payload.product.id]: { ...payload.product },
}
return {
...cart,
products,
total: cartTotal(products),
}
break;
}
case "REMOVE_ITEM": {
const products = { ...cart.products };
delete products[action.payload];
return {
...cart,
products,
total: cartTotal(products),
}
break;
}
You need to call cartTotal to calculate the value.
total: cartTotal()

React hooks - setState does not update some state properties

i am making a game but i am having a problem using setState hook on react, it isnt updating my state, my state properties are
const [state,setState] =useState({
score:0,
holes:9,
initGame:false,
lastHole:-1,
minPeepTime: 200,
maxPeepTime: 1000})
i also have 2 useEffect hook just for testing
useEffect(()=>{
console.log("init game");
console.log(state.initGame);
console.log("--------------");
},[state.initGame])
useEffect(()=>{
console.log("last hole");
console.log(state.lastHole);
console.log("##############");
},[state.lastHole])
and i have a function to start the game
const StartGame=()=>{
console.log("here")
setState({...state,initGame: true,score:0,lastHole: 15})
peep();
setTimeout(() => {
console.log("Game Over")
setState({...state,lastHole:-1,initGame: false})
}, 10000)
}
"here" is actually being printed, and when time is over "Game Over" is being printed.
peep() function update the property lastHole and it call useEffect correctly, but property initGame is never being updated by setState and that my actual problem, useEffect for that property is never call (just the first time when my page is loaded, but not on StartGame function) and its value never change
UPD:
this is the peep function.
const randomTime = (min, max) => {
return Math.round(Math.random() * (max - min) + min);
};
const randomHole = () => {
const idx = Math.floor(Math.random() * state.holes);
if (idx === state.lastHole){
return randomHole();
}
setState({...state,lastHole: idx});
}
const peep = () => {
randomHole();
let time = randomTime(state.minPeepTime,state.maxPeepTime)
if(state.initGame){
setTimeout(() => {
peep()
}, time)
}
}
and this is the output of the code
To properly update state in this scenario use:
setState((initialState) => ({
...initialState,
initGame: true,
score: 0,
lastsHole: 15
}))
The setTimeout part becomes
setState((initialState) => ({
...initialState,
lastHole: -1,
initGame: false
}))
I don't know what peek does or the importance of the setTimeout though;
The problem here is with the setState. setState doesn't immediately change the state as you expect.
setState({
...state,
initGame: true,
score: 0,
lastHole: 15,
});
peep() // value of initGame is false
Unexpectedly, the value of initGame will still be false. Change all the setstates like given below.
setState((state) => ({
...state,
initGame: true,
score: 0,
lastHole: 15,
}));
This will successfully set initGame to true before calling peep()
The final output will be as shown below.

React SetState Overwriting Entire Object Instead of Merging

My state looks like this in the constructor:
this.state = {
selectedFile: null, //current file selected for upload.
appStatus: 'waiting for zip...', //status view
zipUploaded: false,
zipUnpacked: false,
capturingScreens: false,
finishedCapture: false,
htmlFiles: null,
generatedList: [],
optionValues: {
delayValue: 1
},
sessionId: null,
estimatedTime: null,
zippedBackupFile: null,
secondsElapsed:0,
timer: {
screenshotStart:0,
screenshotEnd:0,
timingArray:[],
averageTimePerUnit:25,
totalEstimate:0
}
};
I have the following functions in my app.js:
this.secondsCounter = setInterval(this.countSeconds, 1000); // set inside the constructor
getStateCopy = () => Object.assign({}, this.state);
countSeconds = () => {
let stateCopy = this.getStateCopy();
let currentSeconds = stateCopy.secondsElapsed + 1;
this.setState({secondsElapsed:currentSeconds});
}
captureTime = (startOrStop) => {
let stateCopy = this.getStateCopy();
let secondsCopy = stateCopy.secondsElapsed;
let startPoint;
if(startOrStop === true) {
this.setState({timer:{screenshotStart:secondsCopy}});
} else if(startOrStop === false){
this.setState({timer:{screenshotEnd:secondsCopy}});
startPoint = stateCopy.timer.screenshotStart;
stateCopy.timer.timingArray.push((secondsCopy-startPoint));
this.setState({secondsElapsed:secondsCopy})
stateCopy.timer.averageTimePerUnit = stateCopy.timer.timingArray.reduce((a,b) => a + b, 0) / stateCopy.timer.timingArray.length;
this.setState({secondsElapsed:secondsCopy})
this.setState({timer:{averageTimePerUnit:stateCopy.timer.averageTimePerUnit}})
}
I'm getting an error that "push" does not exist on stateCopy.timer.timingArray. I did some investigation and found that this.setState({timer:{screenshotStart:secondsCopy}}); is actually overwriting the entire "timer" object in state and removing all of the previous properties instead of merging them.
I don't understand what I'm doing wrong.. I'm using stateCopy to avoid mutating state, and to get proper values (avoiding asynchronous confusion). Every article I read online about react suggests that writing an object to state will merge with whatever is already there, so why does it keep overwriting "timer" instead of merging??
I did some investigation and found that this.setState({timer:{screenshotStart:secondsCopy}}); is actually overwriting the entire "timer" object in state and removing all of the previous properties instead of merging them.
Correct. setState only handles merging at the top level. Anything below that you have to handle yourself. For instance:
this.setState(({timer}) => {timer: {...timer, screenshotStart: secondsCopy}});
Note the use of the callback version of setState. It's important to do that any time you're providing state information that's dependent on existing state.
There are other places you have to do the same sort of thing, including when you push to the array. Here are some further notes:
There's no reason to copy state here:
countSeconds = () => {
let stateCopy = this.getStateCopy();
let currentSeconds = stateCopy.secondsElapsed + 1;
this.setState({secondsElapsed: currentSeconds});
}
...and (as I mentioned above) you must use the callback form to reliably modify state based on existing state. Instead:
countSeconds = () => {
this.setState(({secondsElapsed}) => {secondsElapsed: secondsElapsed + 1});
};
Similarly in captureTime:
captureTime = (startOrStop) => {
if (startOrStop) { // *** There's no reason for `=== true`
this.setState(({timer, secondsElapsed}) => {timer: {...timer, screenshotStart: secondsElapsed}});
} else { // *** Unless `startOrStop` may be missing or something, no need for `if` or `=== false`.
this.setState(({timer, secondsElapsed}) => {
const timingArray = [...timer.timingArray, secondsElapsed - timer.screenshotStart];
const update = {
timer: {
...timer,
screenshotEnd: secondsElapsed,
timingArray,
averageTimePerUnit: timingArray.reduce((a,b) => a + b, 0)
}
};
});
}
};
Side note: Your copyState function does a shallow state copy. So if you modify any properties on the objects it contains, you'll be directly modifying state, which you mustn't do in React.
setState hooks overwrite the state with a new object always... that is their correct behavior.
you need to use a function within setState. not just pass in an object.
setState((prevState,prevProps)=>{
//logic to make a new object that you will return ... copy properties from prevState as needed.
//something like const newState = {...prevState} //iffy myself on exact syntax
return newState
})
Your getStateCopy is only shallow cloning the existing state - anything nested is not cloned. To illustrate:
const getStateCopy = () => Object.assign({}, state);
const state = {
foo: 'bar',
arr: [1, 2]
};
const shallowCopy = getStateCopy();
shallowCopy.foo = 'newFoo';
shallowCopy.arr.push(3);
console.log(state);
Either deep clone the state first instead, or use spread to add in the new properties you want:
countSeconds = () => {
this.setState({
...this.state,
secondsElapsed: this.state.secondsElapsed + 1
});
}
captureTime = (startOrStop) => {
if (startOrStop === true) {
this.setState({ ...this.state, timer: { ...this.timer, screenshotStart: this.state.secondsElapsed } });
} else if (startOrStop === false) {
const newTimingValue = this.state.secondsElapsed - this.state.timer.screenshotStart;
const newTimingArray = [...this.state.timer.timingArray, newTimingValue];
this.setState({
...this.state,
timer: {
...this.timer,
screenshotEnd: this.state.secondsElapsed,
timingArray: newTimingArray,
averageTimePerUnit: newTimingArray.reduce((a, b) => a + b, 0) / newTimingArray.length,
},
});
}
}
If captureTime is always called with either true or false, you can make things look a bit cleaner with:
captureTime = (startOrStop) => {
if (startOrStop) {
this.setState({ ...this.state, timer: { ...this.timer, screenshotStart: this.state.secondsElapsed } });
return;
}
const newTimingValue = this.state.secondsElapsed - this.state.timer.screenshotStart;
const newTimingArray = [...this.state.timer.timingArray, newTimingValue];
// etc

How can I dynamically write a reducer (useReducer hook)?

I am trying to write a dynamic reducer to loop over a list and setting each item in this list a counter.
I am not sure I am doing it right - mainly in the section where I set it a value of ' ' (and can't dynamically name it or set the value I want initially (each will have a different value))
const reducer = (state, action) => {
switch(action.type) {
case 'SET_COUNTER':
return {
...state,
[`counter${action.id}`]: action.payload
}
default:
return state
}
}
//is the below correct?
let [{ counter }, dispatchReducer] = useReducer(reducer, {
counter: '',
})
I am then looping over an array of objects to create different counters (e.g. counter0, counter1, counter2 ...) and set each of them a value
//this dispatch is not working
useEffect(() => {
availableTimes.map(item =>
dispatchReducer({
type: 'SET_COUNTER',
id: item.id,
payload: counts[`${item.time}`]
})
)
}, [])
The payload comes from an object which I am using to count the instances of a time. E.g. if "2230" appears 3 times, this object will have "2230": 3
const counts = {}
extractedTiesm.forEach(x => {
counts[x] = (counts[x] || 0) + 1
})
//console.log(counts["2230"]) --> 3

componentDidMount updating synchronously

I'm loading data from saved session using:
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
this.setState({
cartItems: JSON.parse(localStorage.getItem('savedData')),
totalPrice: this.getPriceOnLoad(),
totalItems: this.getItemsOnLoad(),
});
}
}
cartItems is an array of objects. Which seems is updated before
this.getPriceOnLoad();
this.getItemsOnLoad();
functions are called, for example this.getPriceOnLoad function:
getPriceOnLoad() {
let itemsPrice = 0;
for (let i = 0; i <= this.state.cartItems.length - 1; i++) {
itemsPrice += this.state.cartItems[i].quantity * this.state.cartItems[i].price;
}
return itemsPrice;
}
but, in getPriceOnLoad function, this.state.cartItems.length is equal to 0, so for loop is not executing. I can see in React dev tools that this array has some length. Is it because componentDidMount() is executing state change synchronously and can't see updated array immediately? So my question is how could i update price and quantity of items after array is initialized?
Your state is not updated in order. For this, you could store the cartItems in a temporary value, and send it to each functions :
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const cartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems, //Short syntax for 'cartItems: cartItems'
totalPrice: this.getPriceOnLoad(cartItems),
totalItems: this.getItemsOnLoad(cartItems),
});
}
}
You could also make your function significantly shorter by using reduce:
this.setState({
cartItems,
totalPrice: cartItems.reduce((total, item) => total + (item.quantity * item.price), 0),
totalItems: cartItems.reduce((total, item) => total + item.quantity, 0),
});
Can you show us your second function too ? It may be optimized as well. Done.
the wrong thing that you are doing is trying to use values from the state on your functions that will define your state.
you have 2 approaches to solve this:
1) use the callback function from setState and then set the state again with the new data (which into my opinion is not the best approach)
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const cartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems
}, ()=> {
this.setState({
totalPrice: this.getPriceOnLoad(cartItems),
totalItems: this.getItemsOnLoad(cartItems),
});
})
}
}
2) send the values to your functions
componentDidMount() {
if (JSON.parse(localStorage.getItem('savedData')) !== null) {
const savedCartItems = JSON.parse(localStorage.getItem('savedData'))
this.setState({
cartItems,
totalPrice: this.getPriceOnLoad(savedCartItems),
totalItems: this.getItemsOnLoad(savedCartItems),
});
}
}
getPriceOnLoad() is executed before this.setState is executed. So you cannot refer to this.state in getPriceOnLoad().
When you call this.setState({}), JS first needs to generate the object for the setState() function. Means the functions you are referring to run first, then this.setState().
And in any case this.setState() is an asynchronous function, so this.state is not directly available after setState() execution.

Categories