SectionList va cheksiz scroll (onEndReached)

~15 daq · React Native · Ro'yxatlar va formalar

Muammodan boshlaylik

Ikki vazifa:

  1. Kontaktlar ro'yxati — harflar bo'yicha guruhlangan: "A", "B", "D"... va scroll paytida joriy harf tepada yopishib turishi kerak
  2. 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: [...] } massivi
  • renderSectionHeader — bo'lim sarlavhasi
  • stickySectionHeadersEnabled — 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 chaqiriladi
  • onEndReachedThreshold={0.5} — oxiriga yarim ekran qolganda. Foydalanuvchi oxiriga yetguncha yangi ma'lumot yuklanib bo'ladi — kutish sezilmaydi
  • Oxirida — spinner yoki "boshqa yo'q"

[!WARNING] onEndReached bir necha marta ketma-ket chaqirilishi mumkin — ayniqsa ro'yxat qisqa bo'lsa yoki tez scroll qilinsa. yuklanmoqda tekshiruvisiz bir xil sahifa ikki marta yuklanadi va elementlar takrorlanadi (key ogohlantirishi 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: [...] }]}, renderSectionHeader
  • stickySectionHeadersEnabled — sarlavha tepada yopishib turadi
  • Oddiy massivni Map bilan guruhlash, useMemo da
  • onEndReached + onEndReachedThreshold={0.5} — cheksiz scroll
  • yuklanmoqda va tugadi tekshiruvi — 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