Running React Native dependent animations on UI thread using Reanimated

Sangamesh Somawar

By Sangamesh Somawar

on March 13, 2023

Slider

Here we have a slider. When user slides the slider then the loader needs to show how much is loaded. We want to animate the loader component when the slider moves. In other words the loader animation is "dependent" on the slider animation.

This is an example of Dependent Animations on the UI thread. Dependent Animation is when one view is animated based on some another element.

first we need a gesture handler to detect the slider events. Then based on the slider events we need to animate the progress of loader component.

Building gesture handler to detect the slider events

1const [width, setWidth] = useState(200);
2const x = useSharedValue(0);
3
4const gestureHandler = useAnimatedGestureHandler({
5  onStart: (event, ctx) => {
6    x.value = event.absoluteX;
7  },
8  onActive: (event, ctx) => {
9    x.value = event.absoluteX;
10    // We need to calculate `progress` here based on the slider position.
11  },
12});
13
14const animatedStyle = useAnimatedStyle(() => {
15  return {
16    transform: [
17      {
18        translateX: x.value,
19      },
20    ],
21  };
22});
23
24return (
25  <PanGestureHandler onGestureEvent={gestureHandler}>
26    <Animated.View style={[{ height: 20 }]}>
27      <Animated.View
28        pointerEvents="none"
29        style={[
30          {
31            backgroundColor: "blue",
32            height: 20,
33            width: 20,
34            borderRadius: 10,
35          },
36          animatedStyle,
37        ]}
38      />
39      <View
40        pointerEvents="none"
41        style={{
42          backgroundColor: "black",
43          height: 2,
44          width: "100%",
45          position: "absolute",
46          top: 10,
47        }}
48      />
49    </Animated.View>
50  </PanGestureHandler>
51);

Loader component:

1<View style={{ height: 20, marginTop: 10 }}>
2  <Lottie
3    style={{
4      alignSelf: "center",
5      width: "100%",
6    }}
7    progress={"Calculated `progress` based on slider position."}
8    source={require("./progress")}
9    autoPlay={false}
10    loop={false}
11  />
12</View>

Animate the loader based on the slider position

Before we move further, let's understand the difference between UI Thread and JS Thread. The UI Thread handles rendering and gestures of Android and iOS views, whereas the JS Thread takes care of all the logic of the React Native application.

We have two approaches for animating the loader.

In the first approach, when the slider moves, we can store the progress in react state and pass it to Lottie animation. With this approach, the entire component rerenders on setting the progress.

1const [progress, setProgress] = useState(0);
2const gestureHandler = useAnimatedGestureHandler({
3  onStart: (event, ctx) => {
4    x.value = event.absoluteX;
5  },
6  onActive: (event, ctx) => {
7    x.value = event.absoluteX;
8    runOnJS(setProgress)(x.value / width);
9  },
10});
11
12<Lottie
13  style={{
14    alignSelf: "center",
15    width: "100%",
16  }}
17  progress={progress}
18  source={require("./progress")}
19  autoPlay={false}
20  loop={false}
21/>;

In the second approach, when the slider moves, we can calculate progress and pass it to the loader component via the useAnimatedProps. In this way, the progress gets calculated on the UI thread itself. Hence it avoids rerenders.

1const LottieAnimated = Animated.createAnimatedComponent(Lottie);
2const [progress, setProgress] = useState(0);
3const lottieAnimatedProps = useAnimatedProps(() => progress: x.value / width);
4const gestureHandler = useAnimatedGestureHandler({
5  onStart: (event, ctx) => {
6    x.value = event.absoluteX;
7  },
8  onActive: (event, ctx) => {
9    x.value = event.absoluteX;
10  },
11});
12
13<LottieAnimated
14  style={{
15    alignSelf: "center",
16    width: "100%",
17  }}
18  animatedProps={lottieAnimatedProps}
19  source={require("./progress")}
20  autoPlay={false}
21  loop={false}
22/>;

Conclusion

With the first approach, whenever the slider moves, the UI thread will pass the gesture event to the JS thread to store the progress value in react state, and when the progress value changes in the JS thread, it causes re-render. This approach creates a lot of traffic over Communication Bridge because of message exchange between UI and JS threads.

So we should prefer the second approach to run any calculations on the UI thread instead of the JS thread. Here is the entire code for reference:

1let approach1ReRenderCount1 = 0;
2const Approach1: () => Node = () => {
3  const [width, setWidth] = useState(1);
4  const x = useSharedValue(0);
5
6  const [progress, setProgress] = useState(0);
7  const gestureHandler = useAnimatedGestureHandler({
8    onStart: (event, ctx) => {
9      x.value = event.absoluteX;
10    },
11    onActive: (event, ctx) => {
12      x.value = event.absoluteX;
13      runOnJS(setProgress)(x.value / width);
14    },
15  });
16
17  const animatedStyle = useAnimatedStyle(() => {
18    return {
19      transform: [
20        {
21          translateX: x.value,
22        },
23      ],
24    };
25  });
26
27  return (
28    <SafeAreaView>
29      <View
30        style={{
31          height: 150,
32          paddingHorizontal: 20,
33          borderWidth: 1,
34          margin: 10,
35          justifyContent: "center",
36        }}
37        onLayout={({
38          nativeEvent: {
39            layout: { width },
40          },
41        }) => {
42          setWidth(width);
43        }}
44      >
45        <Text style={{ fontSize: 20, paddingBottom: 10 }}>Approach 1</Text>
46        <Text style={{ fontSize: 20 }}>
47          Rerender Count : {approach1ReRenderCount1++}
48        </Text>
49
50        <View style={{ height: 20, marginTop: 10 }}>
51          <LottieAnimated
52            style={{
53              alignSelf: "center",
54              width: "100%",
55            }}
56            progress={progress}
57            source={require("./progress")}
58            autoPlay={false}
59            loop={false}
60          />
61        </View>
62        <PanGestureHandler onGestureEvent={gestureHandler}>
63          <Animated.View style={[{ height: 20 }]}>
64            <Animated.View
65              style={[
66                {
67                  backgroundColor: "blue",
68                  height: 20,
69                  width: 20,
70                  borderRadius: 10,
71                },
72                animatedStyle,
73              ]}
74            />
75            <View
76              style={{
77                backgroundColor: "black",
78                height: 2,
79                width: "100%",
80                position: "absolute",
81                top: 10,
82              }}
83            />
84          </Animated.View>
85        </PanGestureHandler>
86      </View>
87    </SafeAreaView>
88  );
89};
90
91const LottieAnimated = Animated.createAnimatedComponent(Lottie);
92let approach2ReRenderCount = 0;
93const Approach2: () => Node = () => {
94  const [width, setWidth] = useState(1);
95  const x = useSharedValue(0);
96
97  const lottieAnimatedProps = useAnimatedProps(() => progress: x.value / width);
98
99  const gestureHandler = useAnimatedGestureHandler({
100    onStart: (event, ctx) => {
101      x.value = event.absoluteX;
102    },
103    onActive: (event, ctx) => {
104      x.value = event.absoluteX;
105    },
106  });
107
108  const animatedStyle = useAnimatedStyle(() => {
109    return {
110      transform: [
111        {
112          translateX: x.value,
113        },
114      ],
115    };
116  });
117
118  return (
119    <SafeAreaView>
120      <View
121        style={{
122          height: 150,
123          paddingHorizontal: 20,
124          borderWidth: 1,
125          margin: 10,
126          justifyContent: "center",
127        }}
128        onLayout={({
129          nativeEvent: {
130            layout: { width },
131          },
132        }) => {
133          setWidth(width);
134        }}
135      >
136        <Text style={{ fontSize: 20, paddingBottom: 10 }}> Approach 2</Text>
137        <Text style={{ fontSize: 20 }}>
138          Rerender Count : {approach2ReRenderCount++}
139        </Text>
140        <View style={{ height: 20, marginTop: 10 }}>
141          <LottieAnimated
142            animatedProps={lottieAnimatedProps}
143            style={{
144              alignSelf: "center",
145              width: "100%",
146            }}
147            source={require("./progress")}
148            autoPlay={false}
149            loop={false}
150          />
151        </View>
152        <PanGestureHandler onGestureEvent={gestureHandler}>
153          <Animated.View style={[{ height: 20 }]}>
154            <Animated.View
155              pointerEvents="none"
156              style={[
157                {
158                  backgroundColor: "blue",
159                  height: 20,
160                  width: 20,
161                  borderRadius: 10,
162                },
163                animatedStyle,
164              ]}
165            />
166            <View
167              pointerEvents="none"
168              style={{
169                backgroundColor: "black",
170                height: 2,
171                width: "100%",
172                position: "absolute",
173                top: 10,
174              }}
175            />
176          </Animated.View>
177        </PanGestureHandler>
178      </View>
179    </SafeAreaView>
180  );
181};

Stay up to date with our blogs. Sign up for our newsletter.

We write about Ruby on Rails, ReactJS, React Native, remote work,open source, engineering & design.