SectionList va cheksiz scroll (onEndReached)
Muammodan boshlaylik
Ikki vazifa:
- Kontaktlar ro'yxati — harflar bo'yicha guruhlangan: "A", "B", "D"... va scroll paytida joriy harf tepada yopishib turishi kerak
- Yangiliklar lentasi — serverda 50 000 ta yangilik. Hammasini birdaniga yuklab bo'lmaydi: 20 tadan, foydalanuvchi pastga yetganda keyingi 20 ta
Birinchisi — SectionList. Ikkinchisi — FlatList ning onEndReached.
SectionList — guruhlangan ro'yxat
Ma'lumot tuzilmasi — bo'limlar massivi, har birida data:
import { SectionList, View, Text, StyleSheet } from 'react-native';
type Kontakt = { id: string; ism: string; telefon: string };
const bolimlar = [
{ harf: 'A', data: [{ id: '1', ism: 'Aziz', telefon: '+998901112233' }, { id: '2', ism: 'Anvar', telefon: '+998935554433' }] },
{ harf: 'B', data: [{ id: '3', ism: 'Bobur', telefon: '+998977778899' }] },
{ harf: 'D', data: [{ id: '4', ism: 'Dilnoza', telefon: '+998901234567' }] },
];
export default function Kontaktlar() {
return (
<SectionList
sections={bolimlar}
keyExtractor={(k) => k.id}
renderItem={({ item }) => (
<View style={styles.band}>
<Text style={styles.ism}>{item.ism}</Text>
<Text style={styles.tel}>{item.telefon}</Text>
</View>
)}
renderSectionHeader={({ section }) => (
<Text style={styles.harf}>{section.harf}</Text>
)}
stickySectionHeadersEnabled
/>
);
}
const styles = StyleSheet.create({
harf: { backgroundColor: '#f1f5f9', paddingHorizontal: 16, paddingVertical: 6, fontWeight: '700', color: '#475569' },
band: { paddingHorizontal: 16, paddingVertical: 12, backgroundColor: 'white' },
ism: { fontSize: 16 },
tel: { color: '#64748b', marginTop: 2 },
});
sections—{ ...istalgan maydonlar, data: [...] }massivirenderSectionHeader— bo'lim sarlavhasistickySectionHeadersEnabled— joriy bo'lim sarlavhasi tepada "yopishib" turadi (iOS da standart yoqilgan)
Oddiy ro'yxatni guruhlash
Serverdan oddiy massiv keladi — uni bo'limlarga aylantirish:
function harfBoyichaGuruhla(kontaktlar: Kontakt[]) {
const xarita = new Map<string, Kontakt[]>();
for (const k of [...kontaktlar].sort((a, b) => a.ism.localeCompare(b.ism))) {
const harf = k.ism[0].toUpperCase();
if (!xarita.has(harf)) xarita.set(harf, []);
xarita.get(harf)!.push(k);
}
return [...xarita].map(([harf, data]) => ({ harf, data }));
}
const bolimlar = useMemo(() => harfBoyichaGuruhla(kontaktlar), [kontaktlar]);
Xuddi shu naqsh — buyurtmalarni sana bo'yicha ("Bugun", "Kecha", "Oktabr"), mahsulotlarni toifa bo'yicha guruhlash.
Cheksiz scroll — onEndReached
type Yangilik = { id: number; sarlavha: string };
export default function Lenta() {
const [yangiliklar, setYangiliklar] = useState<Yangilik[]>([]);
const [sahifa, setSahifa] = useState(1);
const [yuklanmoqda, setYuklanmoqda] = useState(false);
const [tugadi, setTugadi] = useState(false);
async function yukla() {
if (yuklanmoqda || tugadi) return; // ⚠️ takroriy chaqiruvlardan himoya
setYuklanmoqda(true);
try {
const javob = await fetch(`https://api.misol.uz/yangiliklar?sahifa=${sahifa}&hajm=20`);
const yangi: Yangilik[] = await javob.json();
setYangiliklar((eski) => [...eski, ...yangi]);
setSahifa((s) => s + 1);
if (yangi.length < 20) setTugadi(true); // oxirgi sahifa
} finally {
setYuklanmoqda(false);
}
}
useEffect(() => { yukla(); }, []);
return (
<FlatList
data={yangiliklar}
keyExtractor={(y) => String(y.id)}
renderItem={({ item }) => <YangilikKarta yangilik={item} />}
onEndReached={yukla}
onEndReachedThreshold={0.5}
ListFooterComponent={
yuklanmoqda ? <ActivityIndicator style={{ margin: 16 }} />
: tugadi ? <Text style={{ textAlign: 'center', margin: 16, color: '#94a3b8' }}>Boshqa yangilik yo'q</Text>
: null
}
/>
);
}
onEndReached— ro'yxat oxiriga yaqinlashganda chaqiriladionEndReachedThreshold={0.5}— oxiriga yarim ekran qolganda. Foydalanuvchi oxiriga yetguncha yangi ma'lumot yuklanib bo'ladi — kutish sezilmaydi- Oxirida — spinner yoki "boshqa yo'q"
[!WARNING]
onEndReachedbir necha marta ketma-ket chaqirilishi mumkin — ayniqsa ro'yxat qisqa bo'lsa yoki tez scroll qilinsa.yuklanmoqdatekshiruvisiz bir xil sahifa ikki marta yuklanadi va elementlar takrorlanadi (keyogohlantirishi chiqadi). Himoya — majburiy.
Bu mantiqni har safar qo'lda yozish — xatolarga moyil. 32-darsda TanStack Query ning useInfiniteQuery si hammasini (kesh, takroriy so'rovlar, yangilash) o'zi hal qiladi.
Kursor asosida sahifalash
Lentaga doim yangi yangiliklar qo'shilib turadi. sahifa=2 so'raganingizda, birinchi sahifadagi elementlar siljigan bo'ladi — takrorlanishlar paydo bo'ladi. Yaxshiroq: "oxirgi ko'rganim — id 4821, undan eskilarini ber":
GET /yangiliklar?oldin=4821&hajm=20
Bu MySQL kursidagi keyset sahifalash (11-dars) — mobil lentalar uchun standart.
Pastga tortib yangilash + cheksiz scroll
<FlatList
...
refreshing={yangilanmoqda}
onRefresh={async () => {
setYangilanmoqda(true);
setTugadi(false);
// birinchi sahifani qaytadan yuklab, ro'yxatni almashtirish
setYangilanmoqda(false);
}}
onEndReached={yukla}
/>
Tepada — eng yangilarini olish, pastda — eskilarini yuklash. Har qanday ijtimoiy tarmoq ilovasining asosiy tuzilmasi.
Nima uchun shunday?
Nima uchun onEndReachedThreshold bor — oxiriga yetganda yuklash yetarli emasmi?
Chunki tarmoq so'rovi vaqt oladi — 200 ms dan bir necha soniyagacha. Agar yuklash faqat oxirida boshlansa, foydalanuvchi har 20 ta yangilikdan keyin to'xtab, spinner ni kutadi. Oldindan yuklash (prefetch) — foydalanuvchi oxirgi elementlarni o'qiyotganda keyingi sahifa allaqachon kelayotgan bo'ladi. Natijada lenta "cheksiz" va uzluksiz tuyuladi. Instagram, TikTok aynan shunday ishlaydi.
Xulosa
Eslab qoling:
SectionList—sections={[{ ...sarlavha, data: [...] }]},renderSectionHeaderstickySectionHeadersEnabled— sarlavha tepada yopishib turadi- Oddiy massivni
Mapbilan guruhlash,useMemoda onEndReached+onEndReachedThreshold={0.5}— cheksiz scrollyuklanmoqdavatugaditekshiruvi — takroriy yuklashdan himoya majburiy- Doim yangilanadigan lentalar — kursor (keyset) sahifalash
- Real ilovada — TanStack Query
useInfiniteQuery(32-dars)
Keyingi dars
21-dars: Formalar — React Hook Form bilan