I am trying to create an alert component in React.JS which would receive one array as props and then loop through all the elements inside and display them in the alert component.
I do not know how to restart the loop when the array map ends.
This is my code:
useEffect(() => {
props.messages.map((item, index) => {
setTimeout(() => {
setMessage(item);
}, 5000*index)
});
},[]);
I want to run the loop continuously until the user closes the notification alert.
My guess is to wrap the map inside a while / do while in order to run it until the user closes the notification bar. I am having trouble restarting the loop once the map of the array gets to the last element.
You don't really need to compute an index and reset back to zero.
Add an index state and mounting useEffect hook to increment the index on the 5 second interval. Don't forget to return a cleanup function to clear the interval timer.
const [index, setIndex] = React.useState(0);
useEffect(() => {
const timerRef = setInterval(() => {
setIndex(i => i + 1);
}, 5000);
return () => clearInterval(timerRef);
},[]);
Use a second useEffect hook with a dependency on the index state. When it updates take the index modulus the array length to compute a valid in-range index and update the current message state.
useEffect(() => {
const { messages } = props;
setMessage(messages[index % messages.length]);
}, [index]);
However, the second useEffect hook and message state isn't necessary, and could be considered derived state. Derived from the props.messages array and current index state. You can simply use these directly in the rendered alert.
Example:
<Alert message={messages[index % messages.length]} />
Related
In my React app, I have a list of orders which are supposed to be shown to the user for only 30 seconds so each order has a value of 30 seconds for its duration propery:
[
{
...,
...,
duration: 30
},
{
...,
...,
duration: 30
},
...
]
I'm using Redux Toolkit to store their data so that I can render the UI of these items in various components. I tried to create an action which gets dispatched every 1 second to decrement the duration by one:
decrementCountdown: (state, action) => {
const order = state.entities[action.payload];
if (order) order.duration -= 1;
}
Then, in App.jsx, I dispatch the action using setInterval inside a loop:
useEffect(() => {
let countdown;
for (order of orders) {
// Run until the duration reaches zero
if (order.duration > 1) {
countdown = setInterval(() => dispatch(decrementCountdown(order?.id)), 1000);
}
}
return () => clearInterval(countdown);
}, [orders])
The challenging part is that the timers have to be synched so that everywhere that the items are shown, the same remaining time is shown and decremented.
The method I have used didn't help me much. Especially when more that one order was present. In that case, the new order's duration wouldn't decrement and it caused an infinite loop inside the useEffect.
Is there any way I can create a countdown for each one?
Do you really need to keep countdown in store? It is very bad idea, because every second u will trigger rerenders of components that using this data.
Maybe u can create an CountDown component?
function CountDown(props) {
const [counter, setCounter] = useState(+props.duration);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter - 1);
if (counter <= 0) {
clearInterval(interval);
props.onEnd?.(); // here u can pass any actions after countDown is ended
}
}), 1000);
return () => clearInterval(interval);
}, []);
return <div>{props.counter}</div>
}
But if u need to keep countdowns in redux i recommend you to move it from orders objects and place it to array for example. In this array u will be able to keep counters and intervals for every order.
Take the below code as an example.
import { useEffect, useState } from "react";
export default function Stopwatch(){
const [stopwatch, setStopwatch] = useState(0);
function updateStopwatch(){
console.log("Updating stopwatch from ", stopwatch, " to ", stopwatch+1);
setStopwatch(stopwatch + 1);
}
useEffect(() => {
const interval = setInterval(updateStopwatch, 1000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<h1>{stopwatch}</h1>
</div>
);
}
In this code, the function updateStopwatch will be invoked periodically without depending on any re-rendering of the component ( Although its invocation will cause a re-render ) and each time it will be invoked, it needs to retrieve the current up-to-date value of the state variable stopwatch, so it can update it accordingly. So that the stop watch will keep counting up.
However, what happens is that it only gets the value of stopwatch at the moment the function is declared. It's not really subscribed to a state variable but more like it just reads a constant once. And as a result the count show 1 and is stuck there.
So how can I make such a function get an up-to-date value of a state variable whenever it's invoked?
In your setState method, you can retrieve a prevState argument, which is always the right value, here is the way you could do your function based on your example :
function updateStopwatch(){
setStopwatch((prevState) => prevState + 1);
}
This is an issue of a stale closure over the initial stopwatch state value. Use a functional state update to correctly update from the previous state value instead of whatever value is closed over in callback scope at the time of execution. Move updateStopwatch declaration into the useEffect hook to remove it as an external dependency.
Example:
export default function Stopwatch(){
const [stopwatch, setStopwatch] = useState(0);
useEffect(() => {
function updateStopwatch(){
setStopwatch(stopwatch => stopwatch + 1);
}
const interval = setInterval(updateStopwatch, 1000);
return () => clearInterval(interval);
}, []);
return (
<div>
<h1>{stopwatch}</h1>
</div>
);
}
After clicking the button the console shows 0 and the page 1
function App() {
const [count, setCount] = useState(0);
const addOne = () => {
setCount(count + 1)
console.log(count)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
I think is because the setCount() is happening asynchronously but even if I add a setTimeout to the console.log(), the console keeps showing the unupdated state
Why???
The state updation in React is always asynchronous. you will find the updated state value of count in useEffect
function App() {
const [count, setCount] = useState(0);
useEffect(()=> {
console.log('count',count);
},[count])
const addOne = () => {
setCount(count + 1)
}
return (
<>
<p>{count}</p>
<button onClick={addOne}>Add</button>
</>
);
}
Closures
You are experiencing the unupdated state in the console log, because of closures.
when your function is created when the component is rendered, and closure is created with the value of count at the time the closure is created.
if the value of count is 0, and your component rerenders, a closure of your function will be created and attached to the event listener of the onlcick.
in that case, the first render of your component
const addOne = () => {
setCount(count + 1)
console.log(count)
}
is equivalent to (replace count with 0)
const addOne = () => {
setCount(0 + 1)
console.log(0)
}
therefore it makes sense in your case that count is 0 when it is console logged.
In this case, I believe its the closure you are experiencing combined with the asynchronous behavior of setState
Async behaviour
codesandbox
Async behaviour becomes a problem when asynchronous actions are occuring. setTimeout is one of the basic async actions. Async actions always require that you provide a function to the setCount function, which will accept the latest state as a parameter, with the nextState being the return value of this function. This will always ensure the current state is used to calculate the next state, regardless of when it is executed asynchronously.
const addOneAsync = () => {
setCountAsync((currentState) => {
const nextState = currentState + 1;
console.log(`nextState async ${nextState}`);
return nextState;
});
};
I have created a codesandbox demonstrating the importance of this. CLick the "Count" button fast 4 times. (or any number of times) and watch how the count result is incorrect, where the countAsync result is correct.
addOneAsync:
when the button is clicked, a closure is created around addOneAsync, but since we are using a function which accepts the currentState, when it eventually fires, the current state will be used to calculate the next state
addOne:
When the button is clicked, a closure is created around addOne where count is captured as the value at the time of the click. If you click the count button 4 times before count has increased, you will have 4 closures of addOne set to be fired, where count is captured as 0.
All 4 timeouts will fire and simply set count to 0 + 1, hence the result of 1 for the count.
Yes, you're right about the origins of this behavior and the other posters here seem to have explained how to fix it. However, I don't see the answer to your specific question:
...but even if I add a setTimeout to the console.log(), the console keeps showing the unupdated state Why???
So what you mean is that even if you handle that console.log call like so:
const addOne = () => {
setCount((count) => count + 1);
setTimeout(() => console.log(count), 1000);
}
It will STILL print the old, un-updated value of count. Why? Shouldn't the timeout allow time for count to update? I will quote the answer:
This is subtle but expected behavior. When setTimeout is scheduled it's using the value of count at the time it was scheduled. It's relying on a closure to access count asynchronously. When the component re-renders a new closure is created but that doesn't change the value that was initially closed over.
Source: https://github.com/facebook/react/issues/14010#issuecomment-433788147
So there you have it.
I'm writing an incremental-style game in React and I have this setInterval inside App.ts:
useEffect(() => {
const loop = setInterval(() => {
if (runStatus) {
setTime(time + 1);
}
}, rate);
return () => clearInterval(loop);
});
I've also got various event handlers along the lines of:
<button onClick={() => increment(counter + 1)}>Increment</button>
Unfortunately, if I click that button multiple times in a second, it will block the setInterval from updating the time state.
This state using the hooks as well as Redux stored state.
How can I, at the very least, make my setInterval tick reliably, so that I can click around the page without blocking it?
Do it like that
[] empty dependency means only execute when a component will mount.
useEffect(() => {
const loop = setInterval(() => {
if (runStatus) {
setTime(prev => prev + 1);
}
}, rate);
return () => clearInterval(loop);
}, []); <--- add that []
Notes
adding time as a dependency will create an infinite loop
you need to add runStatus or rate variable as a dependency if its dynamic state
So I'm getting into Reactjs with very basic component.
I'm logging out the same state from different functions, but what I'm seeing is the different values.
import React, { useState, useEffect, useRef } from "react";
const Test = props => {
const [count, setCount] = useState(0);
useEffect(()=>{
setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
},[props]);
function btnClick() {
const newCount = count + 1;
setCount(newCount);
console.log("count changed to: ", newCount);
}
return (
<div>
count is {count}
<br></br>
<button onClick={btnClick}>+</button>
</div>
);
};
export default Test;
Output after some clicks and wait, log is:
Test.js:8 count in interval is: 0
Test.js:15 count changed to: 1
Test.js:15 count changed to: 2
Test.js:15 count changed to: 3
Test.js:15 count changed to: 4
(8 rows) Test.js:8 count in interval is: 0
I expect the "count" to be the same in both functions.
Can any one explain this?
Thank so much.
Test only has one setInterval function where count is always 0. Since it's only created during initial render.
It never had another setInterval created because the effect never got triggered with [props] as the dependency.
To have setInterval's count change on every re-render:
Remove the dependency
Return a clean-up function inside the effect
useEffect(
() => {
const t = setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
return () => clearInterval(t); // cleanup on every re-render
}
// no dependency: effect runs on every re-render
);
But the above code will have a warning:
"missing count dependency"
So simply add count as dependency to only run the effect when count changes.
useEffect(
() => {
const t = setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
return () => clearInterval(t); // cleanup "old" setInterval
}
, [count] // ony run effect every time count changes
);
The value of count doesn't change, this is the expected behavior, though not an obvious one.
See, you even declare count as a const count, it is immutable. What is happening instead is that during the first render count gets assigned value of 0. The value of count never changes, what happens instead is that component Test is called each time you change the state, and function useState assigns different values to the constant count, which is new constant every time.
So during the first render the value of const count gets captured by closure inside your function that is called by setInterval and the value stays 0 forever.