useState function seems to block Animated.timing event? - react-native

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.

Related

Conditionally trigger Animated.timing from ScrollY position of ScrollView

I've created a template for screens throughout my app. The template utilises Animated Header components that respond to the scrollY position of the ScrollView content.
I'm able to use this scrollY variable to interpolate items, but I can't figure out how to use the variable to trigger Animated.timing events.
In simple terms:
if (ScrollY > 30) do Animated.timing one
if (ScrollY < 30) do Animated.timing two
For now I've created an inadequate workaround using the Animated.event() listener:
<ScrollView
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollListener } } }],
{
listener: (event) => {
handleScroll(event);
},
}
)}
...
The handleScroll(event) controls an array of Animated.timing events, each of which operates conditionally from a setScrollPosition() useState variable:
const [scrollPosition, setScrollPosition] = useState(0);
const fixedHeaderTranslatePosition = useRef(new Animated.Value(4)).current;
const handleScroll = (event) => {
setScrollPosition(
event.nativeEvent.contentOffset.y +
(Platform.OS === 'ios' ? HEADER_MAX_HEIGHT : 0)
);
if (scrollPosition > 30 && headerType === 'Animated') {
Animated.timing(fixedHeaderTranslatePosition, {
toValue: 0,
useNativeDriver: true,
}).start();
...
}
Whilst my efforts do create a functional component, there are some minor problems.
Firstly, the event.nativeEvent.contentOffset.y seems to cause a lag on Android devices. This is most noticeable with the fixedHeaderTranslatePosition.
Also logically it would make much more sense to utilise the already declared scrollListener from the Animated.event. So my question is: How can I utilise the ScrollY variable (scrollListener) inside my handleScroll()? 🤔
The ScrollY variable is not noted above since it makes more sense in context. Here's a working snack: https://snack.expo.dev/#dazzerr/animated-header-example . You can search for "TODO" to find the areas that I believe require attention.
Thanks in advance!

React Native - component callback animates a view first time but not on subsequent calls

I have an animation which is triggered via a callback function sent to a component in render(). The animation is just some text moving from outside the right edge of the screen across the screen and off the left side. The first time the callback is triggered it works perfectly, but on subsequent callbacks nothing can be seen and there are no errors. Relevant code below:
import { Animated, Easing....} from 'react-native';
class Play extends Component {
constructor() {
super();
this.state = {currSectName:"Intro",
sectNameXcord: new Animated.Value(Dimensions.get('window').width),....}
}
sectNameTraverse = (newSect) =>{
this.state.currSectName = newSect;
this.state.sectNameXcord = new Animated.Value(Dimensions.get('window').width);
//run the animation
Animated.timing(
this.state.sectNameXcord,
{
toValue: -320,
duration: 3000, // the duration of the animation
easing: Easing.linear, // the style of animation
useNativeDriver: true
}
).start((res)=>{
console.log(res);
});
}
render() {
return (
<>
<Animated.View style={{position: 'absolute',top:100,
transform: [{translateX: this.state.sectNameXcord }]}}>
<Text style={{ color:"white",opacity:0.5,fontStyle: 'italic',fontWeight: 'bold',fontSize:99}}>
{this.state.currSectName}
</Text>
</Animated.View>
<Player
sectionChange={(newSect) => {this.sectNameTraverse(newSect)}}
/>
}
On subsequent callbacks the following is true:
the state variables are updated and correct for the next animation
the callback of Animated.timing().start() shows {"finished": true} after the duration period as if it has executed correctly BUT nothing shows up on the screen
no errors are thrown
I'm guessing it may have something to do with instances/binding but I'm not good enough
with React Native to fix it. Been stuck on it for 2 days and could really do with some help.
Thanks a lot.
You should not mutate the state. you should be using 'setState' to update the state.
sectNameTraverse = (newSect) => {
this.setState(
{
currSectName:newSect,
sectNameXcord: new Animated.Value(Dimensions.get('window').width)
},
() => {
//run the animation
Animated.timing(this.state.sectNameXcord, {
toValue: -320,
duration: 3000, // the duration of the animation
easing: Easing.linear, // the style of animation
useNativeDriver: true,
}).start((res) => {
console.log(res);
});
}
);
};
here is the snack i created.

React-Native Animated does not work near setState

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;

React Native - changing screen content while animating

I have an animated screen using gesture handler, and reanimated libraries. My problem is I am trying to change the screen content while the animations fired. I have made it work, but honestly I am 100% sure this is not the best solution (or it might be, but need some more configurations)
Code:
const onStateChangeLogin = event([
{
nativeEvent: ({state}) => block([
cond(eq(state, State.END), set(buttonOpacity, runTiming(new Clock(), 1, 0))), call([], () => handleTapLogin())
])
}
]);
In the call function, I am trying to change the content:
const handleTapLogin = (e) => {
setContent(<Login />)}
This is the Animated.View:
<TapGestureHandler onHandlerStateChange={onStateChangeLogin}>
<Animated.View style={{...styles.button, opacity: buttonOpacity, transform:[{translateY: buttonY}]}}>
<Text style={{...styles.buttonText, color: colors.white}}>{strings.signin}</Text>
</Animated.View>
</TapGestureHandler>
And the screen content is located in the bottom of my 'return' method:
....</Animated.View>
</TapGestureHandler>
{content}
</Animated.View>
</View>
</Container>
So it's working now, but too slow. I think because the call function in the event is async. So what happens now, the content is changing, but sometimes I need to wait like 2-3 secs to see the right screen.
I have already tried to use 'listener' in the event function:
const onStateChangeLogin = event([
{
nativeEvent: ({state}) => block([
cond(eq(state, State.END), set(buttonOpacity, runTiming(new Clock(), 1, 0))), call([], () => handleTapLogin())
], {
listener: () => handleTapLogin
}
)
}
]);
And I'm also tried to use onGestureEvent and onHandlerStateChange together like:
<TapGestureHandler onGestureEvent={onStateChangeLogin} onHandlerStateChange={handleTapLogin}>
...
</TapGestureHandler>

Make react-native component blink at regular time interval

I am trying to make a component "blink" on my page. I was thinking about setting a visible: true state in my componentWillMount method and then put a timeout of 1s in componentDidUpdate to set state to the "opposite" of the previous state. As I see it the component lifecycle looks like this :
sets state to visible to true (componentWillMount that runs only once and is not triggering a rerender)
enters componentdidUpdate
waits 1s
hides component (setstate to visible false)
enters componentDidUpdate
waits 1s
shows component (setstate to visible true)
However my component is blinking but the intervals of hide and show are not regular, they change and dont seem to follow the 1s logic
Here's my component code :
class ResumeChronoButton extends Component {
componentWillMount(){
console.log('in componentWillMount')
this.setState({visible: true})
}
componentDidUpdate(){
console.log('in componentDidUpdate')
setTimeout(() =>this.setState({visible: !this.state.visible}), 1000)
}
// componentWillUnmount(){
// clearInterval(this.interval)
// }
render(){
const { textStyle } = styles;
if (this.state.visible){
return (
<TouchableOpacity onPress={this.props.onPress}>
<Pause style={{height: 50, width: 50}}/>
</TouchableOpacity>
);
}
else {
return (
<View style={{height: 50, width: 50}}>
</View>
)
}
}
};
How can I make my component blink at regular time interval.
The following works for me
componentDidMount = () => {
this.interval = setInterval(() => {
this.setState((state, props) => {
return {
visible: !state.visible,
};
});
}, 1000);
};
componentWillUnmount = () => {
clearInterval(this.interval);
};
and then your render can just check this.state.visible to determine if it needs to show or not.
alternatively you could change the setState to
this.setState({visible: !this.state.visible})
Most likely because you are using the state and timeouts. State is set asynchronously and, for this reason, it may take different amounts of time to change the value depending on how many resources you are using.
To achieve the effect you want I would recommendo you to use the Animation framework from React Native. Check the docs.
just use
setInterval(()=>{//setstate here},time_in_ms)