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
This commit is contained in:
517
helpers/PaginationGuide.md
Normal file
517
helpers/PaginationGuide.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# 📱 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**
|
||||
|
||||
```tsx
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
```
|
||||
|
||||
### **Step 2: Setup Pagination Hook**
|
||||
|
||||
```tsx
|
||||
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**
|
||||
|
||||
```tsx
|
||||
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**
|
||||
|
||||
```tsx
|
||||
<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)**
|
||||
|
||||
```tsx
|
||||
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)**
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
```tsx
|
||||
{
|
||||
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
|
||||
|
||||
```tsx
|
||||
{
|
||||
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.
|
||||
|
||||
```tsx
|
||||
const SkeletonComponent = createSkeletonList({
|
||||
count: 5,
|
||||
height: 200
|
||||
});
|
||||
```
|
||||
|
||||
#### `createEmptyState(options)`
|
||||
Generate empty state component.
|
||||
|
||||
```tsx
|
||||
const EmptyComponent = createEmptyState({
|
||||
message: "Tidak ada data",
|
||||
searchMessage: "Tidak ada hasil pencarian",
|
||||
searchQuery: search
|
||||
});
|
||||
```
|
||||
|
||||
#### `createLoadingFooter(options)`
|
||||
Generate loading footer component.
|
||||
|
||||
```tsx
|
||||
const FooterComponent = createLoadingFooter({
|
||||
show: loading && listData.length > 0,
|
||||
text: "Memuat data..."
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Custom Components
|
||||
|
||||
### **Custom Empty State**
|
||||
|
||||
```tsx
|
||||
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}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
### **Custom Loading Footer**
|
||||
|
||||
```tsx
|
||||
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
|
||||
|
||||
### 1. **Gunakan Debounce untuk Search**
|
||||
```tsx
|
||||
<SearchInput
|
||||
onChangeText={_.debounce((text) => setSearch(text), 500)}
|
||||
/>
|
||||
```
|
||||
|
||||
### 2. **Sesuaikan Page Size dengan API**
|
||||
```tsx
|
||||
const pagination = usePagination({
|
||||
pageSize: 5, // Harus sama dengan takeData di API
|
||||
});
|
||||
```
|
||||
|
||||
### 3. **Tambahkan Dependencies yang Relevan**
|
||||
```tsx
|
||||
const pagination = usePagination({
|
||||
dependencies: [userId, category, sortBy], // Reload saat berubah
|
||||
});
|
||||
```
|
||||
|
||||
### 4. **Handle Error dengan Baik**
|
||||
```tsx
|
||||
const pagination = usePagination({
|
||||
onError: (error) => {
|
||||
console.error("Error:", error);
|
||||
Alert.alert("Error", "Gagal memuat data");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 5. **Pastikan API Return Format yang Benar**
|
||||
```tsx
|
||||
// ❌ 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:**
|
||||
```tsx
|
||||
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:**
|
||||
```tsx
|
||||
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!
|
||||
280
helpers/paginationHelpers.tsx
Normal file
280
helpers/paginationHelpers.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import { View } from "react-native";
|
||||
import { LoaderCustom, TextCustom, StackCustom } from "@/components";
|
||||
import SkeletonCustom from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import _ from "lodash";
|
||||
|
||||
/**
|
||||
* Pagination Helpers
|
||||
*
|
||||
* Helper functions untuk membuat komponen-komponen pagination
|
||||
* yang sering digunakan (Skeleton, Empty State, Loading Footer)
|
||||
*/
|
||||
|
||||
interface SkeletonListOptions {
|
||||
/**
|
||||
* Jumlah skeleton items
|
||||
* @default 5
|
||||
*/
|
||||
count?: number;
|
||||
|
||||
/**
|
||||
* Tinggi setiap skeleton item
|
||||
* @default 200
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Skeleton List Component untuk loading state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <NewWrapper
|
||||
* listData={listData}
|
||||
* ListEmptyComponent={
|
||||
* loading && _.isEmpty(listData)
|
||||
* ? createSkeletonList({ count: 5, height: 200 })
|
||||
* : createEmptyState({ message: "Tidak ada data" })
|
||||
* }
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const createSkeletonList = (options: SkeletonListOptions = {}) => {
|
||||
const { count = 5, height = 200 } = options;
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<StackCustom>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<SkeletonCustom height={height} key={i} />
|
||||
))}
|
||||
</StackCustom>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface EmptyStateOptions {
|
||||
/**
|
||||
* Pesan untuk empty state
|
||||
* @default "Tidak ada data"
|
||||
*/
|
||||
message?: string;
|
||||
|
||||
/**
|
||||
* Pesan untuk empty state saat search
|
||||
*/
|
||||
searchMessage?: string;
|
||||
|
||||
/**
|
||||
* Query pencarian (untuk menentukan pesan mana yang ditampilkan)
|
||||
*/
|
||||
searchQuery?: string;
|
||||
|
||||
/**
|
||||
* Custom component untuk empty state
|
||||
*/
|
||||
customComponent?: React.ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Empty State Component
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* ListEmptyComponent={
|
||||
* createEmptyState({
|
||||
* message: "Tidak ada diskusi",
|
||||
* searchMessage: "Tidak ada hasil pencarian",
|
||||
* searchQuery: search
|
||||
* })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const createEmptyState = (options: EmptyStateOptions = {}) => {
|
||||
const {
|
||||
message = "Tidak ada data",
|
||||
searchMessage = "Tidak ada hasil pencarian",
|
||||
searchQuery = "",
|
||||
customComponent,
|
||||
} = options;
|
||||
|
||||
if (customComponent) return customComponent;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
<TextCustom align="center" color="gray">
|
||||
{searchQuery ? searchMessage : message}
|
||||
</TextCustom>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface LoadingFooterOptions {
|
||||
/**
|
||||
* Tampilkan loading footer
|
||||
*/
|
||||
show: boolean;
|
||||
|
||||
/**
|
||||
* Custom text untuk loading footer
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* Custom component untuk loading footer
|
||||
*/
|
||||
customComponent?: React.ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Loading Footer Component
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* ListFooterComponent={
|
||||
* createLoadingFooter({
|
||||
* show: loading && !refreshing && listData.length > 0,
|
||||
* text: "Memuat data..."
|
||||
* })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const createLoadingFooter = (options: LoadingFooterOptions) => {
|
||||
const { show, text, customComponent } = options;
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
if (customComponent) return customComponent;
|
||||
|
||||
return (
|
||||
<View style={{ paddingVertical: 16, alignItems: "center" }}>
|
||||
{text ? (
|
||||
<TextCustom color="gray">
|
||||
{text}
|
||||
</TextCustom>
|
||||
) : (
|
||||
<LoaderCustom />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface PaginationComponentsOptions {
|
||||
/**
|
||||
* Loading state
|
||||
*/
|
||||
loading: boolean;
|
||||
|
||||
/**
|
||||
* Refreshing state
|
||||
*/
|
||||
refreshing: boolean;
|
||||
|
||||
/**
|
||||
* List data
|
||||
*/
|
||||
listData: any[];
|
||||
|
||||
/**
|
||||
* Query pencarian
|
||||
*/
|
||||
searchQuery?: string;
|
||||
|
||||
/**
|
||||
* Pesan empty state
|
||||
*/
|
||||
emptyMessage?: string;
|
||||
|
||||
/**
|
||||
* Pesan empty state saat search
|
||||
*/
|
||||
emptySearchMessage?: string;
|
||||
|
||||
/**
|
||||
* Jumlah skeleton items
|
||||
*/
|
||||
skeletonCount?: number;
|
||||
|
||||
/**
|
||||
* Tinggi skeleton items
|
||||
*/
|
||||
skeletonHeight?: number;
|
||||
|
||||
/**
|
||||
* Text loading footer
|
||||
*/
|
||||
loadingFooterText?: string;
|
||||
|
||||
/**
|
||||
* Loading pertama
|
||||
*/
|
||||
isInitialLoad?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate semua komponen pagination sekaligus
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
* loading,
|
||||
* refreshing,
|
||||
* listData,
|
||||
* searchQuery: search,
|
||||
* emptyMessage: "Tidak ada diskusi",
|
||||
* emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
* skeletonCount: 5,
|
||||
* skeletonHeight: 200
|
||||
* });
|
||||
*
|
||||
* <NewWrapper
|
||||
* listData={listData}
|
||||
* ListEmptyComponent={ListEmptyComponent}
|
||||
* ListFooterComponent={ListFooterComponent}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const createPaginationComponents = (
|
||||
options: PaginationComponentsOptions
|
||||
) => {
|
||||
const {
|
||||
loading,
|
||||
refreshing,
|
||||
listData,
|
||||
searchQuery = "",
|
||||
emptyMessage = "Tidak ada data",
|
||||
emptySearchMessage = "Tidak ada hasil pencarian",
|
||||
skeletonCount = 5,
|
||||
skeletonHeight = 200,
|
||||
loadingFooterText,
|
||||
isInitialLoad,
|
||||
} = options;
|
||||
|
||||
// Empty Compotnent: Skeleton saat loading pertama, Empty State saat data kosong
|
||||
const ListEmptyComponent =
|
||||
loading && _.isEmpty(listData)
|
||||
? createSkeletonList({ count: skeletonCount, height: skeletonHeight })
|
||||
: createEmptyState({
|
||||
message: emptyMessage,
|
||||
searchMessage: emptySearchMessage,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
// Footer Component: Loading indicator saat load more
|
||||
const ListFooterComponent = createLoadingFooter({
|
||||
show: loading && !refreshing && listData.length > 0,
|
||||
text: loadingFooterText,
|
||||
});
|
||||
|
||||
return {
|
||||
ListEmptyComponent,
|
||||
ListFooterComponent,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user