How to update toValue for a spring animation in React Native (Animated API)? - react-native

I'd like to be able to change the toValue of an animation responding to props update. The official docs for React Native Animated API state that the spring method
animates a value according to an analytical spring model based on damped harmonic oscillation. Tracks velocity state to create fluid motions as the toValue updates, and can be chained together.
However, I haven't found anywhere how we can update toValue. Basically, my component looks like this:
const ProgressBar = ({ loadPercentage }) => {
const loadAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
animation.current = spring(loadAnim, {
toValue: loadPercentage,
}).start();
}, [loadAnim, loadPercentage]);
....
}
This doesn't work for all cases. In particular, if loadPercentage changes too often, the component takes up a huge amount of resources. This kinda makes sense, since I'm creating a new animation for each update. Instead, I'd like to simply modify toValue without starting a new animation or anything like that.
This seems pretty basic, but after 4 hours of trying stuff/googling, I give up. -.-
Just in case, I also tried using react-native-reanimated, but no luck there either.

Related

How to force a deep render in React Native on a specific interval?

How can I update a react component once every half second? I have a need to update a component to show new information as it flows in from an api. I want the component to rerender so it can display to new information at a specific interval.
In React, we could use forceUpdate. Unfortunately, in React Native, we don't have that luxury. Luckily, useState offers a deep render every time the setState function is called. This solution is a little hacky, but it gets the job done. Here's how it works:
const [inProgress, setInProgress] = useState(false); //This should be true when you want rerendering to happen.
const [updateCounter, setUpdateCounter] = useState(0); //This is the state the will force the update.
if (inProgress) {
setTimeout(() => setUpdateCounter(updateCounter + 1), 500);
}
This is the simplest way I've found to do it. The interval in this example is set to 500ms. However, it could be set to whatever you want. I chose half a second because I felt it offered a responsive enough feel to the user while also giving enough time for setState to run.

Render FlatList of Videos in a performant way

I am using a react native with expo. I have a lot of videos that I need to render (sort of like TikTok does). When I fetch about 30 videos and put them in the flat list in the renderItem method, it gets stuck and luggish. I was thinking about getting an amount of videos but sending to the renderItem method only 3 videos each time, and when the user will scroll down and reach index 2 it will shift the first index and append the fourth video from the fetched one. The idea was to have a small array of size 3 and change the items in it every scroll, in order to prevent rendering all the videos at once. That required array manipulation and caused a rerender each time the array of videos was updated(each change made sort of a flash - what was indicating a whole rerender).
My question is how should it be implemented in order the transition between the videos to be as fast and clean as possible from the client side perspective? What is the correct way to render videos in a flat list so it won't be stuck? I dont think It should be done that way, there has to be a better way.
This is what I have tried:
// challenges is an array coming from a fetch, just sliced it for the purpose of the example
// suppose it is an array that contains 30 items
const [currentVideos, setCurrentVideos] = useState([challenges.slice(0,3)]);
<FlatList
data={currentVideos}
renderItem={renderItem}
keyExtractor={(challenge, i) => challenge._id}
showsVerticalScrollIndicator={false}
snapToInterval={Dimensions.get("window").height - UIConsts.bottomNavbarHeight}
snapToAlignment={"start"}
decelerationRate={"fast"}
ref={(ref) => {
flatListRef.current = ref;
}}
onScrollToIndexFailed={() => alert("no such index")}
onViewableItemsChanged={onViewRef.current}
onScrollEndDrag={() => (scrollEnded.current = true)}
onScrollBeginDrag={beginDarg}
></FlatList>
useEffect(() => {
// just wanted to check on 3 videos
if (currentlyPlaying === 2) {
let temp = currentVideos;
temp.shift(); // pop the top item
temp.push(challenges[4]) // append a new one
setCurrentVideos(temp);
}
}, [currentlyPlaying]);
const onViewRef = useRef(({ viewableItems }) => {
// change playing video only after user stop dragging
scrollEnded.current && setCurrentlyPlaying(viewableItems[0]?.index);
});
I would avoid manipulating the data array and doing business logic inside of the component.
Besides, you can achieve your desired behaviour without the need to manipulate your data array at all, with the maxToRenderPerBatch FlatList prop. As mentioned in the official RN docs for FlatList optimization techniques.
You should avoid using anonymous functions and objects inside of your component's properties, move them outside of the return statement and use the useMemo and useCallback hooks to avoid their unnecessary recreation on every re-render. For example instead of writing your code like this:
const App = () => {
return (
<FlatList
keyExtractor={(challenge, i) => challenge._id}
snapToInterval={Dimensions.get('window').height - UIConsts.bottomNavbarHeight}
/>
);
};
A better approach would be to re-write it to something like this:
const App = () => {
// Because of useCallback, the keyExtractor function will be memoized and won't recreate itself on every re-render
const keyExtractor = useCallback((challenge, i) => challenge._id, []);
// useMemo is almost the same as useCallback, but it is used to return non-function types
// Defining your snapToInterval variable like this will cause it to memoize its value and it
// won't recreate itself on every re-render
const snapToInterval = useMemo(() => Dimensions.get('window').height - UIConsts.bottomNavbarHeight, []);
return (
<FlatList
keyExtractor={keyExtractor}
snapToInterval={snapToInterval}
/>
);
};
If you haven't already, you should consider extracting the component returned from the renderItem function to a different file and applying React.memo to it.
Note: try not to overuse useCallback and useMemo. You can find good and detailed explanation of why not to overuse them here and here.
If you're able to, you should optimize your videos before uploading them to the server. You can optimize your client side part of the app as much as you want, but if the content isn't properly optimized, you won't be able to achieve a smooth and performant experience regardless of your efforts.
Here's also some articles describing how you can optimize your FlatList component:
How did I optimize my React Native FlatList?
8 ways to optimize React native FlatList performance
Optimizing a React Native FlatList With Many Child Components
React Native Performance Optimisation With Hooks
React Native: Optimized FlatList of videos
I hope that some of this will be helpful to you. Good luck.
I have been searching for a solution as well. I have worked out a solution based on some previous work using InViewPort. you can check it out here https://github.com/471Q/React-Native-FlatList-Video-Feed

Low performance on React Native maps when components are used inside of marker

I am making a react native map, using MapView from 'react-native-maps' and this marker clustering engine. This is the component for a cluster marker that has been working just fine with blazing performance:
export default class ClusterMarker extends PureComponent {
constructor(props) {
super(props)
this.state = {
tracksViewChanges: true
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props !== nextProps) {
this.setState(() => ({
tracksViewChanges: true,
}))
}
}
componentDidUpdate() {
if (this.state.tracksViewChanges) {
this.setState(() => ({
tracksViewChanges: false,
}))
}
}
render() {
/*...
bunch of unrelated code where 'message', 'image', 'coordinate' and 'pointCount' are set
...*/
return (
<Marker
anchor={{x:0.5,y:0.5}}
centerOffset={{x:0.5,y:0.5}}
coordinate={coordinate}
image={image}
title={pointCount}
description={message}
tracksViewChanges={this.state.tracksViewChanges}>
{/* <Text>{pointCount}</Text> */} <-------- I want this to work just as fast
</Marker>
)
}
}
See that title={pointCount} in Marker props? When a user clicks a cluster, an overlay pops up to show them how many pins are there in the cluster. I want to bring that text out of there and display it over the cluster. Note that in render inside Marker, there is a commented line. When uncommented, visually it does pretty much what is needed, but with a terrible perfomance on phone (1 fps would be an overstatement).
It's clear that the problem is with Text. My hypothesis is that the Text component keeps on checking parent if there are updates on text, or that it re-renders all the time for no reason. Here is a list of things I've tried to fix this:
Create a component extending from Text, performing the same task without weird updates or re-renders. No fps increase.
Create a component that renders a Text but never updates or re-renders for no reason. No fps increase.
Add the tracksViewChanges logic you can see above. That was somewhat of a success, increasing average fps from 1 to 5.
Use other marker clustering libs (all of which failed to work with decent performance even without the Text inside Marker).
Many hacky solutions from the internet, none of which actually improved performace.
This map is currently dealing with an average of 20,000 pins and the solution has to be performant as this pin count increases, because it will.
I would be very glad if anyone could help!
Perhaps this is not the answer you are looking for, but 20k markers is... a lot. Have you considered clustering the markers based on the zoom level? You could also filter out markers that are not in the map viewport.

React Native state update during animation "resets" the animation

I am facing a problem that I've tried to solve in lots of different ways, but I cannot get it to work. Please see this Expo application, I've created a dumb example that demonstrates my problem: https://snack.expo.io/HJB0sE4jS
To summarize, I want to build an app with a draggable component (The blue dot in the example), but while the user drags the component I also need to update the state of the app (the counter in the example). The problem is that whenever the state updates during dragging, the component resets to it's initial position. I want to allow the user to freely drag the component while state updates happen.
I was able to "solve" the issue by putting the PanResponder in a useRef, so it won't be reinitialized in case of a state update, but as you can see in the example, I want to use the state in the PanResponder. If I put it in a useRef I cannot use the state in the PanResponder because it will contain a stale value (it will always contain the initial value of the counter which is 0).
How do you handle these kind of situations in react native? I guess it is not too uncommon that someone wants to update the state during an animation, although I cannot find any documentation or examples on this.
What am I doing wrong?
Edit: I was investigating further and I can see that the problem is that I'm mapping the (dx,dy) values from the gesture parameter to the position, but the (dx,dy) values are reset to (0,0) when the state changes. I guess (dx,dy) initialized to (0,0) when PanResponder is created. Still don't know what to do to make this work...
A mutable ref that holds the latest counter state value, along with a ref to prevent re-initializing the PanResponder should solve the problem in the example:
const [counter] = useCounter();
// Update the counterValue ref whenever the counter state changes
const counterValue = useRef(counter);
useEffect(() => {
counterValue.current = counter;
}, [counter]);
const position = useRef(new Animated.ValueXY());
const panResponder = useRef(PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event(
[null, { dx: position.current.x, dy: position.current.y }],
{ listener: () => console.log(counterValue.current) } // counterValue being a ref, will not go stale
),
onPanResponderRelease: () => {
Animated.spring(position.current, { toValue: { x: 0, y: 0 } }).start();
}
})
);
You can check the above suggestion here: https://snack.expo.io/rkMLgp4jB
I understand though that this is a rather simplified example, and may not work for your actual use-case. It would help if you could share some more details on the actual usage!

SectionList - Animate on Section Press

We were asked to implement a screen for our app where there would be some data in the form of a list and sections for each data category. When a Section header is pressed, the section data should be expanded or collapsed.
At first, i tried with Listview, and i was changing the datasource each time a header was pressed, but tbh it did not feel like the correct way of doing it.
Creating a custom view, an animating the view's height works ok, but because of the volume of the data, which is big, the initial rendering is a bit slow e.g. there is a noticeable delay when navigating to the screen.
After upgrading to RN 44.3 i was wondering if i could use Sectionlist in a better way than listview.
Generally what's the best way of approaching a requirement like this?
Thanks!
You can animate the items in flatlist/SectionList. A smaple code will look like below (Animates on removing items). You can use same logic for section list as well.
onRemove = () => {
const { onRemove } = this.props;
if (onRemove) {
Animated.timing(this._animated, {
toValue: 0,
duration: ANIMATION_DURATION,
}).start(() => onRemove());
}
};
Refer this link for more details.