Урок 08

ScrollView и FlatList

Контент может не влезать на экран. Для прокрутки — ScrollView и FlatList.

ScrollView

Для небольшого количества контента:

import { ScrollView, Text, View } from 'react-native';

<ScrollView>
    <Text>Строка 1</Text>
    <Text>Строка 2</Text>
    {/* ... */}
</ScrollView>

Свойства:

<ScrollView
    horizontal
    showsVerticalScrollIndicator={false}
    contentContainerStyle={{ padding: 16 }}
    onScroll={(e) => console.log(e.nativeEvent.contentOffset.y)}
    scrollEventThrottle={16}
    refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
    {/* контент */}
</ScrollView>

Проблема: ScrollView рендерит всё сразу. Если элементов 1000 — приложение упадёт или будет тормозить.

FlatList

Для больших списков. Рендерит только видимые элементы (виртуализация).

import { FlatList, Text, View } from 'react-native';

<FlatList
    data={items}
    keyExtractor={(item) => item.id.toString()}
    renderItem={({ item }) => (
        <View>
            <Text>{item.title}</Text>
        </View>
    )}
/>

Обязательные пропсы:

  • data — массив
  • renderItem — функция рендера элемента
  • keyExtractor — уникальный ключ

Разделители

<FlatList
    data={items}
    ItemSeparatorComponent={() => <View style={styles.separator} />}
    ListHeaderComponent={<Text>Заголовок</Text>}
    ListFooterComponent={<Text>Конец</Text>}
    ListEmptyComponent={<Text>Пусто</Text>}
/>

Pull to refresh

const [refreshing, setRefreshing] = useState(false);

async function onRefresh() {
    setRefreshing(true);
    await loadData();
    setRefreshing(false);
}

<FlatList
    data={items}
    refreshing={refreshing}
    onRefresh={onRefresh}
    renderItem={...}
/>

Пагинация

<FlatList
    data={items}
    onEndReached={() => loadMore()}
    onEndReachedThreshold={0.5}
    ListFooterComponent={isLoading ? <ActivityIndicator /> : null}
/>

onEndReachedThreshold — 0.5 означает «сработать за пол-экрана до конца».

Оптимизация

<FlatList
    data={items}
    renderItem={renderItem}
    keyExtractor={keyExtractor}
    initialNumToRender={10}
    maxToRenderPerBatch={10}
    windowSize={5}
    removeClippedSubviews
    getItemLayout={(data, index) => ({
        length: ITEM_HEIGHT,
        offset: ITEM_HEIGHT * index,
        index,
    })}
/>
  • initialNumToRender — сколько рендерить сразу
  • maxToRenderPerBatch — за раз
  • windowSize — сколько экранов вокруг
  • removeClippedSubviews — удалять невидимые
  • getItemLayout — если элементы одинаковой высоты

useCallback

renderItem и keyExtractor должны быть мемоизированы:

const renderItem = useCallback(
    ({ item }: { item: Item }) => <ItemCard item={item} />,
    []
);

const keyExtractor = useCallback(
    (item: Item) => item.id.toString(),
    []
);

SectionList

Секции с заголовками:

import { SectionList } from 'react-native';

<SectionList
    sections={[
        { title: 'A', data: ['Алиса', 'Антон'] },
        { title: 'Б', data: ['Борис', 'Богдан'] },
    ]}
    keyExtractor={(item, index) => item + index}
    renderItem={({ item }) => <Text>{item}</Text>}
    renderSectionHeader={({ section }) => <Text>{section.title}</Text>}
/>

Если ты уже знаешь React

В вебе — .map внутри <div>. В React Native — FlatList. Это не просто оптимизация: без виртуализации длинные списки не работают.

Итоги

  • ScrollView — для малого контента
  • FlatList — для списков
  • keyExtractor обязателен
  • onEndReached для пагинации
  • renderItem мемоизируй
  • SectionList для секций