Files
hipmi-mobile/helpers/PaginationGuide.md
bagasbanuna b3bfbc0f7e Fix Infinite Load Data
Forum & User Search – User
- app/(application)/(user)/forum/index.tsx
- app/(application)/(user)/user-search/index.tsx

Global & Core
- app/+not-found.tsx
- screens/RootLayout/AppRoot.tsx
- constants/constans-value.ts

Component
- components/Image/AvatarComp.tsx

API Client
- service/api-client/api-user.ts

Untracked Files
- QWEN.md
- helpers/
- hooks/use-pagination.tsx
- screens/Forum/ViewBeranda3.tsx
- screens/UserSeach/

### No Issue
2026-01-29 11:36:24 +08:00

13 KiB

📱 Reusable Pagination untuk React Native + Expo

Komponen pagination yang terintegrasi dengan NewWrapper untuk infinite scroll, pull-to-refresh, skeleton loading, dan empty state.


📦 File Structure

/hooks/
  └── usePagination.tsx          # Custom hook untuk logika pagination

/helpers/
  └── paginationHelpers.tsx      # Helper functions untuk komponen UI

/components/
  └── NewWrapper.tsx             # Komponen wrapper utama (existing)

🚀 Cara Penggunaan

Step 1: Import Hook dan Helpers

import { usePagination } from "@/hooks/usePagination";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";

Step 2: Setup Pagination Hook

const pagination = usePagination({
  // ✅ Fungsi untuk fetch data (harus return { data: T[] })
  fetchFunction: async (page, searchQuery) => {
    return await apiForumGetAll({
      category: "beranda",
      search: searchQuery || "",
      userLoginId: user.id,
      page: String(page),
    });
  },
  
  // ✅ Page size (harus sama dengan API)
  pageSize: 5,
  
  // ✅ Query pencarian
  searchQuery: search,
  
  // ✅ Dependencies (reload saat berubah)
  dependencies: [user?.id, category],
  
  // ⚙️ Optional callbacks
  onDataFetched: (data) => console.log("Loaded:", data.length),
  onError: (error) => console.error("Error:", error),
});

Step 3: Generate Komponen Pagination

const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
  loading: pagination.loading,
  refreshing: pagination.refreshing,
  listData: pagination.listData,
  searchQuery: search,
  emptyMessage: "Tidak ada data",
  emptySearchMessage: "Tidak ada hasil pencarian",
  skeletonCount: 5,
  skeletonHeight: 200,
});

Step 4: Gunakan dengan NewWrapper

<NewWrapper
  // Props dari pagination hook
  listData={pagination.listData}
  refreshControl={
    <RefreshControl
      refreshing={pagination.refreshing}
      onRefresh={pagination.onRefresh}
    />
  }
  onEndReached={pagination.loadMore}
  
  // Komponen dari helpers
  ListEmptyComponent={ListEmptyComponent}
  ListFooterComponent={ListFooterComponent}
  
  // Render item
  renderItem={({ item }) => <YourComponent data={item} />}
  
  // Props lain dari NewWrapper
  headerComponent={<SearchInput />}
  floatingButton={<FloatingButton />}
/>

📖 Contoh Implementasi Lengkap

Contoh 1: Forum Page (Basic)

import { usePagination } from "@/hooks/usePagination";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";

export default function ForumPage() {
  const { user } = useAuth();
  const [search, setSearch] = useState("");

  // Setup pagination
  const pagination = usePagination({
    fetchFunction: async (page, searchQuery) => {
      if (!user?.id) return { data: [] };
      
      return await apiForumGetAll({
        category: "beranda",
        search: searchQuery || "",
        userLoginId: user.id,
        page: String(page),
      });
    },
    pageSize: 5,
    searchQuery: search,
    dependencies: [user?.id],
  });

  // Generate komponen
  const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
    loading: pagination.loading,
    refreshing: pagination.refreshing,
    listData: pagination.listData,
    searchQuery: search,
    emptyMessage: "Tidak ada diskusi",
    emptySearchMessage: "Tidak ada hasil pencarian",
  });

  return (
    <NewWrapper
      headerComponent={
        <SearchInput
          placeholder="Cari diskusi..."
          onChangeText={_.debounce(setSearch, 500)}
        />
      }
      listData={pagination.listData}
      renderItem={({ item }) => <ForumItem data={item} />}
      refreshControl={
        <RefreshControl
          tintColor={MainColor.yellow}
          refreshing={pagination.refreshing}
          onRefresh={pagination.onRefresh}
        />
      }
      onEndReached={pagination.loadMore}
      ListEmptyComponent={ListEmptyComponent}
      ListFooterComponent={ListFooterComponent}
    />
  );
}

Contoh 2: Product Page (Dengan Filter)

export default function ProductPage() {
  const [search, setSearch] = useState("");
  const [category, setCategory] = useState("all");

  const pagination = usePagination({
    fetchFunction: async (page, searchQuery) => {
      return await apiProductGetAll({
        page: String(page),
        search: searchQuery || "",
        category: category !== "all" ? category : undefined,
      });
    },
    pageSize: 10,
    searchQuery: search,
    dependencies: [category], // Reload saat category berubah
  });

  const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
    loading: pagination.loading,
    refreshing: pagination.refreshing,
    listData: pagination.listData,
    searchQuery: search,
    emptyMessage: "Belum ada produk",
    skeletonCount: 8,
    skeletonHeight: 100,
  });

  return (
    <NewWrapper
      headerComponent={
        <View>
          <SearchInput onChangeText={setSearch} />
          <CategoryFilter value={category} onChange={setCategory} />
        </View>
      }
      listData={pagination.listData}
      renderItem={({ item }) => <ProductCard product={item} />}
      refreshControl={
        <RefreshControl
          refreshing={pagination.refreshing}
          onRefresh={pagination.onRefresh}
        />
      }
      onEndReached={pagination.loadMore}
      ListEmptyComponent={ListEmptyComponent}
      ListFooterComponent={ListFooterComponent}
    />
  );
}

⚙️ API Reference

usePagination Hook

Props

Prop Type Required Default Deskripsi
fetchFunction (page, search?) => Promise<{data: T[]}> - Fungsi fetch data dari API
pageSize number 5 Jumlah data per halaman
searchQuery string "" Query pencarian
dependencies any[] [] Dependencies untuk trigger reload
onDataFetched (data: T[]) => void - Callback saat data berhasil di-fetch
onError (error: any) => void - Callback saat terjadi error

Return Value

{
  listData: T[];              // Array data untuk NewWrapper
  loading: boolean;           // Loading state
  refreshing: boolean;        // Refreshing state
  hasMore: boolean;           // Apakah masih ada data
  page: number;               // Current page
  onRefresh: () => void;      // Function untuk refresh
  loadMore: () => void;       // Function untuk load more
  reset: () => void;          // Function untuk reset state
  setListData: (data) => void; // Function untuk set data manual
}

createPaginationComponents Helper

Props

Prop Type Required Default Deskripsi
loading boolean - Loading state
refreshing boolean - Refreshing state
listData any[] - List data
searchQuery string "" Query pencarian
emptyMessage string "Tidak ada data" Pesan empty state
emptySearchMessage string "Tidak ada hasil pencarian" Pesan empty saat search
skeletonCount number 5 Jumlah skeleton items
skeletonHeight number 200 Tinggi skeleton items
loadingFooterText string - Text loading footer

Return Value

{
  ListEmptyComponent: React.ReactElement;  // Component untuk empty state
  ListFooterComponent: React.ReactElement; // Component untuk loading footer
}

Helper Functions Lain

createSkeletonList(options)

Generate skeleton list untuk loading state.

const SkeletonComponent = createSkeletonList({
  count: 5,
  height: 200
});

createEmptyState(options)

Generate empty state component.

const EmptyComponent = createEmptyState({
  message: "Tidak ada data",
  searchMessage: "Tidak ada hasil pencarian",
  searchQuery: search
});

createLoadingFooter(options)

Generate loading footer component.

const FooterComponent = createLoadingFooter({
  show: loading && listData.length > 0,
  text: "Memuat data..."
});

🎨 Custom Components

Custom Empty State

import { createSkeletonList } from "@/helpers/paginationHelpers";

const CustomEmpty = (
  <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
    <Text>🔍</Text>
    <TextCustom>Data tidak ditemukan</TextCustom>
  </View>
);

const ListEmptyComponent = 
  pagination.loading && pagination.listData.length === 0
    ? createSkeletonList({ count: 5, height: 200 })
    : CustomEmpty;

<NewWrapper
  ListEmptyComponent={ListEmptyComponent}
  // ...
/>
import { createLoadingFooter } from "@/helpers/paginationHelpers";

const CustomFooter = createLoadingFooter({
  show: pagination.loading && !pagination.refreshing && pagination.listData.length > 0,
  customComponent: (
    <View style={{ padding: 20, alignItems: "center" }}>
      <ActivityIndicator size="large" color="#007AFF" />
      <Text style={{ marginTop: 8 }}>Loading more...</Text>
    </View>
  )
});

<NewWrapper
  ListFooterComponent={CustomFooter}
  // ...
/>

Fitur-Fitur

Infinite Scroll - Auto load saat scroll ke bawah
Pull to Refresh - Swipe down untuk refresh
Skeleton Loading - Smooth loading animation
Empty State - Tampilan saat data kosong
Search Integration - Support search dengan debounce
Multi Dependencies - Reload berdasarkan filter apapun
Error Handling - Built-in error handling
TypeScript - Full type safety
Fully Customizable - Custom components untuk semua state


🎯 Best Practices

<SearchInput
  onChangeText={_.debounce((text) => setSearch(text), 500)}
/>

2. Sesuaikan Page Size dengan API

const pagination = usePagination({
  pageSize: 5, // Harus sama dengan takeData di API
});

3. Tambahkan Dependencies yang Relevan

const pagination = usePagination({
  dependencies: [userId, category, sortBy], // Reload saat berubah
});

4. Handle Error dengan Baik

const pagination = usePagination({
  onError: (error) => {
    console.error("Error:", error);
    Alert.alert("Error", "Gagal memuat data");
  },
});

5. Pastikan API Return Format yang Benar

// ❌ SALAH
fetchFunction: async () => [data1, data2];

// ✅ BENAR
fetchFunction: async () => ({ data: [data1, data2] });

🔧 Troubleshooting

Data tidak muncul?

  • Pastikan fetchFunction return { data: T[] }
  • Cek apakah API return format yang benar
  • Pastikan pageSize sesuai dengan API

Infinite scroll tidak jalan?

  • Pastikan API return data sesuai pageSize
  • Cek hasMore state
  • Pastikan onEndReachedThreshold tidak terlalu kecil (default 0.5)

Skeleton terus muncul?

  • Cek loading state
  • Pastikan fetchFunction resolve dengan benar
  • Cek error di console

Refresh tidak bekerja?

  • Pastikan RefreshControl menggunakan pagination.refreshing dan pagination.onRefresh
  • Cek apakah API dipanggil saat pull-to-refresh

📝 Migration Guide

Dari Code Lama ke Code Baru

BEFORE:

const [listData, setListData] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);

const fetchData = async (pageNumber, clear) => {
  // ... 30+ lines of code
};

useEffect(() => {
  setPage(1);
  setListData([]);
  setHasMore(true);
  fetchData(1, true);
}, [search, user?.id]);

const onRefresh = useCallback(() => {
  fetchData(1, true);
}, [search, user?.id]);

const loadMore = useCallback(() => {
  if (hasMore && !loading && !refreshing) {
    fetchData(page + 1, false);
  }
}, [hasMore, loading, refreshing, page, search, user?.id]);

// ... skeleton, empty, footer components

AFTER:

const pagination = usePagination({
  fetchFunction: async (page, search) => await apiGetData({ page, search }),
  pageSize: 5,
  searchQuery: search,
  dependencies: [user?.id]
});

const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
  loading: pagination.loading,
  refreshing: pagination.refreshing,
  listData: pagination.listData,
  searchQuery: search,
});

Result: 50+ lines → 15 lines! 🎉


👨‍💻 Author

Created by Full-Stack Developer
React Native + Expo Specialist


📄 License

MIT License - Feel free to use in your projects!