Currently useEffect is fired when just one of the dependencies have changed.
How could I update it / use it to fire back when both ( or all ) of the dependencies have changed?
You'll need to add some logic to call your effect when all dependencies have changed. Here's useEffectAllDepsChange that should achieve your desired behavior.
The strategy here is to compare the previous deps with the current. If they aren't all different, we keep the previous deps in a ref an don't update it until they are. This allows you to change the deps multiple times before the the effect is called.
import React, { useEffect, useState, useRef } from "react";
// taken from https://usehooks.com/usePrevious/
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function useEffectAllDepsChange(fn, deps) {
const prevDeps = usePrevious(deps);
const changeTarget = useRef();
useEffect(() => {
// nothing to compare to yet
if (changeTarget.current === undefined) {
changeTarget.current = prevDeps;
}
// we're mounting, so call the callback
if (changeTarget.current === undefined) {
return fn();
}
// make sure every dependency has changed
if (changeTarget.current.every((dep, i) => dep !== deps[i])) {
changeTarget.current = deps;
return fn();
}
}, [fn, prevDeps, deps]);
}
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
useEffectAllDepsChange(() => {
console.log("running effect", [a, b]);
}, [a, b]);
return (
<div>
<button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
<button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
</div>
);
}
An alternate approach inspired by Richard is cleaner, but with the downside of more renders across updates.
function useEffectAllDepsChange(fn, deps) {
const [changeTarget, setChangeTarget] = useState(deps);
useEffect(() => {
setChangeTarget(prev => {
if (prev.every((dep, i) => dep !== deps[i])) {
return deps;
}
return prev;
});
}, [deps]);
useEffect(fn, changeTarget);
}
You'll have to track the previous values of your dependencies and check if only one of them changed, or both/all. Basic implementation could look like this:
import React from "react";
const usePrev = value => {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const App = () => {
const [foo, setFoo] = React.useState(0);
const [bar, setBar] = React.useState(0);
const prevFoo = usePrev(foo);
const prevBar = usePrev(bar);
React.useEffect(() => {
if (prevFoo !== foo && prevBar !== bar) {
console.log("both foo and bar changed!");
}
}, [prevFoo, prevBar, foo, bar]);
return (
<div className="App">
<h2>foo: {foo}</h2>
<h2>bar: {bar}</h2>
<button onClick={() => setFoo(v => v + 1)}>Increment foo</button>
<button onClick={() => setBar(v => v + 1)}>Increment bar</button>
<button
onClick={() => {
setFoo(v => v + 1);
setBar(v => v + 1);
}}
>
Increment both
</button>
</div>
);
};
export default App;
Here is also a CodeSandbox link to play around.
You can check how the usePrev hook works elsewhere, e.g here.
I like #AustinBrunkhorst's soultion, but you can do it with less code.
Use a state object that is only updated when your criteria is met, and set it within a 2nd useEffect.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const [ab, setAB] = useState({a, b});
useEffect(() => {
setAB(prev => {
console.log('prev AB', prev)
return (a !== prev.a && b !== prev.b)
? {a,b}
: prev; // do nothing
})
}, [a, b])
useEffect(() => {
console.log('both have changed')
}, [ab])
return (
<div className="App">
<div>Click on a button to increment its value.</div>
<button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
<button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
</div>
);
}
FWIW, react-use is a nice library of additional hooks for react that has ~30k stars on GitHub:
https://github.com/streamich/react-use
And one of those custom hooks is the useCustomCompareEffect:
https://github.com/streamich/react-use/blob/master/docs/useCustomCompareEffect.md
Which could be easily used to handle this kind of custom comparison
To demonstrate how you can compose hooks in various manners, here's my approach. This one doesn't invoke the effect in the initial attribution.
import React, { useEffect, useRef, useState } from "react";
import "./styles.css";
function usePrevious(state) {
const ref = useRef();
useEffect(() => {
ref.current = state;
});
return ref.current;
}
function useAllChanged(callback, array) {
const previousArray = usePrevious(array);
console.log("useAllChanged", array, previousArray);
if (previousArray === undefined) return;
const allChanged = array.every((state, index) => {
const previous = previousArray[index];
return previous !== state;
});
if (allChanged) {
callback(array, previousArray);
}
}
const randomIncrement = () => Math.floor(Math.random() * 4);
export default function App() {
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const [state3, setState3] = useState(0);
useAllChanged(
(state, prev) => {
alert("Everything changed!");
console.info(state, prev);
},
[state1, state2, state3]
);
const onClick = () => {
console.info("onClick");
setState1(state => state + randomIncrement());
setState2(state => state + randomIncrement());
setState3(state => state + randomIncrement());
};
return (
<div className="App">
<p>State 1: {state1}</p>
<p>State 2: {state2}</p>
<p>State 3: {state3}</p>
<button onClick={onClick}>Randomly increment</button>
</div>
);
}
Related
I am passing the increment function to a list of children (Row), but the count is never actually changed, I know that something about doing this in the children's useEffect is off. But I am still not able to understand this behavior.
Also, I am not setting the dependency array, because in this case, the count will run infinitely.
import { useCallback, useEffect, useState } from "react";
import "./styles.css";
const list = ["One", "Two", "Three"];
export default function App() {
const [count, setCount] = useState(0);
const handleOnClick = useCallback(() => {
setCount(count + 1);
}, [count]);
useEffect(() => {
if (list.length === count) {
alert("yaaay!");
}
}, [count]);
return (
<div className="App">
<h1>Count is: {count}</h1>
{list.map((item) => (
<Row key={item} name={item} addOne={handleOnClick} />
))}
</div>
);
}
const Row = ({ addOne, name }) => {
useEffect(() => {
addOne();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <p>{name}</p>;
};
The output is:
Count is: 1
One
Two
Three
Expected:
Count is: 3
One
Two
Three
okay i've created a demo
Couple of things. first, you need to set the useState values through the callback function since we are updating the counter value continuously
Secondly, need to use useRef in the child component to make sure child component is visited In each iteration. Do the checking inside child component useEffect
export default function App() {
const [count, setCount] = React.useState(0);
const handleOnClick = React.useCallback(() => {
setCount((count) => count + 1);
}, [count, setCount]);
React.useEffect(() => {
if (list.length === count) {
alert('yaaay!');
}
}, [count]);
return (
<div className="App">
<h1>Count is: {count}</h1>
{list.map((item) => (
<Row key={item} name={item} addOne={handleOnClick} />
))}
</div>
);
}
const Row =({ addOne, name }) => {
const dataFetchedRef = React.useRef(false);
React.useEffect(() => {
if (dataFetchedRef.current) return;
dataFetchedRef.current = true;
addOne();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [name]);
return <p>{name}</p>;
};
I am following this simple tutorial and can't get my saved state to work.
I can see in the comments that other users are having to work around this issue.
import React, { useState, useRef, useEffect } from "react"
import TodoList from "./TodoList"
import { v4 as uuidv4 } from 'uuid';
const LOCAL_STORAGE_KEY = 'todosApp.todos'
function App() {
const [todos, setTodos] = useState([])
const todoNameRef = useRef()
useEffect(() => {
console.log(`useEffect[]`)
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedTodos) {
console.log(`set todos to: ${JSON.stringify(storedTodos)}`)
setTodos(storedTodos)
// can't print here - value is set asynchronously
// console.log(`loaded todos: ${JSON.stringify(todos)}`)
}
}, [])
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
console.log(`toggleTodo: ${todo.name}`)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAddTodo(e) {
const name = todoNameRef.current.value
console.log(`handleAddTodo: ${name}`)
// setTodos(todos.concat({completed: false, name: todoNameRef.current.value}))
if (name === '') {
return
}
setTodos([...todos, { id:uuidv4(), name:name, complete:false }])
}
return (
<div>
<TodoList todos={todos} handleCheckboxChanged={toggleTodo}/>
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed</button>
<div>0 left to do</div>
</div>
)
}
export default App;
Here's the output:
useEffect[]
App.js:15 set todos to: [{"id":"77fe1e9e-91aa-4a34-9bfb-b1842ea5518d","name":"asfd","complete":false},{"id":"8dabea66-4ed9-4f10-9003-af1b34b4558a","name":"asfd","complete":false},{"id":"6d4e9350-11cd-4ace-8766-485e1f8817ad","name":"asfd","complete":false}]
App.js:23 useEffect[todos]: []
App.js:12 useEffect[]
App.js:15 set todos to: []
App.js:23 useEffect[todos]: []
App.js:23 useEffect[todos]: []
So it seems like the state is asynchronously initialised after loading the state for some reason.
Here is my workaround:
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
So I have a workaround, but why is this necessary? I can't wrap my head around how this can be intended React functionality.
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
useEffect is always triggered initially, even though it has dependencies. In your case with the above snippet, it will be triggered twice:
Initial loading (like [] - no dependencies)
Updated todos state
So that's why it set empty data to localStorage because of initial loading without empty todos (if you don't have the condition todos.length != 0)
Your above snippet with the condition todos.length != 0 is reasonable, but it won't work for delete-all cases.
If you don't use any server-side rendering frameworks, you can set a default value for todos state
const [todos, setTodos] = useState(localStorage.getItem(LOCAL_STORAGE_KEY))
With this change, you can update useEffect like below
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
If you use a server-side rendering framework like NextJS, you can try to update localStorage directly on events (toggleTodo and handleAddTodo) instead of useEffect.
function App() {
const [todos, setTodos] = useState([])
const todoNameRef = useRef()
useEffect(() => {
console.log(`useEffect[]`)
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedTodos) {
console.log(`set todos to: ${JSON.stringify(storedTodos)}`)
setTodos(storedTodos)
// can't print here - value is set asynchronously
// console.log(`loaded todos: ${JSON.stringify(todos)}`)
}
}, [])
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
console.log(`toggleTodo: ${todo.name}`)
todo.complete = !todo.complete
setTodos(newTodos)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newTodos))
}
function handleAddTodo(e) {
const name = todoNameRef.current.value
console.log(`handleAddTodo: ${name}`)
// setTodos(todos.concat({completed: false, name: todoNameRef.current.value}))
if (name === '') {
return
}
const updatedTodos = [...todos, { id:uuidv4(), name:name, complete:false }]
setTodos(updatedTodos)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updatedTodos))
}
return (
<div>
<TodoList todos={todos} handleCheckboxChanged={toggleTodo}/>
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed</button>
<div>0 left to do</div>
</div>
)
}
I'm trying to click my element in setInterval loop, so it would be clicked every 10 second, but there's always error click is not a function or cannot read click null
I've tired with useRef and also did nothing.
here is my code:
useEffect(() => {
setInterval(function () {
const handleChangeState = () => {
console.log("Now");
document.getElementById("dice").click();
};
handleChangeState();
}, 10 * 1000);
}, []);
return (
<>
<Dice id="dice" rollingTime="3000" triggers={["click", "P"]} />
</>
);
};
It is often considered anti-pattern in React to query the DOM. You should instead use a React ref to gain access to the underlying DOMNode.
There are a couple ways to use a React ref to invoke a dice roll of the child component. FYI, rollingTime should probably be number type instead of a string if using in any setTimeout calls.
Forward the React ref and attach to the button element and invoke the click handler.
Example:
const Dice = forwardRef(({ id, rollingTime }, ref) => {
const timerRef = useRef();
const [value, setValue] = useState();
const [isRolling, setIsRolling] = useState();
useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
const roll = () => {
if (!isRolling) {
setIsRolling(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setValue(Math.floor(Math.random() * 6) + 1);
setIsRolling(false);
}, rollingTime);
}
};
return (
<>
<h1>Dice</h1>
<h2>Roll Value: {isRolling ? "Rolling..." : value}</h2>
<button ref={ref} id={id} type="button" onClick={roll}>
Roll the dice
</button>
</>
);
});
...
export default function App() {
const diceRef = useRef();
useEffect(() => {
const handleChangeState = () => {
console.log("Clicking Dice");
diceRef.current?.click();
};
setInterval(() => {
handleChangeState();
}, 10 * 1000);
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
Forward the React ref and invoke the button's callback function directly via the useImperativeHandle hook.
Example:
const Dice = forwardRef(({ id, rollingTime }, ref) => {
const timerRef = useRef();
const [value, setValue] = useState();
const [isRolling, setIsRolling] = useState();
useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
const roll = () => {
if (!isRolling) {
setIsRolling(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setValue(Math.floor(Math.random() * 6) + 1);
setIsRolling(false);
}, rollingTime);
}
};
useImperativeHandle(ref, () => ({
roll
}));
return (
<>
<h1>Dice 2</h1>
<h2>Roll Value: {isRolling ? "Rolling..." : value}</h2>
<button id={id} type="button" onClick={roll}>
Roll the dice
</button>
</>
);
});
...
export default function App() {
const diceRef = useRef();
useEffect(() => {
const handleRollDice = () => {
console.log("Roll dice");
diceRef.current.roll();
};
setInterval(() => {
handleRollDice();
}, 10 * 1000);
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
Using react-dice-roll
If you examine the react-dice-roll source code you'll see that the Dice component forwards a React ref and uses the useImperativeHandle hook to expose out a rollDice function.
Dice Source
const Dice = forwardRef((props: TProps, ref: React.MutableRefObject<TDiceRef>) => {
...
const handleDiceRoll = (value?: TValue) => {
let diceAudio: HTMLAudioElement;
if (sound) {
diceAudio = new Audio(sound);
diceAudio.play();
}
setRolling(true);
setTimeout(() => {
let rollValue = Math.floor((Math.random() * 6) + 1) as TValue;
if (value) rollValue = value;
if (cheatValue) rollValue = cheatValue;
setRolling(false);
setValue(rollValue);
if (diceAudio) diceAudio.pause();
if (!onRoll) return;
onRoll(rollValue);
}, rollingTime);
};
useImperativeHandle(ref, () => ({ rollDice: handleDiceRoll }));
...
return (
...
)
});
Your code then just needs to create a React ref and pass it to the Dice component, and instantiate the interval in a mounting useEffect hook.
Example:
function App() {
const diceRef = useRef();
useEffect(() => {
const rollDice = () => {
console.log("Rolling Dice");
diceRef.current.rollDice(); // <-- call rollDice function
};
// instantiate interval
setInterval(() => {
rollDice();
}, 10 * 1000);
// immediately invoke so we don't wait 10 seconds for first roll
rollDice();
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
I want to subtract the value of 'percent' from the function by 0.25.
However, subtraction does not work.
I used setState, but I don't know why it doesn't work.
import React, {useState, useRef, useCallback} from 'react';
const Ques = () => {
const [percent,setPercent] = useState(1);
const intervalRef = useRef(null);
const start = useCallback(() =>{
if (intervalRef.current !== null){
return;
}
intervalRef.current = setInterval(()=>{
if (percent > 0){
setPercent(c => c - 0.25);
console.log("percent = ", percent);
}
else {
setPercent(c => 1);
}
}, 1000);
}, []);
return (
<div>
<button onClick={()=>{start()}}>{"Start"}</button>
</div>
);
}
export default Ques;
Issue
The enqueued state updates are working correctly but you've a stale enclosure over the percent state in the interval callback that you are logging, it never will update.
Solution
If you want to log the percent state then use an useEffect hook to log changes.
const Ques = () => {
const [percent, setPercent] = useState(1);
const intervalRef = useRef(null);
useEffect(() => {
console.log("percent = ", percent); // <-- log state changes here
}, [percent]);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setPercent((c) => Math.max(0, c - 0.25)); // <-- simpler updater function
}, 1000);
}, []);
return (
<div>
Percent: {percent * 100}
<button onClick={start}>Start</button>
</div>
);
};
You can create a ref for percent also and chenge its current value as:
codesandbox link
import React, { useRef, useCallback } from "react";
const Ques = () => {
const percentRef = useRef(1);
const intervalRef = useRef(null);
const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
console.log("percent = ", percentRef.current);
percentRef.current > 0
? (percentRef.current -= 0.25)
: (percentRef.current = 1);
}, 1000);
}, []);
return (
<div>
<button onClick={start}>Start</button>
</div>
);
};
export default Ques;
I think useCallback and useRef is not a good fit. Below is a minimal verifiable example using useState and useEffect. Note this function appropriately performs cleanup on the timer when the component is unmounted. Click Run to run the code snippet and click start to begin running the effect.
function App() {
const [percent, setPercent] = React.useState(1)
const [running, setRunning] = React.useState(false)
React.useEffect(() => {
if (!running) return
const t = window.setTimeout(() => {
setPercent(c => c > 0 ? c - 0.25 : 1)
}, 1000)
return () => window.clearTimeout(t)
}, [running, percent])
return <div>
<button onClick={() => setRunning(true)} children="start" />
<pre>percent: {percent}</pre>
</div>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
This may be one possible solution to achieve what is presumed to be the desired objective:
Code Snippet
const {useState, useRef, useCallback} = React;
const Ques = () => {
const [percent,setPercent] = useState(1);
const intervalRef = useRef(null);
const start = useCallback((flag) => {
if (intervalRef.current !== null){
if (flag && flag === 'end') clearInterval(intervalRef.current);
return;
}
intervalRef.current = setInterval(() => {
setPercent(
prev => (prev > 0 ? prev - 0.25 : 1)
);
}, 1000);
}, []);
return (
<div>
percent: {percent} <br/> <br/>
<button onClick={() => start('bgn')}>Start</button>
<button onClick={() => start('end')}>Stop</button>
</div>
);
}
ReactDOM.render(
<div>
<h3>DEMO</h3>
<Ques />
</div>,
document.getElementById('rd')
);
<div id='rd' />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
Explanation
There are two buttons Start and Stop
Both invoke the same start method, but with different params (flag)
If intervalRef is already set (ie, not null) and flag is end, clear the interval
The percent is added to the UI to see real-time changes to its value
setPercent is modified to use prev (which holds the correct state)
I'm trying to refactor my code to react hooks, but I'm not sure if i'm doing it correctly. I tried copying and pasting my setInterval/setTimout code into hooks, but it did not work as intended. After trying different things I was able to get it to work, but I'm not sure if this is the best way to do it.
I know i can use useEffect to clear interval on un-mount, but I want to clear it before un-mounting.
Is the following good practice and if not what is a better way of clearing setInterval/setTimout before un-mounting?
Thanks,
useTimeout
import { useState, useEffect } from 'react';
let timer = null;
const useTimeout = () => {
const [count, setCount] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useEffect(() => {
if (timerOn) {
console.log("timerOn ", timerOn);
timer = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000);
} else {
console.log("timerOn ", timerOn);
clearInterval(timer);
setCount(0);
}
return () => {
clearInterval(timer);
}
}, [timerOn])
return [count, setCount, setTimerOn];
}
export default useTimeout;
Component
import React from 'react';
import useTimeout from './useTimeout';
const UseStateExample = () => {
const [count, setCount, setTimerOn] = useTimeout()
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
<br />
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
<br />
<button onClick={() => setTimerOn(true)}>Set Interval</button>
<br />
<button onClick={() => setTimerOn(false)}>Stop Interval</button>
<br />
</div>
);
}
export default UseStateExample;
--- added # 2019-02-11 15:58 ---
A good pattern to use setInterval with Hooks API:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
--- origin answer ---
Some issues:
Do not use non-constant variables in the global scope of any modules. If you use two instances of this module in one page, they’ll share those global variables.
There’s no need to clear timer in the “else” branch because if the timerOn change from true to false, the return function will be executed.
A better way in my thoughts:
import { useState, useEffect } from 'react';
export default (handler, interval) => {
const [intervalId, setIntervalId] = useState();
useEffect(() => {
const id = setInterval(handler, interval);
setIntervalId(id);
return () => clearInterval(id);
}, []);
return () => clearInterval(intervalId);
};
Running example here:
https://codesandbox.io/embed/52o442wq8l?codemirror=1
In this example, we add a couple of things...
A on/off switch for the timeout (the 'running' arg) which will completely switch it on or off
A reset function, allowing us to set the timeout back to 0 at any time:
If called while it's running, it'll keep running but return to 0.
If called while it's not running, it'll start it.
const useTimeout = (callback, delay, running = true) => {
// save id in a ref so we make sure we're always clearing the latest timeout
const timeoutId = useRef('');
// save callback as a ref so we can update the timeout callback without resetting it
const savedCallback = useRef();
useEffect(
() => {
savedCallback.current = callback;
},
[callback],
);
// clear the timeout and start a new one, updating the timeoutId ref
const reset = useCallback(
() => {
clearTimeout(timeoutId.current);
const id = setTimeout(savedCallback.current, delay);
timeoutId.current = id;
},
[delay],
);
// keep the timeout dynamic by resetting it whenever its' deps change
useEffect(
() => {
if (running && delay !== null) {
reset();
return () => clearTimeout(timeoutId.current);
}
},
[delay, running, reset],
);
return { reset };
};
So in your example above, we could use it like so...
const UseStateExample = ({delay}) => {
// count logic
const initCount = 0
const [count, setCount] = useState(initCount)
const incrementCount = () => setCount(prev => prev + 1)
const decrementCount = () => setCount(prev => prev - 1)
const resetCount = () => setCount(initCount)
// timer logic
const [timerOn, setTimerOn] = useState(false)
const {reset} = useTimeout(incrementCount, delay, timerOn)
const startTimer = () => setTimerOn(true)
const stopTimer = () => setTimerOn(false)
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={incrementCount}>Increment</button>
<br />
<button onClick={decrementCount}>Decrement</button>
<br />
<button onClick={startTimer}>Set Interval</button>
<br />
<button onClick={stopTimer}>Stop Interval</button>
<br />
<button onClick={reset}>Start Interval Again</button>
<br />
</div>
);
}
Demo of clear many timers.
You should declare and clear timer.current instead of timer.
Declare s and timer.
const [s, setS] = useState(0);
let timer = useRef<NodeJS.Timer>();
Initialize timer in useEffect(() => {}).
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
Clear timer.
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
After many attempts to make a timer work with setInterval, I decided to use setTimeOut, I hope it works for you.
const [count, setCount] = useState(60);
useEffect(() => {
if (count > 0) {
setTimeout(() => {
setCount(count - 1);
}, 1000);
}
}, [count]);