DEV Community

Cover image for πŸš€ Implementing Pull-to-Refresh in React Native with Reanimated and Lottie πŸŽ‰
Amit Kumar
Amit Kumar

Posted on β€’ Edited on

15 2 2 2 2

πŸš€ Implementing Pull-to-Refresh in React Native with Reanimated and Lottie πŸŽ‰

Refreshing content seamlessly enhances user experience in mobile applications. In this blog, we'll explore how to implement an animated pull-to-refresh feature in React Native using react-native-reanimated and Lottie animations. 🌟

Image description

πŸ“Œ Key Features

βœ… Smooth pull-down gesture for refreshing
βœ… Animated loader using Lottie
βœ… Optimized performance with Reanimated
βœ… Works seamlessly with FlatList


πŸ”§ Setting Up the Project

To get started, install the required dependencies:

npm install react-native-reanimated lottie-react-native react-native-safe-area-context
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ Implementing the Pull-to-Refresh Component

import React, { useRef, useCallback, memo } from 'react';
import {
  StyleSheet,
  Text,
  View,
  Image,
  PanResponder,
  Dimensions,
  StatusBar,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
  useAnimatedScrollHandler,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';
import LottieView from 'lottie-react-native';
import data from './data';

const { width } = Dimensions.get('screen');

const AnimatedPullToRefresh = () => {
  const scrollPosition = useSharedValue(0);
  const insets = useSafeAreaInsets();
  const pullDownPosition = useSharedValue(0);
  const isReadyToRefresh = useSharedValue(false);
  const isLoaderActive = useSharedValue(false);

  const onRefresh = useCallback((done) => {
    isLoaderActive.value = true;

    setTimeout(() => {
      isLoaderActive.value = false;
      isReadyToRefresh.value = false;
      done();
    }, 5000);
  }, []);

  const onPanRelease = () => {
    pullDownPosition.value = withTiming(isReadyToRefresh.value ? 120 : 0, {
      duration: 180,
    });

    if (isReadyToRefresh.value) {
      isReadyToRefresh.value = false;
      onRefresh(() => {
        pullDownPosition.value = withTiming(0, { duration: 180 });
      });
    }
  };

  const panResponderRef = useRef(
    PanResponder.create({
      onStartShouldSetPanResponderCapture: (_, gestureState) => {
        return scrollPosition.value <= 0 && gestureState.dy > 0;
      },
      onMoveShouldSetPanResponderCapture: (_, gestureState) => {
        return scrollPosition.value <= 0 && gestureState.dy > 0;
      },
      onPanResponderMove: (_, gestureState) => {
        const maxPullDistance = 150;
        pullDownPosition.value = Math.min(
          maxPullDistance,
          Math.max(0, gestureState.dy),
        );
        isReadyToRefresh.value = pullDownPosition.value >= maxPullDistance / 2;
      },
      onPanResponderRelease: onPanRelease,
      onPanResponderTerminate: onPanRelease,
    }),
  );

  const scrollHandler = useAnimatedScrollHandler({
    onScroll: (event) => {
      scrollPosition.value = event.contentOffset.y;
    },
  });

  const pullDownStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: pullDownPosition.value }],
  }));

  const refreshContainerStyle = useAnimatedStyle(() => ({
    height: pullDownPosition.value,
    opacity: 1,
    top: pullDownPosition.value - 200,
  }));

  const renderItem = useCallback(
    ({ item }) => (
      <View>
        <Image source={item.image} style={styles.image} resizeMode="cover" />
        <Text style={styles.title}>{item.title}</Text>
        <Text style={styles.subTitle}>{`${item.director} | ${item.year}`}</Text>
      </View>
    ),
    [],
  );

  return (
    <View style={styles.container}>
      <StatusBar backgroundColor={'#000'} />
      <Animated.View style={[refreshContainerStyle, styles.loaderContainer]}>
        <LottieView
          source={require('./4.json')}
          autoPlay
          loop
          speed={0.5}
          style={styles.loader}
        />
      </Animated.View>

      <Animated.View
        style={[
          pullDownStyle,
          styles.pullDownStyles,
          { paddingTop: Math.max(insets.top, 15) },
        ]}
        {...panResponderRef.current.panHandlers}
      >
        <Animated.FlatList
          data={data}
          scrollEventThrottle={16}
          renderItem={renderItem}
          keyExtractor={(_, index) => index.toString()}
          ItemSeparatorComponent={() => (
            <View style={styles.itemSeparatorStyle} />
          )}
          onScroll={scrollHandler}
          numColumns={2}
          showsVerticalScrollIndicator={false}
          overScrollMode="never"
        />
      </Animated.View>
    </View>
  );
};

export default memo(AnimatedPullToRefresh);

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#111',
    flex: 1,
  },
  pullDownStyles: {
    backgroundColor: '#0A0A0A',
    flex: 1,
    paddingHorizontal: 5,
  },
  itemSeparatorStyle: {
    margin: 6,
  },
  image: {
    width: 200,
    height: 300,
    marginRight: 10,
    borderRadius: 8,
  },
  loader: {
    width,
    height: 300,
  },
  loaderContainer: {
    alignItems: 'center',
    width,
    position: 'absolute',
  },
  title: {
    width: 180,
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
    marginTop: 15,
    marginBottom: 5,
  },
  subTitle: {
    width: 180,
    color: '#888',
    fontSize: 12,
    fontWeight: '600',
    marginBottom: 10,
  },
});
Enter fullscreen mode Exit fullscreen mode

🎯 Final Thoughts

This implementation provides a smooth and visually appealing pull-to-refresh experience in React Native. With Reanimated and Lottie, you can create highly interactive and engaging UI components. Try this out in your projects and elevate your app’s user experience! πŸš€πŸ”₯

Let me know in the comments if you have any questions or suggestions! πŸ’¬πŸ˜Š

Top comments (2)

Collapse
 
developer_new_b6fa778c3d8 profile image
developer new β€’

@amitkumar13 I noticed react-native-gesture-handler is included in the dependencies, but I don't see it used in the code. Could you clarify where it’s needed? Thanks

Collapse
 
amitkumar13 profile image
Amit Kumar β€’

I guess there is no need. Let me update it.