the code bellow skips index 1 of my text, and shows just "hllo world"
export default function App() {
const [message, setMessage] = useState("");
const index = useRef(0);
const text = "hello world";
useEffect(() => {
const interval = setInterval(() => {
setMessage((prevMessage) => prevMessage + text[index.current]);
index.current++;
}, 500);
return () => clearInterval(interval);
}, []);
return (
<div className="App">
<h1>{message}</h1>
</div>
);
}
But if i change this set state to
setMessage(text.substring(0, index.current));
this works well, and i dont know because.
I presume that it is due to the fact that your callback
(prevMessage) => prevMessage + text[index.current]
is set in the queue when using setState, and runs asynchronously with increment of index. There is a possibility of data race.
When you use substring method, you get your string immediately, so it is perfectly synchronized with increment.
Please take a look at this documentation, although I think it’s quite unobvious. But your case is a good example of this asynchrounous nature of setState.
Related
I wanna stop the looping when stopState is true.
The stopState is updated when I click the stop button. but inside the startIncrement method, the stopState is always false.
This is my code:
function App() {
const [num, setNum] = useState(0)
const [stop, setStop] = useState(false)
function Delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function startIncrement(){
for(let i=0; i<100; i++){
console.log(stop) // always false even when i click the stop button
if(stop) i = 2000;
setNum(i)
await Delay(1000)
}
}
return (
<main>
<p>stop: {stop ? "true" : "false"}</p>
<p onClick={startIncrement}>{num}</p>
<button onClick={() => setStop(true)}>stop</button>
</main>
);
}
Short answer:
In order to stop the previously excuted function, you will have to invoke a `clean up` function.
Detail on how it works:
In react, each render has it's own props and state, and we can explain this in action:
First, you excuted the function startIncrement(), at the time of excution, and at that perticular render phase, the value of stop is false;
Every time the num value changes, the page renders, and it just keep going... (excute below code, you can see console prints "renders!")
Then you click setStop(true) button, and at this particular render, stop === true.
However, these two steps involves two different renders, and each renders' state and everything else (props, effect...), does not affect each other, therefore, your stop value in the function never changes.
Here's an alternative to achieve the same:
export default function App() {
const [num, setNum] = useState(0)
const [stop, setStop] = useState(null)
console.log("renders!")
useEffect(() => {
const run = setInterval(() => {
if(stop === false && num < 100) setNum(num + 1);
}, 1000);
return () => clearInterval(run) // clean up function
}, [num, stop]);
return (
<main>
<p>stop: {stop ? "true" : "false"}</p>
<p>{num}</p>
<button onClick={() => setStop(false)}>start</button>
<button onClick={() => setStop(true)}>stop</button>
</main>
);
}
The clean up function in useEffect can be seen as a "undo" function.
Sandbox here, you can read more about side effect from this post in Dan Abramov's blog
const useCounterInterval = (
startAt = 0,
countingStep = 1,
countingTime = 1000
) => {
const [counter, setCounter] = useState(startAt);
const [timerId, setTimerId] = useState(null);
const scheduleCounting = useCallback(() => {
if (timerId !== null) return;
setTimerId(
setInterval(() => {
setCounter(counter => counter + countingStep);
}, countingTime)
);
}, [timerId, countingStep, countingTime]);
const clearSchedule = useCallback(() => {
clearInterval(timerId);
setTimerId(null);
}, [timerId]);
const stopCounting = useCallback(() => {
clearSchedule();
}, [clearSchedule]);
const resetCounting = useCallback(() => {
clearSchedule();
setCounter(0);
}, [clearSchedule]);
useEffect(() => {
scheduleCounting();
// scheduleCounting();
// clearSchedule();
}, []);
return [counter, scheduleCounting, stopCounting, resetCounting];
};
Tried real hard to insert running code snippets for this code but didn't work as I thought sorry for that.
I created this custom hook to get a grasp of what hooks are.
It works but there are logical issues.
cannot 'clearInterval()' before second render (stopCounting())
Logically, can set multiple counting schedules by code (scheduleCounting())
maybe hook design is wrong at first place?
For the first issue, I cannot clear the very first scheduled timer set in motion by useEffect() before the second render where timerId is set with new value. Because obviously, stopCounting() have closure to timerId where the value is null.
And when the timing is right, I can notice by clicking on a button (depends on which event stopCounter() is bounded). I don't have a clue on how to solve this issue for now. I can just remove scheduleCounting() from useEffect() but that's not an option I wanna know if there's a way to solve this.
Second, I can set multiple counting schedule by calling scheduleCounting() in useEffect(). Come to think of it, I might call scheduleCounting() multiple times by clicking on a button really fast but seems like I can't, unlike calling stopCounting() before second render. But the logic is that I can do that right?
So, the problem here was that scheduleCounting() had closure to the same timerId value which is null
const scheduleCounting = useCallback(() => {
if (timerId !== null) return; // bypassing this line of code
The solution here was to make use of useRef(). Instead of checking if timerId is null whose value may vary depending on the creation time of functions referencing it. I can simply
const schedulerState = useRef(false)
if (schedulerState.current === true) return;
It made sure every scheduleCounting() was referencing the same latest value at any time. But using a flag(ref, state) make code less readable. I wanna keep myself away from using flags. I wonder maybe there's another reactive way of dealing with this problem?
I don't know where I'm getting wrong. Can't stop thinking I might go down the wrong road if I don't get it right. Need some guidance.
From design point of view, I'd not keep timerId in state. Instead I'd have state isCounting:
const useCounterInterval = (
startAt = 0,
countingStep = 1,
countingTime = 1000
) => {
const [isCounting, setIsCounting] = useState(false);
const [currentTick, setTick] = useState(0);
const startCounting = useCallback(() => { setIsCounting(true); }, []);
const stopCounting = useCallback(() => { setIsCounting(false); }, []);
const resetCounting = useCallback(() => { setTick(0); }, []);
useEffect(() => {
resetCounting();
if (isCounting) {
const timerId = setInterval(() => {
setTick(i => i + 1);
}, countingTime);
return () => { clearInterval(timerId); };
}
}, [isCounting, resetCounting, countingTime, startAt, countingStep]);
return [
currentTick * countingStep + startAt,
startCounting,
stopCounting,
resetCounting,
isCounting
];
};
It's easier not to store timerId but keep it in useEffect's closure. As well as it's better to return whether counting is happening(by returning isCounting).
I have a react component that performs a certain task at regular intervals of time after mounting. But I want to clear the interval once after a criterion is met. How can I achieve that?
My code
const [totalTime, setTotalTime] = React.useState(10000);
const foo = () => {
console.log("Here");
};
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
});
React.useEffect(() => {
let originalInterval;
if (totalTime > 0)
originalInterval = setInterval(() => {
foo();
console.log(totalTime);
}, 5000);
return () => clearInterval(originalInterval);
}, []);
When I watch the console even after 10000ms It is still logging Here and also totalTime is always being 10000ms. I am not able to figure out what exactly is happening.
You may need to pass the older state as an argument to the setTotalTime updater function. You also may need to pass (another) state variable as a dependency to the useEffect hook so that the function is executed every time the state variable changes
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime => totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
}, [...]);
Depends on your criteria, and what you call a criteria, but you could just use another state and then useEffect on that another state:
const [criteriaIsMet,setCriteriaIsMet] = useState(false);
useEffect(() => { if(criteriaIsMet) {clearInterval(...)} },[criteriaIsMet])
And then somewhere when you do your actual "criteria logic" you just go ahead and do setCriteriaIsMet(true)
And how would you know Id of interval in above useEffect - again you could just create a special state that will store that id, and you could set it in your original useEffect
As for why your current code is not clearing the interval is because when you use useEffect with empty array as second argument, and return function in first function argument - that will be execute on component unmounting
And these are just one of numerous options you have actually :)
I am a bit confused as to why this component does not work as expected:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // This effect depends on the `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` is not specified as a dependency
return <h1>{count}</h1>;
}
but rewriting as below works:
function Counter() {
const [count, setCount] = useState(0);
let c = count;
useEffect(() => {
const id = setInterval(() => {
setCount(c++);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
React documentation says:
The problem is that inside the setInterval callback, the value of count does not change, because we’ve created a closure with the value of count set to 0 as it was when the effect callback ran. Every second, this callback then calls setCount(0 + 1), so the count never goes above 1.
But the explanation does not make sense. So why the first code does not update count correctly but the second does?
(Also declaring as let [count, setCount] = useState(0) then using setCount(count++) works fine too).
Why it looks like it doesn't work?
There are a couple hints that can help understand what's going on.
count is const, so it'll never change in its scope. It's confusing because it looks like it's changing when calling setCount, but it never changes, the component is just called again and a new count variable is created.
When count is used in a callback, the closure captures the variable and count stays available even though the component function is finished executing. Again, it's confusing with useEffect because it looks like the callbacks are created each render cycle, capturing the latest count value, but that's not what's happening.
For clarity, let's add a suffix to variables each time they're created and see what's happening.
At mount time
function Counter() {
const [count_0, setCount_0] = useState(0);
useEffect(
// This is defined and will be called after the component is mounted.
() => {
const id_0 = setInterval(() => {
setCount_0(count_0 + 1);
}, 1000);
return () => clearInterval(id_0);
},
[]);
return <h1>{count_0}</h1>;
}
After one second
function Counter() {
const [count_1, setCount_1] = useState(0);
useEffect(
// completely ignored by useEffect since it's a mount
// effect, not an update.
() => {
const id_0 = setInterval(() => {
// setInterval still has the old callback in
// memory, so it's like it was still using
// count_0 even though we've created new variables and callbacks.
setCount_0(count_0 + 1);
}, 1000);
return () => clearInterval(id_0);
},
[]);
return <h1>{count_1}</h1>;
}
Why does it work with let c?
let makes it possible to reassign to c, which means that when it is captured by our useEffect and setInterval closures, it can still be used as if it existed, but it is still the first one defined.
At mount time
function Counter() {
const [count_0, setCount_0] = useState(0);
let c_0 = count_0;
// c_0 is captured once here
useEffect(
// Defined each render, only the first callback
// defined is kept and called once.
() => {
const id_0 = setInterval(
// Defined once, called each second.
() => setCount_0(c_0++),
1000
);
return () => clearInterval(id_0);
},
[]
);
return <h1>{count_0}</h1>;
}
After one second
function Counter() {
const [count_1, setCount_1] = useState(0);
let c_1 = count_1;
// even if c_1 was used in the new callback passed
// to useEffect, the whole callback is ignored.
useEffect(
// Defined again, but ignored completely by useEffect.
// In memory, this is the callback that useEffect has:
() => {
const id_0 = setInterval(
// In memory, c_0 is still used and reassign a new value.
() => setCount_0(c_0++),
1000
);
return () => clearInterval(id_0);
},
[]
);
return <h1>{count_1}</h1>;
}
Best practice with hooks
Since it's easy to get confused with all the callbacks and timing, and to avoid any unexpected side-effects, it's best to use the functional updater state setter argument.
// ❌ Avoid using the captured count.
setCount(count + 1)
// ✅ Use the latest state with the updater function.
setCount(currCount => currCount + 1)
In the code:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// I chose a different name to make it clear that we're
// not using the `count` variable.
const id = setInterval(() => setCount(currCount => currCount + 1), 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
There's a lot more going on, and a lot more explanation of the language needed to best explain exactly how it works and why it works like this, though I kept it focused on your examples to keep it simple.
More on closures.
useRef makes it easy
function Counter() {
const countRef = useRef(0);
useEffect(() => {
const id = setInterval(() => {
countRef.current++;
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{countRef.current}</h1>;
}
I have a search input in one of my page and I'm trying to make sure that it does not make 10 requests in one second if the user types too fast.
Seems like debouncing is the way to go. I've read multiple blog posts and SO questions. I'm trying to use debounce from lodash. I must be doing something wrong since what happens is that all my call are passed, just later.
Here is my component code:
const Partners = (props: any) => {
const [query, setQuery] = useState("");
const { partners, totalPages, currentPage } = props.partnersList;
useEffect(() => {
props.getPartners();
}, []);
useEffect(() => debounce(() => props.getPartners(query), 10000), [query]);
const handleQueryChange = (e: any) => {
setQuery(e.target.value);
};
const handlePageChange = (e: React.ChangeEvent<unknown>, value: number) => {
props.getPartners(query, value);
};
return (
<React.Fragment>
<Sidebar />
<MainContent className="iamSearchPartners">
<h1>Partners</h1>
<TextField
className="iamSearchPartners__search_input"
variant="outlined"
size="small"
onChange={handleQueryChange}
value={query}
/>
<PartnersList partners={partners} />
{totalPages > 1 && (
<Pagination
currentPage={currentPage || 1}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
)}{" "}
</MainContent>
</React.Fragment>
);
};
As you can see, I have a useEffect listening to changes to query.
debounce creates a debounced version of the function that passed as the argument. In this specific case, calling it in useEffect will create a new debounced version of the function on every render.
To mitigate this, the debounced version could be created outside of the render function, so it is not recreated on every render.
const myDebouncedFunction = debounce((handler, query) => handler(query), 10000);
const Partners = (props: any) => {
// omitted ...
useEffect(() => {
myDebouncedFunction(props.getPartners, query);
}, [query]);
// omitted ...
useMemo or putting it into a useState could work as well.
Another thing: useEffect only called the returned debounced version since it's an arrow function without braces, making it return the result of the evaluation. useEffect leverages the return of it's function to be some kind of "unsubscribe" handler, see React docs about "Effects with cleanup". So whenever query changed (after the second time), the effect was calling the function. The version above should be called on every query change.
Personally, I would try to handle the call of the debounced version of the function in the handleQueryChange instead of a useEffect, to make the "when should it happen" more clear.
I end up doing this manually honestly. Check out the differences between debouncing and throttling. I think you want to throttle the requests. If you debounce, nothing is going to happen until the timer ends.
If you want to wait 10 seconds as in your example, nothing should happen if the user types at least once every ten seconds, until 10 seconds after the last type. As opposed to throttling where a request would go out every 10 seconds.
This approach is kind of a hybrid because we are making sure the last one still goes out (as a debounce would), and throttling may not do, but we are still sending requests while the user is typing.
const timer = useRef<number>(0);
const lastEventSent = useRef(Date.now());
useEffect(() => {
if (Date.now() - lastEventSent.current < 500) {
// The event was fired too recently, but we still want
// to fire this event after the timeout if it is the last
// one (500ms in this case).
timer.current = setTimeout(() => {
lastEventSent.current = Date.now();
props.getPartners(query)
}, 500);
} else {
// An event hasn't been fired for 500ms, lets fire the event
props.getPartners(query)
lastEventSent.current = Date.now();
// And if the user was typing, there is probably a timer, lets clear that
clearTimeout(timer.current);
}
// Cleanup the timer if there was one and this effect is reloaded.
return () => clearTimeout(timer.current);
}, [query]);