Создание бесконечной прокрутки с помощью 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 with 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 и внутри нее создайте следующие файлы:

  • Загрузка.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 — опции: Среди опций нам нужна одна под названием 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
Войдите в полноэкранный режим Выход из полноэкранного режима

queryFn получает несколько параметров, среди которых 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
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь последний параметр — это опции, которые являются объектом, который мы будем использовать свойством 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).

Внутри функции, поскольку Rick and Morty 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 — Мы оценим, есть ли больше страниц.

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

  • Если true, то мы возвращаем 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, и делаем его булевым с помощью !!, потому что по умолчанию оно не определено.

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

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

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

Мы создадим хранимую переменную, которая будет меняться каждый раз при изменении свойства данных 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 мы будем итерироваться по запомненным символам константы, оценивая, существует ли она.

<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
Добавить комментарий