React: React hooks + old way components = cached state value (😱😱😱)

Created on 30 Oct 2018  Β·  11Comments  Β·  Source: facebook/react

Please see this demo.
Edit React Hooks Problem
As you can see, I have an old way component looks like this

import React, { Component } from "react";

class OldLibrary extends Component {
  componentDidMount() {
    setInterval(this.props.onProgress, 50);
  }
  render() {
    return null;
  }
}
export default OldLibrary;

A simple component setting interval when mount.

Now I have a component use React Hooks

function App() {
  const [value, setValue] = useState(0);
  const onProgress = () => {
    console.log(value);
    // this will always console logging 0 no matter what
  };
  const onClick = () => {
    setValue(value + 1);
    // Click me and state changes, but onProgress won't notice
  };
  return (
    <>
      <OldLibrary onProgress={onProgress} />
      <button onClick={onClick}>Click</button>
    </>
  );
}

This code above, no matter how I click the button, onProgress will always console logging initial value(which is 0), it's just like the state is being cached or something I don't know.

useRef won't help too.

function App() {
  const [value, setValue] = useState(0);
  const onProgress = useRef(() => {
    console.log(value);
  });
  const onClick = () => {
    setValue(value + 1);
  };
  return (
    <>
      <OldLibrary onProgress={onProgress.current} />
      <button onClick={onClick}>Click</button>
    </>
  );
}

But if we use the old way, it will work just fine.

class App extends Component {
  state = {
    value: 0
  };
  onClick = () => {
    this.setState({ value: this.state.value + 1 });
  };
  onProgress = () => {
    console.log(this.state.value);
    // I will console logging different value when state changes
  };
  render() {
    return (
      <>
        <OldLibrary onProgress={this.onProgress} />
        <button onClick={this.onClick}>Click</button>
      </>
    );
  }
}

Thank you for listening my question.
Please help, I can't live without hooks 😭😭😭

Most helpful comment

@wojtekmaj's explanation is correct – the onProgress prop gets updated to a new function but it is the old function that was passed to setInterval. In your class component example, onProgress always reads the latest this.state which is a mutable field.

If you aren't able to change OldLibrary, your best approach is to store the value in a ref as described in our FAQ: https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback.

function App() {
  const [value, setValue] = useState(0);
  const onClick = () => {
    setValue(value + 1);
  };

  const valueRef = useRef();
  useEffect(() => {
    valueRef.current = value;
  });
  const onProgress = () => {
    console.log(valueRef.current);
  };

  return (
    <>
      <OldLibrary onProgress={onProgress} />
      <button onClick={onClick}>Click</button>
    </>
  );
}

Note that this is one of the "sharp edges" of Hooks right now and we might introduce a better way to deal with this before the final release.

All 11 comments

With the first render of App, you created onProgress function that returns value (0). You are then invoking onProgress function and it returns 0.

You then increment the value of value, so that it's now 1. Within App, now there is value with value of 1, and a new onProgress function which would return 1 if called.

You continue to call the FIRST onProgress function in setInterval, but you ignore the fact that now OldLibrary have a SECOND onProgress function passed by props, which returns incremented value of value.

What you can do is wrap your setInterval so it would not call the first given this.props.onProgress directly, but an internal method which gets the newest onProgress function from props and calls it.

import React, { Component } from "react";

class OldLibrary extends Component {
  onProgress = () => {
    this.props.onProgress();
  }

  componentDidMount() {
    setInterval(this.onProgress, 500);
  }
  render() {
    return null;
  }
}
export default OldLibrary;

@wojtekmaj Thank you for your solution!
I think I will have to modify some old code.
I want to ask one more question, is there any way to fix this without touching old component?
I know the best solution is to rewrite some old code, but this question is just my curious, thank you.

I gave it much thought and I only came up with a hacky solution to set the value outside the function. I wouldn't recommend this workaround.

let valueOutside;
function App() {
  const [value, setValue] = useState(0);
  // Don't do this
  valueOutside = value;
  const onProgress = () => {
    console.log(value, valueOutside);
  };
  const onClick = () => {
    setValue(value + 1);
  };
  console.log(value);
  return (
    <>
      <OldLibrary onProgress={onProgress} />
      <button onClick={onClick}>Click</button>
    </>
  );
}

@wojtekmaj Wow, thank you for your solution!
It seems like there is no other way without hacky solutions.
But thank you again!

It might works
<OldLibrary key={value} onProgress={onProgress} />
I hope your OldLibrary would clearInterval when unmount

Edit React Hooks Problem

@dislido Yes, that would also work, but my OldLibrary is actually a video (react-player), unmounting it will restart playing the video, so sad.

@wojtekmaj's explanation is correct – the onProgress prop gets updated to a new function but it is the old function that was passed to setInterval. In your class component example, onProgress always reads the latest this.state which is a mutable field.

If you aren't able to change OldLibrary, your best approach is to store the value in a ref as described in our FAQ: https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback.

function App() {
  const [value, setValue] = useState(0);
  const onClick = () => {
    setValue(value + 1);
  };

  const valueRef = useRef();
  useEffect(() => {
    valueRef.current = value;
  });
  const onProgress = () => {
    console.log(valueRef.current);
  };

  return (
    <>
      <OldLibrary onProgress={onProgress} />
      <button onClick={onClick}>Click</button>
    </>
  );
}

Note that this is one of the "sharp edges" of Hooks right now and we might introduce a better way to deal with this before the final release.

@sophiebits Thank you!!!

Sorry to dig up this old issue, but I was also having this issue and wanted to make sure I understand properly! So in this example, even though onProgress will be recreated with each render of App, and even if OldLibrary isn't doing any memoizing of its props, on subsequent renders of App, where value has changed, the onProgress prop/callback within OldLibrary will still be closing over the old value?

function App() {
  const [value, setValue] = useState(0);
  const onClick = () => {
    setValue(value + 1);
  };

  const onProgress = () => {
    console.log(value);
  };

  return (
    <>
      <OldLibrary onProgress={onProgress} />
      <button onClick={onClick}>Click</button>
    </>
  );
}

Btw your solution to use a ref totally works, so thank you for that @sophiebits! I just want to also make sure I understand the underlying issue as well, so I can avoid it in the future.

No, your example should work fine unless OldLibrary ignores updates to the onProgress prop. That’s something the library should fix because it means it would also be buggy with classes when you do onProgress={this.state.something ? this.method1 : this.method2}.

Ah that's awesome! So it's only because of the setInterval holding on to the closure this time, got it. Yeah I was surprised if that was the behavior haha, good to know.

I actually found the issue for my problem was with my lower level library, as you are describing @gaearon. It was in react-contenteditable, in case it comes up again; it was cancelling updates to the component just as you suspected. I really appreciate the help, thank you! πŸ™

Was this page helpful?
0 / 5 - 0 ratings