React-Native Animated does not work near setState - react-native

let animatedHeight = new Animated.Value(50);
const animate = () => {
animatedHeight.setValue(50);
Animated.timing(animatedHeight, {
toValue: 0,
duration: 200,
useNativeDriver: true
}).start();
};
const handleSubmit = async (values:ILoginProps) => {
setLoading(true);
axios.post('http://127.0.0.1:3333/api/v1/auth/login', values)
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
animate();
setLoading(false);
})
}
<Animated.View style={[style.errorContainer, {transform: [{translateY: animatedHeight}]}]}>
<Text style={style.errorText}>Credentials not found!</Text>
</Animated.View>
I have this code, first I set a loading state and then execute a animate function which runs the Animated function to create the animation effect.
When i have setLoading(true) before animate(), the animation doesn't happen.
I really have no idea why this happens and no idea how to solve this

Use useRef hook to wrap your animated value. React will keep tracking of its value after re-rendering, in other case you might lose it.
let animatedHeight = React.useRef(new Animated.Value(50)).current;

Related

React native - stopping an animation

I'm trying to stop the animation once the data has loaded. For the sake of testing, I'm using a timer to simulate state change (data has loaded) to interrupt animation. The problem is that the animation keeps running after the state has changed.
const [isLoading, setIsLoading] = useState(false);
const animationRef = useRef(new Animated.Value(0)).current;
const loadData = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 3000)
}
useEffect(() => {
const rotateElement = () => {
animationRef.setValue(0);
Animated.timing(animationRef, {
toValue: 1,
duration: 1500,
easing: Easing.linear,
useNativeDriver: true,
}).start(rotateElement);
};
if (isLoading) {
rotateElement();
} else {
Animated.timing(animationRef).stop();
}
}, [isLoading, animationRef]);
const spin = animationRef.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
Snack:
https://snack.expo.dev/#wastelandtime/timer
Only try to remove the parameter rotateElement from start.
const rotateElement = () => {
animationRef.setValue(0);
Animated.timing(animationRef, {
toValue: 1,
duration: 1500,
easing: Easing.linear,
useNativeDriver: true,
}).start();
You could set a really long duration, ideally as long as your configured timeout. Then, you wouldn't need to call the start function passing the rotateElement as callback as Ivan suggested. That way, when you call the stop method, it will work as desired.
Another option would be using the react-native-reanimated package for writing this function in a more declarative way, making sure that the animation would run as long as you need it to.
I edited your snack, implementing the reanimated alternative: https://snack.expo.dev/#scalfs/timer
On seeing the code sample, simply using Animated.loop (with the required iterations) should get this working well enough. So the useEffect in the codeblock would be like so,
useEffect(() => {
const rotateElement = () => {
// animationRef.setValue(0); This isn't necessary anymore.
Animated.loop(
Animated.timing(animationRef, {
toValue: 1,
duration: 1500,
easing: Easing.linear,
useNativeDriver: true,
}),
{
iterations: 2,
}
).start(rotateElement);
};
if (isLoading) {
rotateElement();
} else {
Animated.timing(animationRef).stop();
}
}, [isLoading, animationRef]);
As for your use case, the iterations could be set to a high enough number and just use the if statement to stop the loop when loading is complete as you have already done here. You could also add a check for errors as well, so the loop doesn't continue ad infinitum.
You are creating a new instant of rotateElement each time your state changes. The animation you attempt to stop isn't the one you think.
const loadData = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 1000)
}
const rotateElement = Animated.timing(animationRef, {
toValue: 1,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true,
});
useEffect(() => {
if (isLoading) {
rotateElement.start();
} else {
rotateElement.stop();
}
}, [isLoading]);
Best creating the rotateElement outside the useEffect as shown above.
I've also changed the timing of the animation because the demo was too short to simulate end of loading correctly.

useState function seems to block Animated.timing event?

I've created a "twitter style" button that when pressed opens up a sub-menu of items that can be selected/"tweeted" about.
The button is simple in that when pressed, it triggers a function with Animated events:
const toggleOpen = () => {
if (this._open) {
Animated.timing(animState.animation, {
toValue: 0,
duration: 300,
}).start();
} else {
Animated.timing(animState.animation, {
toValue: 1,
duration: 300,
}).start(); // putting '() => setFirstInteraction(true)' here causes RenderItems to disappear after the animation duration, until next onPress event.
}
this._open = !this._open;
};
and here's the button that calls this function:
<TouchableWithoutFeedback
onPress={() => {
toggleOpen();
// setFirstInteraction(true); // this works here, but the button doesn't toggleOpen until the 3rd + attempt.
}}>
<Animated.View style={[
styles.button,
styles.buttonActiveBg,
]}>
<Image
style={styles.icon}
source={require('./assets/snack-icon.png')}
/>
</Animated.View>
</TouchableWithoutFeedback>
I need to add a second useState function that is called at the same time as toggleOpen();. You can see my notes above regarding the problems I'm facing when using the setFirstInteraction(true) useState function I'm referring to.
Logically this should work, but for some reason when I add the setFirstInteraction(true) it seems to block the toggleOpen() function. If you persist and press the button a few times, eventually the toggleOpen() will work exactly as expected. My question is, why does this blocking type of action happen?
You can reproduce the issue in my snack: https://snack.expo.dev/#dazzerr/topicactionbutton-demo . Please use a device. The web preview presents no issues, but on both iOS and Android the issue is present. Line 191 is where you'll see the setFirstInteraction(true) instance.
Your animatedValue isn't stable. This causes it to be recreated on each state change. It is advised to useRef instead (though, useMemo would do the trick here as well).
const animState = useRef(new Animated.Value(0)).current;
Your toggleOpen function can also be simplified. In fact, you only need a single state to handle what you want and react on it in a useEffect to trigger the animations that you have implemented.
I have called this state isOpen and I have removed all other states. The toggleOpen function just toggles this state.
const [isOpen, setIsOpen] = useState(false)
const toggleOpen = () => {
setIsOpen(prev => !prev)
}
In the useEffect we react on state changes and trigger the correct animations.
const animState = useRef(new Animated.Value(0)).current;
useEffect(() => {
Axios.get('https://www.getfretwise.com/wp-json/buddyboss/v1/forums')
.then(({ data }) => setData(data))
.catch((error) => console.error(error));
}, []);
useEffect(() => {
Animated.timing(animState, {
toValue: isOpen ? 1 : 0,
duration: 300,
useNativeDriver: true,
}).start();
}, [isOpen, animState])
I have adapted your snack. Here is a working version.
Remarks: Of course, you still need for your data to be fetched from your API. The opacity change of the button is still the same and it remains disabled until the data has been fetched.

Why is SectionList ref.current always undefined?

I am trying to use the scrollToOffset and scrollToIndex that SectionList provides. However, the SectionList ref I am using is always undefined when using ref.current.
In my class component I have the following:
private sectionListRef: any;
constructor(props: SampleScreenProps) {
super(props);
this.sectionListRef = null;
}
componentDidMount = (): void => {
setTimeout(() => this.scrollToSection(), 2000);
};
scrollToSection = (): void => {
// do some calculations to get the itemIndex and sectionIndex to scroll to
this.sectionListRef.scrollToLocation({
animated: false,
itemIndex,
sectionIndex,
viewPosition: 0.5
}); // this fails if the list is huge and the indices are far from the initial render location, and so moves on to onScrollToIndexFailed
};
onScrollToIndexFailed = (error) => {
// this.sectionListRef.current is always undefined in this function
this.sectionListRef.current?.scrollToOffset({
offset: error.averageItemLength * error.index,
animated: true
});
setTimeout(() => {
if (this.state.dataSet.length !== 0 && this.sectionListRef !== null) {
this.sectionListRef.current?.scrollToIndex({ index: error.index, animated: true });
}
}, 10);
};
finally, here's the SectionList itself:
<SectionList
inverted
ref={(ref) => {
this.sectionListRef = ref;
}}
sections={mappedSections}
renderItem={this.renderItem}
renderSectionFooter={this.renderSectionFooter}
keyExtractor={(item, index) => index.toString()}
keyboardShouldPersistTaps="never"
keyboardDismissMode="on-drag"
bounces={false}
initialNumToRender={20}
onScrollToIndexFailed={this.onScrollToIndexFailed}
/>
What is the cause of this?
Edit: I tried doing this.sectionListRef = React.createRef() in the constructor instead, but same result.
I understand scrollToOffset is part of SectionList, but the other two scroll functions are part of the underlying virtualized list, but it should still work the same as far as I know.

React Native ScrollView scrollTo spring animation

I am using React Native's ScrollView's scrollTo method to animate the scroll view. How can I make this animation spring loaded, as in Animated.spring (without the use of a 3rd party library)?
Track the scroll position
<ScrollView
ref={(ref) => { this.scrollView = ref; }}
onScroll={(event) => {
this.scrollY = event.nativeEvent.contentOffset.y;
}}
scrollEventThrottle={16}
>
And then
scrollTo(y) {
if (!this.scrollY) this.scrollY = 0;
const animatedValue = new Animated.Value(this.scrollY);
const id = animatedValue.addListener(({ value }) => {
this.scrollView.scrollTo({ x: 0, y: value, animated: false });
});
Animated.spring(animatedValue, { toValue: y }).start(() => { animatedValue.removeListener(id); /* finished callback */ });
}

How do we set speed to scrollTo() in React Native?

I'm scroll the page on click of a button using:
this.scrollTo({y: height, x: 0, animated: true})
The scroll works fine, however I'd like to slow down the scroll animation.
How do we do that?
This is a pretty neat solution that uses the scrollview's content height to scroll an entire view (on mount). However, the same trick can be used (add a listener to an animated value) to create a scroll function that can be triggered by some event at any given moment (to any given value).
import { useEffect, useRef, useState } from 'react'
import { Animated, Easing, ScrollView } from 'react-native'
const SlowAutoScroller = ({ children }) => {
const scrollRef = useRef()
const scrollAnimation = useRef(new Animated.Value(0))
const [contentHeight, setContentHeight] = useState(0)
useEffect(() => {
scrollAnimation.current.addListener((animation) => {
scrollRef.current &&
scrollRef.current.scrollTo({
y: animation.value,
animated: false,
})
})
if (contentHeight) {
Animated.timing(scrollAnimation.current, {
toValue: contentHeight,
duration: contentHeight * 100,
useNativeDriver: true,
easing: Easing.linear,
}).start()
}
return () => scrollAnimation.current.removeAllListeners()
}, [contentHeight])
return (
<Animated.ScrollView
ref={scrollRef}
onContentSizeChange={(width, height) => {
setContentHeight(height)
}}
onScrollBeginDrag={() => scrollAnimation.current.stopAnimation()}
>
{children}
</Animated.ScrollView>
)
}
On android you can use the smoothScrollTo option