Бесконечная прокрутка — это способ реализации нумерации страниц на мобильных устройствах. Это распространено среди мобильных интерфейсов из-за ограниченного количества места. Если вы используете приложения для социальных сетей, такие как Instagram или Twitter, эта реализация обычно используется в этих приложениях.

В этом руководстве давайте узнаем, как реализовать бесконечную прокрутку с помощью компонента FlatList в React Native. Для получения данных мы будем использовать настоящий сервис REST API, предоставляемый RAWG. Это одна из крупнейших баз данных видеоигр, и у них есть бесплатный уровень, когда дело доходит до использования их API для личных или хобби-проектов. Затем библиотека React Query поможет нам сделать процесс получения данных намного более плавным.

Предпосылки

Чтобы следовать этому руководству, убедитесь, что в вашей локальной среде разработки установлены следующие инструменты и утилиты, и у вас есть доступ к службам, упомянутым ниже:

  • Установлен Node.js версии 12.x.x или выше
  • Иметь доступ к одному менеджеру пакетов, такому как npm, yarn или npx.
  • RAWG API-ключ

Вы также можете проверить полный исходный код этого примера в этом репозитории GitHub.

Создание нового приложения React Native

Чтобы создать новое приложение React Native, давайте сгенерируем проект с помощью инструмента командной строки create-react-native-app. Этот инструмент помогает создавать универсальные приложения React Native, поддерживает React Native Web, и вы можете использовать нативные модули. В настоящее время он поддерживается потрясающей командой Expo.

Откройте окно терминала и выполните следующую команду:

npx create-react-native-app
# when prompted following questions
What is your app named? infinite-scroll-with-react-query
How would you like to start › Default new app
# navigate inside the project directory after it has been created
cd infinite-scroll-with-react-query

Затем давайте установим все зависимости, которые будут использоваться для создания демонстрационного приложения. В том же окне терминала:

yarn add native-base react-query && expo install expo-linear-gradient react-native-safe-area-context react-native-svg

Эта команда должна загрузить все необходимые зависимости. Чтобы запустить приложение в исходном состоянии, вы можете выполнить любую из следующих команд (в зависимости от используемой мобильной ОС). Эти команды создадут приложение.

# for iOS
yarn ios
# for android
yarn android

Создание домашнего экрана

Давайте создадим новый каталог с именем /src. Этот каталог будет содержать весь код, относящийся к демонстрационному приложению. Внутри него создайте подкаталог с именем /screens, который будет содержать файл компонента HomeScreen.js.

В этот файл давайте добавим код JSX для отображения заголовка экрана приложения.

import React from 'react';
import { Box, Text, Divider } from 'native-base';
export const HomeScreen = () => {
  return (
    <Box flex={1} safeAreaTop backgroundColor='white'>
      <Box height={16} justifyContent={'center'} px={2}>
        <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
          Explore Games
        </Text>
      </Box>
      <Divider />
    </Box>
  );
};

Компонент Box из NativeBase является универсальным компонентом. Он поставляется со многими реквизитами, некоторые из них предназначены для применения SafeAreaView устройства. Свойство safeAreaTop применяет отступы от верхней части экрана устройства. Одним из преимуществ использования библиотеки NativeBase является то, что ее встроенные компоненты предоставляют реквизиты, такие как обработка представлений безопасной области.

Большинство компонентов NativeBase также используют служебные реквизиты для наиболее часто используемых стилизованных свойств, таких как justifyContent, backgroundColor и т. д., и сокращения для этих служебных реквизитов, такие как px для заполнения по горизонтали.

Настройка провайдеров

Обе библиотеки NativeBase и React Query требуют, чтобы их соответствующие поставщики были настроены в корне приложения. Откройте файл App.js и добавьте следующее:

import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NativeBaseProvider } from 'native-base';
import { QueryClient, QueryClientProvider } from 'react-query';
import { HomeScreen } from './src/screens/HomeScreen';
const queryClient = new QueryClient();
export default function App() {
  return (
    <>
      <StatusBar style='auto' />
      <NativeBaseProvider>
        <QueryClientProvider client={queryClient}>
          <HomeScreen />
        </QueryClientProvider>
      </NativeBaseProvider>
    </>
  );
}

Все провайдеры должны оборачивать точку входа или первый экран приложения. В приведенном выше фрагменте есть только один экран, поэтому все провайдеры обертывают HomeScreen.

Компонент QueryClientProvider предоставляет экземпляр в виде QueryClient, который в дальнейшем можно использовать для взаимодействия с кешем.

После изменения файла App.js вы получите следующий вывод на устройстве:

Добавьте базовый URL-адрес для использования RAWG REST API.

Если вы хотите продолжить чтение этого поста и строить вместе с демо-приложением, убедитесь, что у вас есть доступ к ключу API для вашей учетной записи RAWG. Как только вы это сделаете, создайте новый файл с именем index.js внутри каталога /src/config. Этот файл будет экспортировать базовый URL-адрес API и ключ API.

const BASE_URL = 'https://api.rawg.io/api';
// Replace the Xs below with your own API key
const API_KEY = 'XXXXXX';
export { BASE_URL, API_KEY };

Замените крестики в приведенном выше фрагменте своим собственным ключом API.

Получение данных из API

Для получения данных мы будем использовать метод JavaScript fetch API. Создайте новый файл с именем index.js внутри /src/api. Он импортирует базовый URL-адрес и ключ API из каталога /config и предоставляет функцию, которая извлекает данные.

import { BASE_URL, API_KEY } from '../config';
export const gamesApi = {
  // later convert this url to infinite scrolling
  fetchAllGames: () =>
    fetch(`${BASE_URL}/games?key=${API_KEY}`).then(res => {
      return res.json();
    })
};

Затем в файле HomeScreen.js импортируйте хук React Query с именем useQuery. Этот хук принимает два аргумента. Первый аргумент — это уникальный ключ. Этот ключ представляет собой уникальный идентификатор в виде строки, он отслеживает результат запроса и кэширует его.

Второй аргумент — это функция, которая возвращает обещание. Это обещание разрешается, когда есть данные, или выдает ошибку, когда что-то не так при извлечении данных. Мы уже создали функцию обещания, которая асинхронно извлекает данные из базового URL-адреса API в виде gamesApi.fetchAllGames(). Давайте также импортируем gamesApi.

Внутри HomeScreen давайте вызовем этот хук, чтобы получить данные.

import React from 'react';
import { Box, Text, FlatList, Divider, Spinner } from 'native-base';
import { useQuery } from 'react-query';
import { gamesApi } from '../api';
export const HomeScreen = () => {
  const { isLoading, data } = useQuery('games', gamesApi.fetchAllGames);
  const gameItemExtractorKey = (item, index) => {
    return index.toString();
  };
  const renderData = item => {
    return (
      <Text fontSize='20' py='2'>
        {item.item.name}
      </Text>
    );
  };
  return isLoading ? (
    <Box
      flex={1}
      backgroundColor='white'
      alignItems='center'
      justifyContent='center'
    >
      <Spinner color='emerald.500' size='lg' />
    </Box>
  ) : (
    <Box flex={1} safeAreaTop backgroundColor='white'>
      <Box height={16} justifyContent={'center'} px={2}>
        <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
          Explore Games
        </Text>
      </Box>
      <Divider />
      <Box px={2}>
        <FlatList
          data={data.results}
          keyExtractor={gameItemExtractorKey}
          renderItem={renderData}
        />
      </Box>
    </Box>
  );
};

В приведенном выше фрагменте обратите внимание, что React Query поставляется с реализацией состояний запроса, таких как isLoading. Состояние isLoading подразумевает, что данных нет и в настоящее время находится в состоянии «выборки». Чтобы улучшить взаимодействие с пользователем, пока состояние isLoading истинно, может отображаться индикатор загрузки или компонент счетчика (как это сделано в приведенном выше фрагменте с использованием компонента Spinner из NativeBase).

Вот результат после этого шага:

Добавление разбивки на страницы в запрос API

Хук useInfiniteQuery, предоставляемый библиотекой React Query, представляет собой модифицированную версию хука useQuery. В дополнение к состояниям запроса, таким как isLoading и data, он использует функцию для получения номера следующей страницы с помощью getNextPageParam.

В случае RAWG REST API выборка данных по каждому запросу содержит следующие ключи:

  • count: общее количество игр.
  • next: URL-адрес следующей страницы.
  • previous: URL-адрес предыдущей страницы. null, если текущая страница первая.
  • results: массив элементов на отдельной странице.

Имена ключей next и previous будут зависеть от структуры ответа запроса API. Обязательно проверьте свой ответ данных, какие имена ключей и каковы их значения.

В настоящее время запрос API, сделанный в файле /api/index.js, не учитывает номер текущей страницы. Измените, как показано ниже, чтобы получить данные на основе номера страницы.

export const gamesApi = {
  // later convert this url to infinite scrolling
  fetchAllGames: ({ pageParam = 1 }) =>
    fetch(`${BASE_URL}/games?key=${API_KEY}&page=${pageParam}`).then(res => {
      return res.json();
    })
};

Добавление &page=${pageParam} в приведенном выше фрагменте показывает, как функция getNextPageParam будет переходить на следующую страницу, если номер текущей страницы передается в конечной точке запроса. Первоначально значение pageParam равно 1.

Использование хука useInfiniteQuery

Давайте импортируем хук useInfiniteQuery в файл HomeScreen.js.

// rest of the import statements remain same
import { useInfiniteQuery } from 'react-query';

Затем внутри компонента HomeScreen замените крючок useQuery на крючок useInfiniteQuery, как показано ниже. Наряду с двумя аргументами новый хук также будет содержать объект в качестве третьего аргумента. Этот объект содержит логику для получения данных со следующей страницы с помощью функции getNextPageParam.

Функция извлекает номер страницы следующей страницы. Он принимает параметр lastPage, содержащий ответ на последний запрос. Согласно структуре ответа, которую мы обсуждали ранее в предыдущем разделе, проверьте значение lastPage.next. Если это не null, вернуть номер следующей страницы. Если это null, вернуть ответ из последнего запроса.

const { isLoading, data, hasNextPage, fetchNextPage } = useInfiniteQuery(
  'games',
  gamesApi.fetchAllGames,
  {
    getNextPageParam: lastPage => {
      if (lastPage.next !== null) {
        return lastPage.next;
      }
      return lastPage;
    }
  }
);

Реализация бесконечной прокрутки в FlatList

В предыдущем фрагменте hasNextPage и fetchNextPage необходимы. hasNextPage содержит логическое значение. Если это true, это означает, что можно получить больше данных. fetchNextPage — это функция, предоставляемая useInfiniteQuery для получения данных следующей страницы.

Добавьте метод дескриптора внутри компонента HomeScreen с именем loadMore. Эта функция будет использоваться в свойстве FlatList под названием onEndReached. Это свойство вызывается, когда положение прокрутки достигает порогового значения.

const loadMore = () => {
  if (hasNextPage) {
    fetchNextPage();
  }
};

Еще одно различие между useInfiniteQuery и useQuery заключается в том, что структура ответа первого включает массив выбранных страниц в виде data.pages. Используя функцию JavaScript map, получите массив results каждой страницы.

Измените компонент FlatList, как показано ниже:

<FlatList
  data={data.pages.map(page => page.results).flat()}
  keyExtractor={gameItemExtractorKey}
  renderItem={renderData}
  onEndReached={loadMore}
/>

Вот результат после этого шага. Обратите внимание на индикатор прокрутки в правой части экрана. Как только он достигает чуть ниже половины списка, он меняет свое положение. Это изменение положения указывает на то, что данные со следующей страницы извлекаются хуком useInfiniteQuery.

Значение порога по умолчанию — 0.5. Это означает, что loadMore сработает при полувидимой длине списка. Чтобы изменить это значение, вы можете добавить еще один реквизит, onEndReachedThreshold. Он принимает значение от 0 до 1, где 0 — это конец списка.

<FlatList
  data={data.pages.map(page => page.results).flat()}
  keyExtractor={gameItemExtractorKey}
  renderItem={renderData}
  onEndReached={loadMore}
  onEndReachedThreshold={0.3}
/>

Отображение счетчика при получении данных следующей страницы

Еще один способ улучшить взаимодействие с пользователем — когда достигнут конец списка, а данные следующей страницы все еще извлекаются (скажем, сеть слабая). Пока пользователь приложения ожидает данных, полезно отображать индикатор загрузки.

Хук useInfiniteQuery обеспечивает состояние, называемое isFetchingNextPage. Его значение будет истинным, когда данные со следующей страницы будут получены с использованием fetchNextPage.

Измените компонент HomeScreen, как показано ниже. Счетчик загрузки отображается, когда значение isFetchingNextPage равно true. ListFooterComponent в компоненте FlatList используется для отображения индикатора загрузки в конце элементов списка.

export const HomeScreen = () => {
  const { isLoading, data, hasNextPage, fetchNextPage, isFetchingNextPage } =
    useInfiniteQuery('games', gamesApi.fetchAllGames, {
      getNextPageParam: lastPage => {
        if (lastPage.next !== null) {
          return lastPage.next;
        }
        return lastPage;
      }
    });
  const loadMore = () => {
    if (hasNextPage) {
      fetchNextPage();
    }
  };
  const renderSpinner = () => {
    return <Spinner color='emerald.500' size='lg' />;
  };
  const gameItemExtractorKey = (item, index) => {
    return index.toString();
  };
  const renderData = item => {
    return (
      <Box px={2} mb={8}>
        <Text fontSize='20'>{item.item.name}</Text>
      </Box>
    );
  };
  return isLoading ? (
    <Box
      flex={1}
      backgroundColor='white'
      alignItems='center'
      justifyContent='center'
    >
      <Spinner color='emerald.500' size='lg' />
    </Box>
  ) : (
    <Box flex={1} safeAreaTop backgroundColor='white'>
      <Box height={16} justifyContent={'center'} px={2}>
        <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
          Explore Games
        </Text>
      </Box>
      <Divider />
      <Box px={2}>
        <FlatList
          data={data.pages.map(page => page.results).flat()}
          keyExtractor={gameItemExtractorKey}
          renderItem={renderData}
          onEndReached={loadMore}
          onEndReachedThreshold={0.3}
          ListFooterComponent={isFetchingNextPage ? renderSpinner : null}
        />
      </Box>
    </Box>
  );
};

Вот результат:

Подведение итогов

В этом руководстве вы успешно реализовали бесконечную прокрутку, используя useInfiniteQuery из React Query. Использование этой библиотеки для извлечения данных и управления ими внутри приложения React Native устраняет множество проблем. Обязательно ознакомьтесь с документацией Infinite Queries здесь.

Вы также можете проверить полный исходный код этого примера в этом репозитории GitHub.

Наконец, не забывайте уделять особое внимание, если вы разрабатываете коммерческие приложения React Native, содержащие конфиденциальную логику. Вы можете защитить их от кражи кода, подделки и обратного проектирования, следуя нашему руководству.

Первоначально опубликовано в Блоге Jscrambler Аманом Митталом.