Why my sleep function doesn't make the React application freeze? - javascript

Why my sleep function doesn't make the React application freeze? Here's my code:
import React from "react";
import "./App.css";
function App() {
const [count, setCount] = React.useState(0);
(async () => {
const sleep = async (miliseconds: number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("");
}, miliseconds);
});
};
await sleep(5000);
console.log("hey");
})();
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</div>
);
}
export default App;
So, I have an IIFE sleep function inside the component that is supposed to execute before every render. But when I click on the increment button of my counter, the DOM being updated immediately without waiting for my sleep function to finish its execution. What's wrong with it? If I use for loop to freeze the app everything works as expected but the sleep function implemented with promise doesn't cause my app freeze.

What this block of code does:
(async () => {
const sleep = async (miliseconds: number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("");
}, miliseconds);
});
};
await sleep(5000);
console.log("hey");
})();
is it creates a Promise that resolves after 5 seconds. That's it. The Promise isn't used anywhere, and so it isn't connected to anything in the rest of the code.
function App() {
const [count, setCount] = React.useState(0);
// here, create a Promise that resolves after 5 seconds, and don't do anything with it
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</div>
);
}
The App's return still executes immediately when App is called, so there's no delay before it renders.
If you wanted to add a render delay, conditionally render the component and set a state after 5 seconds.
function App() {
const [count, setCount] = React.useState(0);
const [render, setRender] = React.useState(false);
React.useEffect(() => {
setTimeout(() => {
setRender(true);
}, 5000);
}, []);
return !render ? null : (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount((count) => count + 1)}>+</button>
</div>
);
}
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>

Related

Callback Function of setInterval is not having access to updated state if the state is updated after the interval is started

export default function App() {
const [active, setActive] = useState("");
function handleButtonClick(value) {
if (!active) {
setActive(value);
setTimeout(() => {
setActive("");
console.log(active);
}, 3000);
} else {
let interval = setInterval(() => {
console.log(active);
if (!active) {
setActive(value);
setTimeout(() => {
setActive("");
}, 3000);
clearInterval(interval);
}
}, 0);
}
}
return (
<div
style={{
display: "flex",
flexDirection: "column",
height: "100vh",
justifyContent: "space-between"
}}
>
<div>
<button onClick={() => handleButtonClick("first")}>first</button>
<button onClick={() => handleButtonClick("second")}>second</button>
<button onClick={() => handleButtonClick("third")}>third</button>
</div>
<div>{active.length > 0 && <p>{active} button Clicked</p>}</div>
</div>
);
}
The callback function of setInterval in else case of handleButtonClick function is not having access to updated state, if the setInterval is started and then after that state is updated then the callback function is only having access to previous state but not updated state. How can i fix this
Required functionality
when any button is clicked the active state is set to respective value and then after 3 seconds with setTimeout the state is changed to empty string as i want it to act it as a poppup.
if another button is clicked before 3 seconds then it should wait for current 3 seocnds to complete and should show the respestive button clicked popup
The active identifier that the interval callback closes over will always be the same inside the interval - it'll be that one const [active ... that was declared at the rendering where the handleButtonClick was created.
It looks like you want to queue up button values pressed when one is already active - consider pushing the future values to a stateful array, and use useEffect to move the first value in that array to the active state when needed. This way, you'll never be depending on values in a possibly stale closure.
const { useState, useEffect } = React;
function App() {
const [active, setActive] = useState("");
const [queue, setQueue] = useState([]);
const toggleActive = (val) => {
setActive(val);
const timeoutId = setTimeout(() => {
setActive('');
}, 3000);
// don't forget to clean up
return () => clearTimeout(timeoutId);
};
function handleButtonClick(value) {
if (active) {
setQueue([...queue, value]);
} else {
toggleActive(value);
}
}
useEffect(() => {
// Run the next value in the queue, if active is empty and the queue is not
if (!active && queue.length) {
toggleActive(queue[0]);
setQueue(queue.slice(1));
}
}, [active]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
height: "100vh",
justifyContent: "space-between"
}}
>
<div>
<button onClick={() => handleButtonClick("first")}>first</button>
<button onClick={() => handleButtonClick("second")}>second</button>
<button onClick={() => handleButtonClick("third")}>third</button>
</div>
<div>{active.length > 0 && <p>{active} button Clicked</p>}</div>
</div>
);
}
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
The required functionality is probably called 'debounce' and can be easily implemented in React using the 'useEffect' hook:
const [active, setActive] = useState('')
useEffect(() => {
const timer = setTimeout(() => setActive(''), 3000);
return () => clearTimeout(timer);
}, [active]);
const handleClick = (value) => {
setActive(value);
}
Here's an alternative approach that uses promises instead of timeout. I think you will find that the complexity is reduced. Note we are careful to avoid setState calls on unmounted components. See the You Might Not Need An Effect guide from React -
const sleep = ms =>
new Promise(r => setTimeout(r, ms))
function App() {
const [active, setActive] = React.useState("")
const queue = React.useRef(Promise.resolve())
const mounted = React.useRef(true)
React.useEffect(_ => _ => { mounted.current = false },[])
const alert = message => event => {
queue.current = queue.current
.then(_ => mounted.current && setActive(message))
.then(_ => sleep(3000))
.then(_ => mounted.current && setActive(""))
}
return (
<div>
<button onClick={alert("first")}>first</button>
<button onClick={alert("second")}>second</button>
<button onClick={alert("third")}>third</button>
{active && <p>{active}</p>}
</div>
)
}
ReactDOM.createRoot(document.querySelector("#app")).render(<App />)
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
setState (here setActive) is a async function hence updated value wont be print in console used just after it.
you can use
setActive(prevState => { return updatedState; }
here prevState will have access to updated state

Why is prior useEffect() still running for a significant amount of time even after the component is rendered?

I'm implementing stopwatch in ReactJs this is how my code looks as of now:
const App: React.FC = () => {
const [seconds, setSeconds] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const secondsToTimerFormat = (seconds: number): string => {
console.log(seconds)
return (seconds-seconds%60)/60+":"+seconds%60
}
const manipulateTimer = (toPauseTimer: boolean) => {
setIsPaused(toPauseTimer);
}
useEffect(() => {
if(!isPaused){
setTimeout(() => {
setSeconds(seconds + 1)
}, 1000)
}
}, [seconds, isPaused])
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button onClick={() => {manipulateTimer(true)}}>Pause</button>
<button onClick={() => {manipulateTimer(false)}}>Resume</button>
<button onClick={() => {
setSeconds(0);
}}>Reset</button>
</div>
</div>
);
}
I'm expecting this to work normally. But the "Reset" button is not working as expected.
If I click on "Reset" after 13 seconds, this is the console.log() output.
If I add a new variable inside useEffect(), say something like let execute: boolean = true and then set it to false in useEffect() clean up, everything is working as expected.
So, I know the fix, but I want to know the reason behind the current behaviour. I understand that when I click on reset, there is already a useEffect() running with seconds value as 13. But since its setTimeout() ends in one second and at the same time, I'm doing setSeconds(0), why would the previous useEffect() run multiple times before coming to halt?
Issues like this usually arise because the timers being used are not being cleared between renders. Also, when the next state depends on the current state, it is better to use the second form of the state setter function which takes the current state as the parameter and returns the next state. Modify the useEffect as given below to get this to work:
useEffect(() => {
let timer;
if (!isPaused) {
timer = setTimeout(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
}
return () => {
if (timer) clearTimeout(timer);
};
}, [seconds, isPaused]);
Try using setInterval and separate methods for handling the timer state:
import { useState } from "react";
export default function App() {
const [seconds, setSeconds] = useState(0);
const [intervalId, setIntervalId] = useState(0);
const secondsToTimerFormat = (seconds) => {
console.log(seconds);
return (seconds - (seconds % 60)) / 60 + ":" + (seconds % 60);
};
const handleStart = () => {
const id = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
setIntervalId(id);
};
const handlePause = () => {
clearInterval(intervalId);
};
const handleReset = () => {
handlePause();
setSeconds(0);
};
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button
onClick={() => {
handlePause();
}}
>
Pause
</button>
<button
onClick={() => {
handleStart();
}}
>
Resume
</button>
<button
onClick={() => {
handleReset();
}}
>
Reset
</button>
</div>
</div>
);
}
Link to sandbox

How can I start / stop setInterval?

I've tried different ways, but It doesn't works.
[...]
const [automatic, setAutomatic] = useState(false);
[...]
var startAuto;
useEffect(() => {
if (!automatic) {
console.log("stop");
clearInterval(startAuto);
} else {
startAuto = setInterval(() => {
changeQuestion("+");
}, 5 * 1000);
}
}, [automatic]);
[...]
<Button
onPress={() => setAutomatic(!automatic)}
title="turn on/off"
/>
[...]
It works when I put a setTimeout outside the useEffect, that way:
setTimeout(() => { clearInterval(startAuto); alert('stop'); }, 10000);
But I want to have a button to start / stop
Your var startAuto; is redeclared on each render, and since changing the state causes a re-render, it never holds the reference to the interval, which is never cleared.
Use the useEffect cleanup function to clear the interval. Whenever automatic changes, it would call the cleanup (if returned by the previous invocation), and if automatic is true it would create a new interval loop, and return a new cleanup function of the current interval.
useEffect(() => {
if(!automatic) return;
const startAuto = setInterval(() => {
changeQuestion("+");
}, 5 * 1000);
return () => {
clearInterval(startAuto);
};
}, [automatic]);
Working example:
const { useState, useEffect } = React;
const Demo = () => {
const [automatic, setAutomatic] = useState(false);
const [question, changeQuestion] = useState(0);
useEffect(() => {
if(!automatic) return;
const startAuto = setInterval(() => {
changeQuestion(q => q + 1);
}, 5 * 100);
return () => {
clearInterval(startAuto);
};
}, [automatic]);
return (
<div>
<button
onClick={() => setAutomatic(!automatic)}
>
turn {automatic ? 'off' : 'on'}
</button>
<p>{question}</p>
</div>
);
}
ReactDOM
.createRoot(root)
.render(<Demo />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
For example, you can check and use this hook:
https://usehooks-ts.com/react-hook/use-interval
export default function Component() {
// The counter
const [count, setCount] = useState<number>(0)
// Dynamic delay
const [delay, setDelay] = useState<number>(1000)
// ON/OFF
const [isPlaying, setPlaying] = useState<boolean>(false)
useInterval(
() => {
// Your custom logic here
setCount(count + 1)
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null,
)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDelay(Number(event.target.value))
}
return (
<>
<h1>{count}</h1>
<button onClick={() => setPlaying(!isPlaying)}>
{isPlaying ? 'pause' : 'play'}
</button>
<p>
<label htmlFor="delay">Delay: </label>
<input
type="number"
name="delay"
onChange={handleChange}
value={delay}
/>
</p>
</>
)
}

Async Increment at once in a React Counter using hooks

Following is the code which increments the value at once after 4 sec, though I am expecting the batch of update should increment the valus after 4 sec only on multiple clicks.
Ex. - Let us say, I clicked the "Async Increase" button 5 times, then after 4 sec the counter increases to 1,2,3,4,5 but I want after 4 sec it should increment making it 1 then after 4 sec it should increment it to 2, then after 4 sec it should increase to 3 and so on.
Let me know how can I fix this.
Code -
const UseStateCounter = () => {
const [value, setValue] = useState(0);
const reset = () => {
setValue(0);
}
const asyncIncrease = () => {
setTimeout(() => {
setValue(prevValue => prevValue + 1);
}, 4000);
}
const asyncDecrease = () => {
setTimeout(() => {
setValue(prevValue => prevValue - 1);
}, 4000);
}
return <>
<section style={{margin: '4rem 0'}}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</>
};
export default UseStateCounter;
To do that, wait for the previous change to finish before you start the next one. For instance, one way to do that is with a promise chain; see comments:
// Promise-ified version of setTimeout
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const UseStateCounter = () => {
const [value, setValue] = useState(0);
// Remember the promise in a ref we initialize
// with a fulfilled promise
const changeRef = useRef(Promise.resolve());
/* Alternatively, if there's a lot of initialization logic
or object construction, you might use `null` above
and then:
if (!changeRef.current) {
changeRef.current = Promise.resolve();
}
*/
const reset = () => {
queueValueUpdate(0, false);
};
// A function to do the queued update
const queueValueUpdate = (change, isDelta = true) => {
changeRef.current = changeRef.current
// Wait for the previous one to complete, then
.then(() => timeout(4000)) // Add a 4s delay
// Then do the update
.then(() => setValue(prevValue => isDelta ? prevValue + change : change));
};
const asyncIncrease = () => {
queueValueUpdate(1);
};
const asyncDecrease = () => {
queueValueUpdate(-1);
};
// Sadly, Stack Snippets can't handle the <>...</> form
return <React.Fragment>
<section style={{ margin: '4rem 0' }}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</React.Fragment>;
};
export default UseStateCounter;
Live Example:
const {useState, useRef} = React;
// Promise-ified version of setTimeout
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const UseStateCounter = () => {
const [value, setValue] = useState(0);
// Remember the promise in a ref we initialize
// with a fulfilled promise
const changeRef = useRef(Promise.resolve());
/* Alternatively, if there's a lot of initialization logic
or object construction, you might use `null` above
and then:
if (!changeRef.current) {
changeRef.current = Promise.resolve();
}
*/
const reset = () => {
queueValueUpdate(0, false);
};
// A function to do the queued update
const queueValueUpdate = (change, isDelta = true) => {
changeRef.current = changeRef.current
// Wait for the previous one to complete, then
.then(() => timeout(4000)) // Add a 4s delay
// Then do the update
.then(() => setValue(prevValue => isDelta ? prevValue + change : change));
};
const asyncIncrease = () => {
queueValueUpdate(1);
};
const asyncDecrease = () => {
queueValueUpdate(-1);
};
// Sadly, Stack Snippets can't handle the <>...</> form
return <React.Fragment>
<section style={{ margin: '4rem 0' }}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</React.Fragment>;
};
ReactDOM.render(<UseStateCounter />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Note: Normally I make a big noise about handling promise rejections, but none of the promise stuff above will ever reject, so I'm comfortable not bothering with catch in queueValueUpdate.

Latest function callback is not caught in React

In the example bellow, Child component calls onFinish callback 5 seconds after clicking on button. The problem is that onFinish callback can change in those 5 seconds, but the it will call the last caught one.
import React, { useState } from "react";
const Child = ({ onFinish }) => {
const [finished, setFinished] = useState(false);
const finish = async () => {
setFinished(true);
setTimeout(() => onFinish(), 5000);
};
return finished ? (
<p>Wait 5 seconds and increment while waiting.</p>
) : (
<button onClick={finish}>Click here to finish</button>
);
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<Child onFinish={() => alert(`Finished on count: ${count}`)} />
</>
);
}
The workaroud for this one is to replace finish with the following:
const cb = useRef();
cb.current = onFinish;
const finish = async () => {
setFinished(true);
setTimeout(() => cb.current(), 5000);
};
Is there a better approach to update the callback to the latest one?
Yes, you can check the current state and compare it with prev state like
setFinished((prevState) => newState)

Categories