Создание бесконечной прокрутки с помощью React JS! ♾️

На этот раз мы собираемся реализовать бесконечную прокрутку с помощью React JS.

Приложение, реализующее бесконечную прокрутку, состоит из макета, который позволяет пользователям продолжать потреблять определенный объем информации без паузы, поскольку контент автоматически загружается по мере прокрутки.

🚨 Примечание: Этот пост требует от вас знания основ React с TypeScript (базовые хуки).

Любой вид обратной связи приветствуется, спасибо и надеюсь, что статья вам понравится.🤗

Оглавление.

📌 Используемые технологии.

📌 Создание проекта.

📌 Первые шаги.

📌 Выполнение запроса API.

📌 Показываем карты.

📌 Создание бесконечной прокрутки.

📌 Рефакторинг.

📌 Заключение.

📌 Живая демонстрация.

📌 Исходный код.

 

🎈 Используемые технологии.

  • ▶️ React JS (версия 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ React Query
  • ▶️ Rick and Morty API
  • ▶️ CSS ваниль (Стили можно найти в репозитории в конце этого поста)

 

🎈 Создание проекта.

Мы назовем проект: infinite-scroll (опционально, вы можете назвать его как угодно).

npm init vite@latest
Вход в полноэкранный режим Выход из полноэкранного режима

Мы создаем проект с помощью Vite JS и выбираем React с TypeScript.

Затем мы выполняем следующую команду для перехода в только что созданную директорию.

cd infinite-scroll
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем мы устанавливаем зависимости.

npm install
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем открываем проект в редакторе кода (в моем случае VS code).

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

 

🎈 Первые шаги.

Сначала в файле src/App.tsx удалим содержимое и добавим заголовок.

const App = () => {
  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выйти из полноэкранного режима

Далее мы создадим два компонента, которые будем использовать позже. Создадим папку src/components и внутри нее создадим следующие файлы:

  • Loading.tsx

Этот файл будет содержать следующее:

export const Loading = () => {
    return (
        <div className="container-loading">
            <div className="spinner"></div>
            <span>Loading more characters...</span>
        </div>
    )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Он будет использоваться для показа спиннера при новом запросе к API.

  • Card.tsx

Этот файл будет содержать следующее:

import { Result } from '../interface';

interface Props {
    character: Result
}
export const Card = ({ character }: Props) => {
    return (
        <div className='card'>
            <img src={character.image} alt={character.name} width={50} loading='lazy' />
            <p>{character.name}</p>
        </div>
    )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Это карточка, которая будет показывать персонажа API Рика и Морти.

В папке src/interfaces создадим файл index.ts и добавим следующие интерфейсы.

export interface ResponseAPI {
    info: Info;
    results: Result[];
}

export interface Info {
    count: number;
    pages: number;
    next: string;
    prev: string;
}

export interface Result {
    id: number;
    name: string;
    image: string;
}
Вход в полноэкранный режим Выход из полноэкранного режима

🚨 Примечание: интерфейс Result на самом деле имеет больше свойств, но в данном случае я буду использовать только те, которые я определил.

 

🎈 Выполнение API запроса.

В этом случае мы будем использовать библиотеку React Query, которая позволит нам выполнять запросы более удобным способом (а также имеет другие возможности, такие как управление кэшем).

  • Установите зависимость
npm i @tanstack/react-query
Войдите в полноэкранный режим Выйти из полноэкранного режима

Затем в файле src/main.tsx мы сделаем следующее:

Мы заключим наш компонент App внутри QueryClientProvider и отправим ему клиента, который является просто новым экземпляром QueryClient.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
)

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

Теперь в файле src/App.tsx мы будем использовать специальный хук React Query под названием useInfiniteQuery.

const App = () => {

  useInfiniteQuery()

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выход из полноэкранного режима

Хуку useInfiniteQuery необходимо несколько параметров:

1 — queryKey: массив строк или вложенных объектов, который используется в качестве ключа для управления хранением кэша.

2 — queryFn: функция, которая возвращает обещание, обещание должно быть разрешено или выбросить ошибку.

3 — options: среди опций нам нужна одна под названием getNextPageParam, которая представляет собой функцию, возвращающую информацию для следующего запроса к API.

Первым параметром является queryKey, в данном случае мы помещаем массив со словом ‘characters’.

const App = () => {

  useInfiniteQuery(
        ['characters']
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выйти из полноэкранного режима

Вторым параметром является queryFn, в данном случае мы помещаем массив со словом ‘characters’.

Сначала мы передаем ему функцию

const App = () => {

  useInfiniteQuery(
        ['characters'],
        () => {}
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Войти в полноэкранный режим Выйти из полноэкранного режима

Эта функция должна вернуть разрешенное обещание.
Для этого вне компонента мы создаем функцию, которая будет получать в качестве параметра страницу для выборки и возвращать обещание типа ResponseAPI.

import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {

  useInfiniteQuery(
        ['characters'],
        () => fetcher()
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выход из полноэкранного режима

ЗапросFn получает несколько параметров, среди которых pageParam, который по умолчанию будет неопределенным, а затем число, поэтому если значение не существует, мы приравняем его к 1, и это свойство мы передадим функции fetcher.

import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {

  useInfiniteQuery(
        ['characters'],
        ({ pageParam = 1 }) => fetcher(pageParam),
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь последний параметр — options, который является объектом, который мы будем использовать свойством getNextPageParam.

import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {

  useInfiniteQuery(
        ['characters'],
        ({ pageParam = 1 }) => fetcher(pageParam),
        {
            getNextPageParam: () => {}
        }
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выйти из полноэкранного режима

Функция getNextPageParam получает два параметра, но мы будем использовать только первый, который является последней полученной страницей (то есть последним ответом, который дал нам API).

Внутри функции, поскольку API Рика и Морти не поставляется со следующей страницей (скорее он поставляется с url для следующей страницы), мы должны будем сделать следующее:

1 — Мы получим предыдущую страницу.

В ответе API приходит свойство info, которое содержит свойство prev, мы оцениваем, существует ли оно (потому что в первом вызове свойство prev равно null).

  • Если оно не существует, то это страница 0.
  • Если оно существует, то мы получаем строку, разделяем ее и получаем номер.
const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
Вход в полноэкранный режим Выход из полноэкранного режима

2 — Мы получим текущую страницу.

Мы просто добавляем предыдущую страницу плюс 1.

const currentPage = previousPage + 1;
Вход в полноэкранный режим Выход из полноэкранного режима

3 — Оценим, есть ли еще страницы.

Мы оцениваем, равна ли текущая страница общему количеству страниц.

  • Если это так, то возвращаем false, чтобы не делать повторный запрос.

  • Если false, то возвращаем следующую страницу, которая является результатом суммы текущей страницы плюс 1.

if ( currentPage === lastPage.info.pages) return false;

return currentPage + 1;
Вход в полноэкранный режим Выход из полноэкранного режима

А вот как будет выглядеть хук.

import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {

  useInfiniteQuery(
        ['characters'],

        ({ pageParam = 1 }) => fetcher(pageParam),

        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выйти из полноэкранного режима

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

  • data: объект, содержащий запрос API.

    • Внутри этого свойства есть еще одно, называемое pages, которое представляет собой массив, содержащий полученные страницы, отсюда мы будем получать данные API.
  • error (ошибка): сообщение об ошибке, возникающее при неудачном выполнении API-запроса.

  • fetchNextPage: функция, позволяющая сделать новый запрос к следующей странице API.

  • status: строка, содержащая значения «error» | «loading» | «success», указывающая на статус запроса.

  • hasNextPage: булево значение, которое является истинным, если функция getNextPageParam возвращает значение, которое не является неопределенным.

import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {

  const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
        ['characters'],

        ({ pageParam = 1 }) => fetcher(pageParam),

        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выход из полноэкранного режима

 

🎈 Показываем карты.

Теперь мы можем показать результаты благодаря тому, что у нас уже есть доступ к данным.

Мы создадим div и внутри него сделаем итерацию по свойству data с доступом к свойству page, которое является массивом, к которому на данный момент мы будем обращаться к первой позиции и к результатам.

Кроме того, мы оцениваем статус и, если он загружается, показываем компонент Loading.tsx, но если он находится в состоянии ошибки, мы размещаем сообщение об ошибке.

import { ResponseAPI } from "./interface"

const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {

  const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
        ['characters'],

        ({ pageParam = 1 }) => fetcher(pageParam),

        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

  if (status === 'loading') return <Loading />

  if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>

  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>

      <div className="grid-container">
        {
          data?.pages[0].results.map(character => (
            <Card key={character.id} character={character} />
          ))
        }
      </div>

    </div>
  )
}
export default App
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь показана только первая страница, дальше мы будем реализовывать бесконечную прокрутку.

 

🎈 Делаем бесконечную прокрутку.

Для этого мы будем использовать популярную библиотеку react-infinite-scroll-component.

Устанавливаем зависимость.

npm i react-infinite-scroll-component
Вход в полноэкранный режим Выход из полноэкранного режима

Сначала нам понадобится компонент InfiniteScroll.

<InfiniteScroll/>
Войти в полноэкранный режим Выход из полноэкранного режима

Этот компонент получит несколько свойств

  • dataLength: Количество элементов, в данный момент мы поместим значение, так как нам необходимо его вычислить.

  • next: функция, которая будет вызвана при достижении конца страницы при прокрутке. Здесь мы вызовем функцию, которую нам предлагает useInfiniteQuery, fetchNextPage.

  • hasMore: булево свойство, указывающее, есть ли еще элементы. Здесь мы вызовем свойство, предлагаемое useInfiniteQuery, hasNextPage, и преобразуем его в boolean с помощью !!, потому что по умолчанию оно не определено.

  • загрузчик: JSX-компонент, который будет использоваться для отображения сообщения о загрузке во время выполнения запроса. Здесь мы будем вызывать компонент Loading.tsx.

<InfiniteScroll
    dataLength={}
    next={() => fetchNextPage()}
    hasMore={!!hasNextPage}
    loader={<Loading />}
/>
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь свойство dataLength, которое мы могли бы использовать, но оно будет показывать только следующую страницу, не накапливая предыдущие результаты, поэтому мы должны сделать следующее:

Создадим хранимую переменную, которая будет меняться каждый раз, когда изменяется свойство data в useInfiniteQuery.

Эта переменная символов должна возвращать новый ResponseAPI, но свойство results должно накапливать предыдущие и текущие символы. А свойство info будет свойством текущей страницы.

const characters = useMemo(() => data?.pages.reduce((prev, page) => {
        return {
            info: page.info,
            results: [...prev.results, ...page.results]
        }
    }), [data])
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы передаем эту константу в dataLength, делаем оценку, если символы существуют, то ставим длину свойства results, если нет, то ставим 0.

<InfiniteScroll
    dataLength={characters ? characters.results.length : 0}
    next={() => fetchNextPage()}
    hasMore={!!hasNextPage}
    loader={<Loading />}
/>
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь внутри компонента мы должны поместить список для рендеринга, вот так:

Теперь вместо итерации по data?.pages[0].results мы будем итерировать по запомненной константе characters, оценивая, существует ли она.

<InfiniteScroll
    dataLength={characters ? characters.results.length : 0}
    next={() => fetchNextPage()}
    hasMore={!!hasNextPage}
    loader={<Loading />}
>
    <div className="grid-container">
        {
            characters && characters.results.map(character => (
                <Card key={character.id} character={character} />
            ))
        }
    </div>
</InfiniteScroll>
Вход в полноэкранный режим Выход из полноэкранного режима

И так все будет завершено:

import { useMemo } from "react";
import InfiniteScroll from "react-infinite-scroll-component"
import { useInfiniteQuery } from "@tanstack/react-query";

import { Loading } from "./components/Loading"
import { Card } from "./components/Card"

import { ResponseAPI } from "./interface"


const fetcher = (page: number): Promise<ResponseAPI> => fetch(`https://rickandmortyapi.com/api/character/?page=${page}`).then(res => res.json())

const App = () => {

  const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
        ['characters'],

        ({ pageParam = 1 }) => fetcher(pageParam),

        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0

                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

    const characters = useMemo(() => data?.pages.reduce((prev, page) => {
        return {
            info: page.info,
            results: [...prev.results, ...page.results]
        }
    }), [data])

  if (status === 'loading') return <Loading />

  if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>


  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>

      <InfiniteScroll
        dataLength={characters ? characters.results.length : 0}
        next={() => fetchNextPage()}
        hasMore={!!hasNextPage}
        loader={<Loading />}
      >
        <div className="grid-container">
          {
            characters && characters.results.map(character => (
              <Card key={character.id} character={character} />
            ))
          }
        </div>
      </InfiniteScroll>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выход из полноэкранного режима

И вот как это будет выглядеть.

 

🎈 Рефакторинг.

Создадим новую папку src/hooks и добавим туда файл useCharacter.ts.

И переносим всю логику.

import { useMemo } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import { ResponseAPI } from "../interface";

export const useCharacter = () => {

    const { data, error, fetchNextPage, status, hasNextPage } = useInfiniteQuery(
        ['characters'],
        ({ pageParam = 1 }) => fetch(`https://rickandmortyapi.com/api/character/?page=${pageParam}`).then(res => res.json()),
        {
            getNextPageParam: (lastPage: ResponseAPI) => {

                const previousPage = lastPage.info.prev ? +lastPage.info.prev.split('=')[1] : 0
                const currentPage = previousPage + 1;

                if (currentPage === lastPage.info.pages) return false;
                return currentPage + 1;
            }
        }
    )

    const characters = useMemo(() => data?.pages.reduce((prev, page) => {
        return {
            info: page.info,
            results: [...prev.results, ...page.results]
        }
    }), [data])

    return {
        error, fetchNextPage, status, hasNextPage,
        characters
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь в src/App.tsx стало легче читать.

import InfiniteScroll from "react-infinite-scroll-component"

import { Loading } from "./components/Loading"
import { Card } from "./components/Card"

import { useCharacter } from './hooks/useCharacter';

const App = () => {
  const { characters, error, fetchNextPage, hasNextPage, status } = useCharacter()

  if (status === 'loading') return <Loading />

  if (status === 'error') return <h4>Ups!, {`${error}` as string}</h4>


  return (
    <div>
      <h1 className="title">React Infinite Scroll</h1>

      <InfiniteScroll
        dataLength={characters ? characters.results.length : 0}
        next={() => fetchNextPage()}
        hasMore={!!hasNextPage}
        loader={<Loading />}
      >
        <div className="grid-container">
          {
            characters && characters.results.map(character => (
              <Card key={character.id} character={character} />
            ))
          }
        </div>
      </InfiniteScroll>
    </div>
  )
}
export default App
Вход в полноэкранный режим Выход из полноэкранного режима

 

🎈 Заключение.

Весь процесс, который я только что показал, является одним из способов быстрой реализации бесконечной прокрутки с помощью сторонних пакетов. ♾️

Надеюсь, я помог вам понять, как реализовать этот дизайн, спасибо большое, что дошли до этого! 🤗❤️

Я приглашаю вас прокомментировать, если вы нашли эту статью полезной или интересной, или если вы знаете какой-либо другой или лучший способ, как реализовать бесконечную прокрутку. 🙌

 

🎈 Живое демо.

https://infinite-scroll-app-fml.netlify.app

 

🎈 Исходный код.

Franklin361 / infinite-scroll

Создание бесконечной прокрутки с помощью react js ♾️

Создание бесконечной прокрутки с помощью React JS! ♾️

На этот раз мы собираемся реализовать макет бесконечной прокрутки с помощью React JS и других библиотек!

 

 

  1. Просмотр карточек.
  2. Загружайте больше карточек при прокрутке.

 

 

  1. Клонируйте репозиторий (у вас должен быть установлен Git).
    git clone https://github.com/Franklin361/infinite-scroll
Войдите в полноэкранный режим Выйдите из полноэкранного режима
  1. Установите зависимости проекта.
    npm install
Войдите в полноэкранный режим Выйдите из полноэкранного режима
  1. Запустите проект.
    npm run dev
Войти в полноэкранный режим Выйти из полноэкранного режима

 

🇲🇽 🔗🇺🇲 🔗

Посмотреть на GitHub

Оцените статью
devanswers.ru
Добавить комментарий