1

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?

3
  • 1
    ticker.addListener() does not return an unsubscribe function. Commented Mar 26, 2022 at 19:39
  • Did you expect ticker.addListener(runEachSecond) to be called again when the changingValue prop changes? Commented Mar 26, 2022 at 19:39
  • @Bergi yes the unsubscribe code is missing. And as for the second question, No just want it to be subscribed after mounting, and unsubscribed when unmounting. Commented Mar 26, 2022 at 19:42

2 Answers 2

2

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>
  )
}
Sign up to request clarification or add additional context in comments.

Comments

1

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 update
  • runEachSecond is created again (instance #2)
  • Ticker still hold a reference of runEachSecond #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 update
  • runEachSecond is created again (instance #2)
  • Ticker unsubscribe from runEachSecond #1
  • Ticker subscribe again, but to runEachSecond #2

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.