How to use the latest value of the state inside a useEffect? - javascript

My component renders an image every 4 seconds. When I click on the image I want to stop rendering new images. For that I've used a useEffect hook. When I click to the image, the state hasToRefresh changes it's values, but inside useEffect it doesn't change. This is my code:
import { useEffect, useState } from "react";
const VariableImage = () => {
const imageUrl = "https://picsum.photos/200";
const imageRefresh = "?forcerefresh=";
const [image, setImage] = useState(imageUrl);
const [hasToRefresh, setHasToRefresh] = useState(true);
useEffect(() => {
if (hasToRefresh) {
setInterval(() => {
setImage(imageUrl + imageRefresh + Math.random());
}, 4000);
}
}, [imageUrl, imageRefresh, hasToRefresh]);
return (
<>
<img
src={image}
onClick={() => setHasToRefresh(!hasToRefresh)}
alt="scenery"
height="200"
width="200"
/>
</>
);
};
export default VariableImage;
Also in sandbox: https://codesandbox.io/s/variable-image-zxhejs
How can I do for when I click the image to not render more images?
If anyone could help me I would be very grateful. Thanks.

You're never stopping your interval. And to only trigger the useEffect() for hasToRefresh, I would move the creation of image string outside of it.
const VariableImage = () => {
const imageUrl = "https://picsum.photos/200";
const imageRefresh = "?forcerefresh=";
const [imageNumber, setImageNumber] = useState(Math.random());
const image = imageUrl + imageRefresh + imageNumber;
const [hasToRefresh, setHasToRefresh] = useState(true);
const intervalRef = useRef();
useEffect(() => {
if (hasToRefresh) {
intervalRef.current = setInterval(() => {
setImageNumber(Math.random());
}, 1000);
}
return () => {
intervalRef.current && clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [hasToRefresh]);
return (
<>
<img
src={image}
onClick={() => setHasToRefresh(!hasToRefresh)}
alt="scenery"
height="200"
width="200"
/>
</>
);
};
Here's the updated codesandbox: https://codesandbox.io/s/variable-image-forked-oxfgc9?file=/src/VariableImage/VariableImage.js:54-896

As Roy Schut mentioned, you never stop your timer. But the best option would be here to stop the timer when the image shouldn't be refreshed. Here's the code I would prefer.
import { useEffect, useState, useRef } from "react";
const VariableImage = () => {
const imageUrl = "https://picsum.photos/200";
const imageRefresh = "?forcerefresh=";
const [image, setImage] = useState(imageUrl);
const [hasToRefresh, setHasToRefresh] = useState(true);
const intervalRef = useRef(null);
useEffect(() => {
startTimer();
return () => stopTimer();
}, []);
const startTimer = () => {
intervalRef.current = setInterval(() => {
setImage(imageUrl + imageRefresh + Math.random());
}, 4000);
};
const stopTimer = () => {
clearInterval(intervalRef.current);
};
const toggleRefresh = () => {
if (hasToRefresh) {
stopTimer();
} else {
startTimer();
}
setHasToRefresh(state => !state);
};
return (
<>
<img
src={image}
onClick={() => toggleRefresh()}
alt="scenery"
height="200"
width="200"
/>
</>
);
};
export default VariableImage;

Related

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>
</>
)
}

How to render an image every 5 seconds in React?

Every time you enter this url: https://picsum.photos/200, is shown a different image. I want my react component to render every 5 seconds a different image with this url, but I can't do it. This is my code:
import { useEffect, useState } from "react";
const VariableImage = () => {
const imageUrl = "https://picsum.photos/200";
const [image, setImage] = useState(imageUrl);
useEffect(() => {
setInterval(() => {
const newImage = new Image();
newImage.src = imageUrl;
setImage(imageUrl);
}, 5000);
}, [imageUrl]);
return (
<>
<img src={image} alt="scenery" height="200" width="200" />
</>
);
};
export default VariableImage;
An image is shown in first render but later don't change.
If anyone could help me I would be very grateful. Thanks.
Add a dummy randomized query parameter to the external URL so as to force the browser to make a new request (and give you a new image).
Doing new Image isn't helping you any here - you can leave that off entirely.
const { useEffect, useState } = React;
const imageUrl = "https://picsum.photos/200";
const VariableImage = () => {
const [src, setSrc] = useState(imageUrl);
useEffect(() => {
setInterval(() => {
setSrc(imageUrl + '?forcerefresh=' + Math.random());
}, 5000);
}, []);
return <img src={src} alt="scenery" height="200" width="200" />;
};
ReactDOM.createRoot(document.querySelector('.react')).render(<VariableImage />);
<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>
'
You are adding the same imageUrl(reference) into it, because of which react
is not able to find any changes so it is not updating the state.
'
Example with vanilla js.
const img = document.querySelector("img");
setInterval(() => {
img.src = "https://picsum.photos/200" + "?forcerefresh=" + Math.random();
}, 5000);
<image src="https://picsum.photos/200" />
Please add clearInterval(id) to stop the time when the component unmounts.
const { useEffect, useState } = React;
const imageUrl = "https://picsum.photos/200";
const VariableImage = () => {
const [src, setSrc] = useState(imageUrl);
useEffect(() => {
const id = setInterval(() => {
setSrc(imageUrl + "?forcerefresh=" + Math.random());
}, 5000);
return () => clearInterval(id)
}, []);
return (
<>
<img src={src} alt="scenery" height="200" width="200" />
</>
)
};

Is it possible to expose a function defined within a React function component to be called in other components?

I'm refactoring some old code for an alert widget and am abstracting it into its own component that uses DOM portals and conditional rendering. I want to keep as much of the work inside of this component as I possibly can, so ideally I'd love to be able to expose the Alert component itself as well as a function defined inside of that component triggers the render state and style animations so that no outside state management is required. Something like this is what I'm looking to do:
import Alert, { renderAlert } from '../Alert'
const CopyButton = () => (
<>
<Alert text="Text copied!" />
<button onClick={() => renderAlert()}>Copy Your Text</button>
</>
)
Here's what I currently have for the Alert component - right now it takes in a state variable from outside that just flips when the button is clicked and triggers the useEffect inside of the Alert to trigger the renderAlert function. I'd love to just expose renderAlert directly from the component so I can call it without the additional state variable like above.
const Alert = ({ label, color, stateTrigger }) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger])
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
};
export default Alert;
Though it's not common to "reach" into other components and invoke functions, React does allow a "backdoor" to do so.
useImperativeHandle
React.forwardRef
The idea is to expose out the renderAlert function imperatively via the React ref system.
Example:
import { forwardRef, useImperativeHandle } from 'react';
const Alert = forwardRef(({ label, color, stateTrigger }, ref) => {
const { Alert__Container, Alert, open } = styles;
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
const portalElement = document.getElementById('portal');
const renderAlert = (): void => {
setAlertRendered(false);
setAlertVisible(false);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
useEffect(() => {
renderAlert();
}, [stateTrigger]);
useImperativeHandle(ref, () => ({
renderAlert,
}));
const ele = (
<div className={Alert__Container}>
{ alertRendered && (
<div className={`${Alert} ${alertVisible ? open : ''}`}>
<DesignLibAlert label={label} color={color}/>
</div>
)}
</div>
);
return portalElement
? ReactDOM.createPortal(ele, portalElement) : null;
});
export default Alert;
...
import { useRef } from 'react';
import Alert from '../Alert'
const CopyButton = () => {
const ref = useRef();
const clickHandler = () => {
ref.current?.renderAlert();
};
return (
<>
<Alert ref={ref} text="Text copied!" />
<button onClick={clickHandler}>Copy Your Text</button>
</>
)
};
A more React-way to accomplish this might be to abstract the Alert state into an AlertProvider that renders the portal and handles the rendering of the alert and provides the renderAlert function via the context.
Example:
import { createContext, useContext, useState } from "react";
interface I_Alert {
renderAlert: (text: string) => void;
}
const AlertContext = createContext<I_Alert>({
renderAlert: () => {}
});
const useAlert = () => useContext(AlertContext);
const AlertProvider = ({ children }: { children: React.ReactElement }) => {
const [text, setText] = useState<string>("");
const [alertVisible, setAlertVisible] = useState<boolean>(false);
const [alertRendered, setAlertRendered] = useState<boolean>(false);
...
const renderAlert = (text: string): void => {
setAlertRendered(false);
setAlertVisible(false);
setText(text);
setTimeout(() => {
setAlertVisible(true);
}, 5);
setAlertRendered(true);
setTimeout(() => {
setTimeout(() => {
setAlertRendered(false);
}, 251);
setAlertVisible(false);
}, 3000);
};
const ele = <div>{alertRendered && <div> ..... </div>}</div>;
return (
<AlertContext.Provider value={{ renderAlert }}>
{children}
// ... portal ...
</AlertContext.Provider>
);
};
...
const CopyButton = () => {
const { renderAlert } = useAlert();
const clickHandler = () => {
renderAlert("Text copied!");
};
return (
<>
<button onClick={clickHandler}>Copy Your Text</button>
</>
);
};
...
function App() {
return (
<AlertProvider>
...
<div className="App">
...
<CopyButton />
...
</div>
...
</AlertProvider>
);
}

timer for react function not starting correctly

I'm trying to time how long a user takes to answer a question in a quiz.
The current code looks something like this
function Answer({ currentQuestion, submitAnswer }) {
var start = Date.now();
const inputsRef = useRef();
// const [secondsRemaining, setSecondsRemaining] = useState(0)
const handleInput = (e) => {
let value = e.target.value;
let nth = parseInt(e.target.dataset.nth, 10);
if (value === "") return;
let inputs = inputsRef.current.querySelectorAll("input");
inputs.forEach((element) => {
if (parseInt(element.dataset.nth, 10) === nth + 1) {
element.focus();
element.select();
}
});
};
const handleSubmit = () => {
let finalValue = "";
const finished = Date.now()
const secondsCompleted = finished - start;
let seconds = Math.floor((secondsCompleted / 1000) % 60);
console.log( seconds)
let inputs = inputsRef.current.querySelectorAll("input");
inputs.forEach((element) => (finalValue += element.value));
finalValue = finalValue.toUpperCase();
submitAnswer(finalValue);
};
useEffect(() => {
inputsRef.current.querySelectorAll("input")[0].focus();
}, []);
return (
<div>
<div
ref={inputsRef}
className="input-container flex justify-center flex-wrap"
>
{.... <input.../>}
....
);
}
Currently the timer only begins when use inputs a value into the input form.
How can I have it start when this function renders?
You should use state for the start in your component because each time there is update inside your application, it will change and it will never be the right time the user started to see the question. And you should add useEffect to check when the question changes and update the start time.
You should make it like this:
const [start, setStart] = useState();
useEffect(() => {
setStart(Date.now());
}, [currentQuestion])
You could do something like this:
import { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
function Answer({ currentQuestion, submitAnswer }) {
const inputsRef = useRef();
const [secondsRemaining, setSecondsRemaining] = useState(0);
const handleInput = (e) => {
if (!secondsRemaining) {
setSecondsRemaining(new Date().getTime());
}
};
const handleSubmit = () => {
const time = new Date().getTime();
setSecondsRemaining(time - secondsRemaining);
};
useEffect(() => {
inputsRef.current.focus();
}, []);
console.log(secondsRemaining);
return (
<div>
<div ref={inputsRef} onChange={handleInput}>
<input />
<button type="submit" onClick={handleSubmit}>
Submit
</button>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Answer />, rootElement);

Start interval after time delay and stop on button release / React

I am trying to build a simple plus/minus-control in React. When clicked on either plus or minus (triggered by onMouseDown) the value should change by a defined step and when the button is held the value should in-/decrease at a specified interval after a specified delay. When the button is released (onMouseUp), the interval should stop.
The code below runs ok on onMouseDown and hold, but when I just click on the button the interval starts anyway. I see that I need to make sure that the button is still down before the interval is started, but how do I achieve that? Thank you for any insights.
let plusTimer = useRef(null);
const increment = () => {
setMyValue(prev => prev + myStep);
setTimeout(() => {
plusTimer.current = setInterval(
() => setMyValue(prev => prev + myStep),
100
);
}, 500);
};
const intervalClear = () => {
clearInterval(plusTimer.current);
};
I think I will let the code speak for itself:
const {useCallback, useEffect, useState} = React;
const CASCADE_DELAY_MS = 1000;
const CASCADE_INTERVAL_MS = 100;
function useDelayedCascadeUpdate(intervalTime, delay, step, callback) {
const [started, setStarted] = useState(false);
const [running, setRunning] = useState(false);
const update = useCallback(() => callback((count) => count + step), [
callback,
step
]);
const handler = useCallback(() => {
update();
setStarted(true);
}, [update, setStarted]);
const reset = useCallback(() => {
setStarted(false);
setRunning(false);
}, [setStarted, setRunning]);
useEffect(() => {
if (started) {
const handler = setTimeout(() => setRunning(true), delay);
return () => {
clearTimeout(handler);
};
}
}, [started, setRunning, delay]);
useEffect(() => {
if (running) {
const handler = setInterval(update, intervalTime);
return () => {
clearInterval(handler);
};
}
}, [running, update, intervalTime]);
return [handler, reset];
}
function App() {
const [count, setCount] = useState(0);
const [incrementHandler, incrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
1,
setCount
);
const [decrementHandler, decrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
-1,
setCount
);
return (
<div>
<div>{count}</div>
<button onMouseDown={incrementHandler} onMouseUp={incrementReset}>
+
</button>
<button onMouseDown={decrementHandler} onMouseUp={decrementReset}>
-
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

Categories