React hooks: accessing up-to-date state from within a callback

后端 未结 8 1713
梦谈多话
梦谈多话 2020-12-25 09:35

EDIT (22 June 2020): as this question has some renewed interest, I realise there may be a few points of confusion. So I would like to highlight: the example in the question

相关标签:
8条回答
  • 2020-12-25 10:28

    The only method i know is to call the setState(current_value => ...) function and use the current_value in ur logic. Just make sure you return it back. Ex:

    const myPollingFunction = () => {
        setInterval(() => {
            setState(latest_value => {
                // do something with latest_value
                return latest_value;    
            }
        }, 1000);
    };
    
    0 讨论(0)
  • 2020-12-25 10:39

    I encountered a similar bug trying to do exactly the same thing you're doing in your example - using a setInterval on a callback that references props or state from a React component.

    Hopefully I can add to the good answers already here by coming at the problem from a slightly different direction - the realisation that it's not even a React problem, but a plain old Javascript problem.

    I think what catches one out here is thinking in terms of the React hooks model, where the state variable, just a local variable after all, can be treated as though it's stateful within the context of the React component. You can be sure that at runtime, the value of the variable will always be whatever React is holding under the hood for that particular piece of state.

    However, as soon as you break out of the React component context - using the variable in a function inside a setInterval for instance, the abstraction breaks and you're back to the truth that that state variable really is just a local variable holding a value.

    The abstraction allows you to write code as if the value at runtime will always reflect what's in state. In the context of React, this is the case, because what happens is whenever you set the state the entire function runs again and the value of the variable is set by React to whatever the updated state value is. Inside the callback, however, no such thing happens - that variable doesn't magically update to reflect the underlying React state value at call time. It just is what it is when the callback was defined (in this case 0), and never changes.

    Here's where we get to the solution: if the value pointed to by that local variable is in fact a reference to a mutable object, then things change. The value (which is the reference) remains constant on the stack, but the mutable value(s) referenced by it on the heap can be changed.

    This is why the technique in the accepted answer works - a React ref provides exactly such a reference to a mutable object. But I think it's really important to emphasise that the 'React' part of this is just a coincidence. The solution, like the problem, has nothing to do with React per-se, it's just that a React ref happens to be one way to get a reference to a mutable object.

    You can also use, for instance, a plain Javascript class, holding its reference in React state. To be clear, I'm not suggesting this is a better solution or even advisable (it probably isn't!) but just using it to illustrate the point that there is no 'React' aspect to this solution - it's just Javascript:

    class Count {
      constructor (val) { this.val = val }
      get () { return this.val }
      
      update (val) {
        this.val += val
        return this
      }
    }
    
    function Card(title) {
      const [count, setCount] = React.useState(new Count(0))
      const [callbackSetup, setCallbackSetup] = React.useState(false)
      
      function setupConsoleCallback(callback) {
        console.log("Setting up callback")
        setInterval(callback, 3000)
      }
    
      function clickHandler() {
        setCount(count.update(1));
        if (!callbackSetup) {
          setupConsoleCallback(() => {console.log(`Count is: ${count.get()}`)})
          setCallbackSetup(true)
        }
      }
      
      
      return (
        <div>
          Active count {count.get()} <br/>
          <button onClick={clickHandler}>Increment</button>
        </div>
      )
    }
    
    const el = document.querySelector("#root");
    ReactDOM.render(<Card title='Example Component' />, el);
    

    You can see there that simply by having the state point to a reference, that doesn't change, and mutating the underlying values that the reference points to, you get the behaviour you're after both in the setInterval closure and in the React component.

    Again, this is not idiomatic React, but just illustrates the point about references being the ultimate issue here. Hope it's helpful!

    0 讨论(0)
提交回复
热议问题