If Animated.Value is used for a natively driven animation and then the component is unmounted, when the component is mounted back, the rendered value resets to the initial state.
0.61.5
Animated.Value should hold the last value it had before the component was unmounted, and the component should return to the same state on being re-mounted as a result.
https://github.com/djonin/react-native-animated-value-bug
This is a duplicate of issues #23712 and #23621, which were incorrectly closed as fixed, due to using a listener in the example, which avoids this defect. The stated fix #24571 only fixed the problem with the listener not correctly re-binding to the new value, however the broader case issue mentioned in #23621 is still there.
This issue is due to the fact the native node, which has the up to date value, is destroyed after a component is dismounted. The JS animated value object doesn't receive any native value updates unless there is a listener attached explicitly by the user. Therefore the next time the component is mounted, it uses the value as it was before the native animation.
Currently the only way to preserve the value is to attach a listener, meaning the value updates will be sent over the bridge often during animations. In addition, it's not obvious to users that attaching a listener is required for correct re-mounting behavior.
One possible fix would be to make a new event to call back to JS with the value just once when the native node is being detached/destroyed, so that we can preserve whatever the final native value was in our JS object, and for the JS animated value object to always listen to this new native event, even with no listeners attached explicitly. We need some place to store our value, and since the native side is quite correctly being cleaned up after unmount, JS side seems like the best place to do it.
Hi, At first let me explain the problem in details then I'll suggest possible solutions.
All explanations and possible solutions are assuming animations using
useNativeDriver = true
export default class App extends React.Component {
x = new Animated.Value(0);
render() {
return <Animated.View style={transform: {translate: this.x}}/>
}
}
export default class App extends React.Component {
state = {show: true}
x = new Animated.Value(0);
render() {
return (
<>
{show && <Animated.View style={transform: {translate: this.x}}/>}
<Button onPress={() => this.setState(prevState => ({show: !prevState }))}>Toggle</Button>
</>
}
}
Example contains simple React component that renders Animated.View. Here we are building tree from Animated nodes on JS side as well as on a native side. After componentDidMount has been invoked AnimatedProps node connects with corresponding AnimatedView on a native side. Nothing really interesting yet though It will change when we try to unmount our component :). It starts from detaching whole Animated nodes tree on both sides(JS, native) but it will detach node itself only when there are no children(so all children have detached itself earlier). On native side we don't have any kind of cache because all information are contained in the tree so when node was detached we lost all information.
export default class App extends React.Component {
state = {show: true}
x = new Animated.Value(0);
render() {
return (
<>
{show && <Animated.View style={{ transform: [{ translateX: this.x }] }}/>}
<Animated.View style={{ transform: [{ translateX: this.x }] }}/>
<Button onPress={() => this.setState(prevState => ({show: !prevState }))}>Toggle</Button>
</>
}
}
Here we are rendering Animated.View twice !
Your fix works because our Animated.Value x has always at least one child therefore in detaching phase we aren't detached Animated.Value itself so it will still be alive on the native side so it'll be properly calculated. It works because our x value has the same identity so as nativeTag. That's why when we mount our Animated.View again it will be connected with proper Animated.Value nativeTag which on the native side has value changed by animation.
Save last value when animation has finished but it is required that every native driver with callback has to be invoked with additional argument which is last animation value and then we save value on JS side though It wouldn't work when animated component was unmounted during animation.
We have to invoke newly created function from native side i.e getValue in nodes managers,(which just takes current value from given AnimatedValue node), when we are detaching node and then we save returned value as a internal value of Animated.Value node on JS side. Something similar is has been already implemented in reanimated.
From the way the components are written, it seems the intention was for the JS node to hold the correct value while it is not attached to any native nodes. I definitely agree with your second suggestion, I had a look through reanimated, and calling getValue explicitly is a good fix as well. I had in mind a listener similar to onAnimatedValue , however provided there are no situations where the native value gets lost without going through JS __detach, asking for the value there directly might be better.
cc @JoshuaGross Can you take a look at our proposed solution #28841? We could really use any feedback. Thanks
Most helpful comment
Hi, At first let me explain the problem in details then I'll suggest possible solutions.
All explanations and possible solutions are assuming animations using
Explanation in details
Example without unmounting:
Example with unmounting:
Example contains simple React component that renders
Animated.View. Here we are building tree fromAnimatednodes on JS side as well as on a native side. After componentDidMount has been invoked AnimatedProps node connects with correspondingAnimatedViewon a native side. Nothing really interesting yet though It will change when we try to unmount our component :). It starts from detaching wholeAnimatednodes tree on both sides(JS, native) but it will detach node itself only when there are no children(so all children have detached itself earlier). On native side we don't have any kind of cache because all information are contained in the tree so when node was detached we lost all information.Example with your "fix"
Here we are rendering Animated.View twice !
Why your "fix" works
Your fix works because our Animated.Value
xhas always at least one child therefore in detaching phase we aren't detached Animated.Value itself so it will still be alive on the native side so it'll be properly calculated. It works because ourxvalue has the same identity so as nativeTag. That's why when we mount ourAnimated.Viewagain it will be connected with properAnimated.ValuenativeTag which on the native side has value changed by animation.Possible solutions
Save last value when animation has finished but it is required that every native driver with callback has to be invoked with additional argument which is last animation value and then we save value on JS side though It wouldn't work when animated component was unmounted during animation.
We have to invoke newly created function from native side i.e
getValuein nodes managers,(which just takes current value from given AnimatedValue node), when we are detaching node and then we save returned value as a internal value of Animated.Value node on JS side. Something similar is has been already implemented inreanimated.