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. 🌟

📌 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! 💬😊

Sentry blog image

Build, Ship, See It All: MCP Monitoring with Sentry

Built an MCP server? Now see everything it does. Sentry’s MCP Server Monitoring tracks every client, tool, and request so you can fix issues fast and build with confidence.

Read more

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.

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Explore this practical breakdown on DEV’s open platform, where developers from every background come together to push boundaries. No matter your experience, your viewpoint enriches the conversation.

Dropping a simple “thank you” or question in the comments goes a long way in supporting authors—your feedback helps ideas evolve.

At DEV, shared discovery drives progress and builds lasting bonds. If this post resonated, a quick nod of appreciation can make all the difference.

Okay