На этот раз мы собираемся реализовать бесконечную прокрутку с помощью 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 и других библиотек!
- Просмотр карточек.
- Загружайте больше карточек при прокрутке.
- Клонируйте репозиторий (у вас должен быть установлен Git).
git clone https://github.com/Franklin361/infinite-scroll
- Установите зависимости проекта.
npm install
- Запустите проект.
npm run dev