Background of the problem
I have a simple "Ticker" class that can hold a single callback, and execute that callback each ~1s via setInterval
The code is as follows.
class Ticker{
listner = null;
constructor(){
setInterval(this.excuteCallbackInterval, 1000)
}
addListner(callback){
this.listner = callback
}
excuteCallbackInterval = () => {
if(this.listner){
this.listner();
}else {
console.log("no listner registered");
}
}
}
I have a React Functional Component
in a nother file, that instantiates a Ticker Object, at class level(I.E outside the functional component) but inside the same file. Here's the code for that component. (some parts are removed to make it concise)
.
.
.
const ticker = new Ticker()
// react component.
function TimerDuration({ changingValue }) {
const unsubscribe = useRef(null)
function runEachSecond() {
console.log('inside runEachSecond', changingValue)
}
useEffect(() => {
unsubscribe.current = ticker.addListener(runEachSecond)
// Cleanup
return () => {
try {
unsubscribe.current()
} catch (err) {
// Do nothing
}
}
}, [])
return (
<Text>
{changingValue}
</Text>
)
}
Problem
My problem is that when the timer TimerDuration
renders initially changingValue
painted on the screen and changingValue
inside excuteCallbackInterval
is the same,
But when the changingValue
prop is updated, that updated value is not reflected inside excuteCallbackInterval
but the change is reflected in the value painted on the screen.
Solution.
My solution was (in a way by instinct) was to store the value of changingValue
in a ref and update it inside a second useEffect
that runs each time. Might not be the ideal one hence this question.
Question
My question is why am I seeing this behavior? runEachSecond
is evaluated with each prop update. And it's able to access changingValue
because it's in its parent scope. excuteCallbackInterval
function within the Ticker
also has access to changingValue
because of the closure. But why does the update not reflect within excuteCallbackInterval
? Also is there a better way to solve this issue?
CodePudding user response:
You want to unsubscribe and subscribe again every time changingValue
change. Or, said in another way: you want to update the ticker
's callback to prevent it from going stale.
useEffect(() => {
function runEachSecond() {
console.log('inside runEachSecond', changingValue)
}
const unsubscribe = ticker.addListener(runEachSecond)
// Cleanup
return () => {
try {
unsubscribe()
} catch (err) {
// Do nothing
}
}
}, [changingValue]) // see changingValue in the deps
Why do you need to do this? Right now, your component does something like this:
- mount
runEachSecond
is created (instance #1)- subscribe to Ticker, pass
runEachSecond
#1 changingValue
prop get updaterunEachSecond
is created again (instance #2)Ticker
still hold a reference ofrunEachSecond
#1
After adding changingValue
in the deps of the useEffect:
- mount
runEachSecond
is created (instance #1)- subscribe to Ticker, pass
runEachSecond
#1 changingValue
prop get updaterunEachSecond
is created again (instance #2)Ticker
unsubscribe fromrunEachSecond
#1Ticker
subscribe again, but torunEachSecond
#2
CodePudding user response:
My question is why am I seeing this behavior?
runEachSecond
is evaluated with each prop update.
No, it's not updated. In React, all variables stay constant. A new runEachSecond
function is created every time the TimerDuration
function component is rendered, and that new function would close over the new changingValue
parameter.
However, runEachSecond
is only used in the effect (in ticker.addListener(runEachSecond)
), which runs only once (when the component is mounted). The ticker
will store only the first of the many runEachSecond
functions, and it will never change (addListener
is not called again), and that first function still closes over the first changingValue
.
I just want it to be subscribed after mounting, and unsubscribed when unmounting.
In that case, you should store the changingValue
in the ref, and then refer to that ref (and its future values) from the ticker listener function. The ref for the unsubscribe
function returned by addListener
is unnecessary.
function TimerDuration({ changingValue }) {
const value = useRef(null);
value.current = changingValue;
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
useEffect(() => {
const unsubscribe = ticker.addListener(function runEachSecond() {
console.log('inside runEachSecond', value.current);
// ^^^^^^^^^^^^^
});
// Cleanup
return () => {
try {
unsubscribe();
} catch (err) {
// Do nothing
}
}
}, [])
return (
<Text>
{changingValue}
</Text>
)
}