Please see this demo.
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 πππ
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!
@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! π
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.
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.