Skip to content

Examples

Automatic Promise completion

tsx
<SmartRefreshLayout
  hasMore={hasMore}
  onRefresh={reload}
  onLoadMore={async () => {
    const result = await loadNextPage();
    setHasMore(result.hasMore);
    return { hasMore: result.hasMore };
  }}
>
  <FlatList {...listProps} />
</SmartRefreshLayout>

Error handling

tsx
<SmartRefreshLayout
  onRefresh={reload}
  onRefreshError={(error) => reportError(error)}
  onLoadMore={loadNextPage}
  onLoadMoreError={() => showToast('Unable to load more')}
>
  <FlatList {...listProps} />
</SmartRefreshLayout>

Programmatic refresh

tsx
const refreshRef = useRef<SmartRefreshLayoutRef>(null);

useEffect(() => {
  refreshRef.current?.beginRefresh(150);
}, []);

<SmartRefreshLayout ref={refreshRef} onRefresh={reload}>
  <FlatList {...listProps} />
</SmartRefreshLayout>;

Lottie custom header

The complete Expo Router example is example/app/lottie.tsx. This standalone component covers the full lifecycle: progress follows native pull and spring-back in pulling/ready; refreshing removes controlled progress and uses play() + loop; completion pauses and resets the animation. Do not update progress only when isDragging is true, or a cancelled pull cannot return to its first frame.

tsx
import { useCallback, useEffect, useRef, useState } from 'react';
import { FlatList, Text, View } from 'react-native';
import LottieView from 'lottie-react-native';
import {
  SmartRefreshLayout,
  type HeaderMovingEvent,
} from 'expo-smartrefreshlayout';

const rows = ['Inbox', 'Mentions', 'Saved'];
const wait = (milliseconds: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, milliseconds));

export function LottieRefreshList() {
  const lottieRef = useRef<LottieView>(null);
  const refreshingRef = useRef(false);
  const [refreshing, setRefreshing] = useState(false);
  const [headerProgress, setHeaderProgress] = useState(0);
  const [headerOffset, setHeaderOffset] = useState(0);

  refreshingRef.current = refreshing;

  useEffect(() => {
    const animation = lottieRef.current;
    if (refreshing) {
      animation?.play();
      return;
    }

    animation?.pause();
    animation?.reset();
  }, [refreshing]);

  const handleHeaderMoving = useCallback(({ percent, offset }: HeaderMovingEvent) => {
    if (refreshingRef.current) {
      return;
    }

    setHeaderProgress(Math.min(Math.max(percent, 0), 1));
    setHeaderOffset(Math.max(0, Math.round(offset)));
  }, []);

  const refresh = useCallback(async () => {
    refreshingRef.current = true;
    setRefreshing(true);

    try {
      await wait(900);
    } finally {
      refreshingRef.current = false;
      setRefreshing(false);
      setHeaderProgress(0);
      setHeaderOffset(0);
    }
  }, []);

  const handleAnimationLoaded = useCallback(() => {
    if (refreshingRef.current) {
      lottieRef.current?.play();
    }
  }, []);

  return (
    <SmartRefreshLayout
      style={{ flex: 1 }}
      loadMoreEnabled={false}
      refreshHeader={
        <View
          pointerEvents="none"
          style={{ alignItems: 'center', height: 80, justifyContent: 'center' }}
        >
          <LottieView
            ref={lottieRef}
            source={require('../assets/load.json')}
            progress={refreshing ? undefined : headerProgress}
            loop
            style={{ height: 48, width: 48 }}
            onAnimationLoaded={handleAnimationLoaded}
          />
          <Text>
            {refreshing
              ? 'Refreshing...'
              : headerProgress >= 1
                ? 'Release to refresh'
                : `Pull ${headerOffset} px`}
          </Text>
        </View>
      }
      onHeaderMoving={handleHeaderMoving}
      onRefresh={refresh}
    >
      <FlatList
        data={rows}
        keyExtractor={(item) => item}
        renderItem={({ item }) => <Text style={{ padding: 24 }}>{item}</Text>}
      />
    </SmartRefreshLayout>
  );
}

A custom Header is a fixed 80 logical-pixel native slot. offset, height, and maxDragHeight use Android dp / iOS pt; percent >= 1 is the refresh threshold.

Automatic loading

tsx
<SmartRefreshLayout
  loadMoreMode="auto"
  hasMore={hasMore}
  onLoadMore={loadNextPage}
>
  <FlatList {...listProps} />
</SmartRefreshLayout>

Automatic mode requires overflowing content and a real upward scroll. Keep FlatList.onEndReached disabled.

For a complete Android second-floor example, see Android second floor.

Released under the MIT License.