This question already has answers here:
State not updating when using React state hook within setInterval
(14 answers)
Closed 4 years ago.
I'm trying to use new React hooks capabilities, but stumbled a bit.
Fiddle
I have a useEffect which calls setInterval, which updates local state. Like this:
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const k = setInterval(() => {
setCounter(counter + 1);
}, 1000);
return () => clearInterval(k);
}, []);
return (
<div>Counter via state: {counter}<br/></div>
);
It doesn't work right, because counter is captured on first call and so counter is stuck at 1 value.
If i use refs, ref is updated, but rerender is not called (will only see 0 value in UI):
const counterRef = React.useRef(0);
React.useEffect(() => {
const k = setInterval(() => {
counterRef.current += 1;
}, 1000);
return () => clearInterval(k);
}, []);
return (
<div>Counter via ref: {counterRef.current}</div>
);
I can make what i want by combining them, but it really doesn't look right:
const [counter, setCounter] = React.useState(0);
const counterRef = React.useRef(0);
React.useEffect(() => {
const k = setInterval(() => {
setCounter(counterRef.current + 1);
counterRef.current += 1;
}, 1000);
return () => clearInterval(k);
}, []);
return (
<div>Counter via both: {counter}</div>
);
Could you please tell who to handle such cases properly with hooks?
useRef is useful only in cases when component update is not desirable. But the problem with accessing current state in asynchronous useEffect can be fixed with same recipe, i.e. using object reference instead of immutable state:
const [state, setState] = React.useState({ counter: 0 });
React.useEffect(() => {
const k = setInterval(() => {
state.counter++;
setCounter(state);
}, 1000);
return () => clearInterval(k);
}, []);
The use of mutable state is discouraged in React community because it has more downsides than immutable state.
As with setState in class components, state updater function can be used to get current state during state update:
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const k = setInterval(() => {
setCounter(conter => counter + 1);
}, 1000);
return () => clearInterval(k);
}, []);
Alternatively, setTimeout can be used to set new interval every second:
React.useEffect(() => {
const k = setTimeout(() => {
setCounter(counter + 1);
}, 1000);
return () => clearInterval(k);
}, [counter]);
I have found myself in this situation a couple times by now, and my prefered solution is to use useReducer instead of useState, like so:
const [counter, dispatch] = React.useReducer((state = 0, action) => {
// better declared outside of the component
if (action.type === 'add') return state + 1
return state
});
React.useEffect(() => {
const k = setInterval(() => {
dispatch({ type: 'add' });
}, 1000);
return () => clearInterval(k);
}, []);
return (
<div>Counter via state: {counter}<br/></div>
);
Although is adds a bit of boilerplate, it really simplifies the "when is my variable taken in account ?".
More here https://reactjs.org/docs/hooks-reference.html#usereducer
Related
In the following React Component below, I am trying to add increment count by each second passed so it looks like a stopwatch, but the count is shown as 2, then blinks to 3, and back to 2. Does anyone know how to deal with this bug, and get the count to show up as intended?
import React, { useEffect, useState } from "react";
const IntervalHook = () => {
const [count, setCount] = useState(0);
const tick = () => {
setCount(count + 1);
};
useEffect(() => {
const interval = setInterval(tick, 1000);
return () => {
clearInterval(interval);
};
}, [ count ]);
return <h1> {count} </h1>;
};
export default IntervalHook;
if you want to change some state based on its previous value, use a function:setCount(count => count + 1); and your useEffect becomes independant of [ count ]. Like
useEffect(() => {
const tick = () => {
setCount(count => count + 1);
};
const interval = setInterval(tick, 1000);
return () => {
clearInterval(interval);
};
}, [setCount]);
or you get rid of tick() and write.
useEffect(() => {
const interval = setInterval(setCount, 1000, count => count + 1);
return () => {
clearInterval(interval);
};
}, [setCount]);
But imo it's cleaner to use a reducer:
const [count, increment] = useReducer(count => count + 1, 0);
useEffect(() => {
const interval = setInterval(increment, 1000);
return () => {
clearInterval(interval);
};
}, [increment]);
am writing an app in react native and i got a problem with useState and useEffect hooks. I would like to change increment state value by one every 10 seconds.
Here is my code:
const [tripDistance, setTripDistance] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTripDistance((prevState) => prevState + 1);
console.log(tripDistance);
}, 10000);
return () => {
clearInterval(interval);
};
}, []);
but the output from the console.log is always 0.
What am I doing wrong?
The output is always zero because in your useEffect you are not listening for the changes on tripDistance state. When you call setTripDistance you cannot access the updated value immediately.
You should add another useEffect that listen on tripDistance in order to have the correct console.log.
So you have to do something like:
const [tripDistance, setTripDistance] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTripDistance((prevState) => prevState + 1);
}, 10000);
return () => {
clearInterval(interval);
};
}, []);
useEffect(() => console.log(tripDistance), [tripDistance]);
try this
const [tripDistance, setTripDistance] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTripDistance((prevState) => prevState + 1);
}, 10000);
return () => {
clearInterval(interval);
};
}, [tripDistance]);
That's the warning in the console,
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Here is my code
const [index, setIndex] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refContainer: any = useRef();
const [selectedIndex, setSelectedIndex] = useState(0);
const navigation = useNavigation();
useEffect(() => {
refContainer.current.scrollToIndex({animated: true, index});
}, [index]);
const theNext = (index: number) => {
if (index < departments.length - 1) {
setIndex(index + 1);
setSelectedIndex(index + 1);
}
};
setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
const onRefresh = () => {
if (refreshing === false) {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}
};
What should I do to make clean up?
I tried to do many things but the warning doesn't disappear
setTimeout need to use in useEffect instead. And add clear timeout in return
useEffect(() => {
const timeOut = setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
return () => {
if (timeOut) {
clearTimeout(timeOut);
}
};
}, []);
Here is a simple solution. first of all, you have to remove all the timers like this.
useEffect(() => {
return () => remover timers here ;
},[])
and put this
import React, { useEffect,useRef, useState } from 'react'
const Example = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
isScreenMounted.current = true
return () => isScreenMounted.current = false
},[])
const somefunction = () => {
// put this statement before every state update and you will never get that earrning
if(!isScreenMounted.current) return;
/// put here state update function
}
return null
}
export default Example;
I have this code which updates the state count every 1 seconds.
How can I access the value of the state object in setInterval() ?
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
export default function Timer({objectId}) {
const [object, setObject] = useState({increment: 1});
const [count, setCount] = useState(0);
useEffect(() => {
callAPI(); // update state.object.increment
const timer = setInterval(() => {
setCount(count => count + object.increment); // update state.count with state.object.increment
}, 1000);
return () => clearTimeout(timer); // Help to eliminate the potential of stacking timeouts and causing an error
}, [objectId]); // ensure this calls only once the API
const callAPI = async () => {
return await axios
.get(`/get-object/${objectId}`)
.then(response => {
setObject(response.data);
})
};
return (
<div>{count}</div>
)
}
The only solution I found is this :
// Only this seems to work
const timer = setInterval(() => {
let increment = null;
setObject(object => { increment=object.increment; return object;}); // huge hack to get the value of the 2nd state
setCount(count => count + increment);
}, 1000);
In your interval you have closures on object.increment, you should use useRef instead:
const objectRef = useRef({ increment: 1 });
useEffect(() => {
const callAPI = async () => {
return await axios.get(`/get-object/${objectId}`).then((response) => {
objectRef.current.increment = response.data;
});
};
callAPI();
const timer = setInterval(() => {
setCount((count) => count + objectRef.current);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [objectId]);
I am trying to create a reusable hook that solves the problem of the stale closure problem that is outlined in this blog post.
Here is a codesandbox that shows the problem of the stale closure in action:
const useInterval = (callback, delay) => {
useEffect(() => {
let id = setInterval(() => {
callback();
}, 1000);
return () => clearInterval(id);
}, []);
};
const App: React.FC = () => {
let [count, setCount] = useState(0);
useInterval(() => setCount(count + 1), 1000);
return <h1>{count}</h1>;
};
Basically count is frozen at 0 when the closure is created meaning 0 is added to 1 continually in the setInterval.
The blog post solves this by introducing a mutable ref to store the callback in:
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
The useEffect never gets re-executed because the callback is not in the useEffect dependency array with the setInteval.
I've seen some libraries using a hook like the useStoreCallback below but the linter still complains that the savedCallback variable below needs to be added to the dependency array.
Is this actually better?
type UnknownResult = unknown;
type UnknownArgs = any[];
function useStoreCallback<R = UnknownResult, Args extends any[] = UnknownArgs>(
fn: (...args: Args) => R
) {
const ref = React.useRef(fn);
useEffect(() => {
ref.current = fn;
});
return React.useCallback<typeof fn>(
(...args) => ref.current.apply(void 0, args),
[]
);
}
function useInterval<R = UnknownResult, Args extends any[] = UnknownArgs>(
callback: (...args: UnknownArgs) => R,
delay: number
) {
const savedCallback = useStoreCallback(callback);
useEffect(() => {
function tick() {
savedCallback();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay, savedCallback]);
}
const App: React.FC = () => {
let [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
};
Use the functional setState and rid off the closure.
const App: React.FC = () => {
const [count, setCount] = useState(0);
useInterval(() => setCount(c => c + 1), 1000);
return <h1>{count}</h1>;
};