Бесконечная прокрутка GraphQL
Вы когда-нибудь задумывались, как улучшить свой веб-сайт с помощью функции бесконечной прокрутки, которая будет поддерживать бесконечную вовлеченность ваших пользователей?
В этой статье мы разберем процесс создания бесконечной прокрутки с помощью GraphQL и React. К концу этого руководства вы сможете реализовать его в своих собственных веб-проектах.
Список сообщений
Мы реализуем простой список сообщений, который послужит примером для этой статьи. Мы будем использовать этот API для получения фиктивных сообщений.
В нашем случае мы хотим показать 100 самых рейтинговых публикаций. Изначально мы не хотим загружать все публикации одновременно. Было бы более уместно загрузить несколько сообщений, и когда пользователь меньше всего прокручивает вниз, мы загружаем еще несколько и так далее. В этом и состоит цель бесконечной прокрутки: избавить пользователей от первоначальной полной загрузки страницы.
Итак, у нас есть код, который отображает страницу, на которой мы показываем все сообщения.
import Spinner from '~/components/Spinner'
import useLayout from './hooks'
import Post from './Post'
import {
Container,
Content,
Description,
Header,
List,
PageTitle,
Title,
} from './styles'
const Layout = () => {
const { loading, posts, thresholdElementRef } = useLayout()
return (
<Container>
<PageTitle>Infinite Scroll</PageTitle>
<Content>
<Header>
<Title>100 Most rated posts</Title>
<Description>Check them all by scrolling down!</Description>
</Header>
{loading ? (
<Spinner />
) : (
<List>
{posts.map(({ id, title }, index) => (
<Post
id={id}
key={id}
ref={
index === posts.length - 1 ? thresholdElementRef : undefined
}
title={title}
/>
))}
</List>
)}
</Content>
</Container>
)
}
export default Layout
Мы используем собственный хук useLayout
, откуда извлекаем три вещи:
loading
: не имеет отношения к данной статье. Просто сообщает нам, загружается ли запрос.posts
: сообщения, которые отображаются.thresholdElementRef
: ссылка, прикрепленная к элементу, который будет служить порогом. В нашем случае этим элементом будет последняя запись.
Давайте посмотрим на наш useLayout
.
import usePosts from '~/hooks/usePosts'
import useInfiniteScroll from '~/lib/use-infinite-scroll'
const useLayout = () => {
const { fetchMorePosts, loading, posts } = usePosts()
const { thresholdElementRef } = useInfiniteScroll({
fetchNextPage: fetchMorePosts,
options: { rootMargin: '400px' },
})
return { loading, posts, thresholdElementRef }
}
export default useLayout
Прямо здесь мы создаем файл thresholdElementRef
, используяuseInfiniteScroll
. Эта библиотека получает fetchMorePosts
функцию и некоторые параметры. Давайте разберем все по шагам.
Библиотека useInfiniteScroll
Эта библиотека позволит нам получить ссылку на порог, которую мы присвоим элементу, который будет служить порогом.
import useIntersectedElement from '../use-intersected-element'
import { UseInfiniteScrollProps } from './types'
const useInfiniteScroll = <ThresholdElement extends Element = Element>({
fetchNextPage,
options,
}: UseInfiniteScrollProps) => {
const { thresholdElementRef } = useIntersectedElement<ThresholdElement>({
callback: fetchNextPage,
options,
})
return { thresholdElementRef }
}
export default useInfiniteScroll
Эта библиотека просто передает полученные реквизиты в другую библиотеку: useIntersectedElement
.
import { useEffect, useMemo, useState } from 'react'
import { UseIntersectedElementProps } from './types'
const useIntersectedElement = <ThresholdElement extends Element = Element>({
callback,
options,
}: UseIntersectedElementProps) => {
const [thresholdElement, thresholdElementRef] =
useState<ThresholdElement | null>(null)
const observer = useMemo(
() =>
new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return
callback()
}, options),
[callback, options],
)
useEffect(() => {
if (!thresholdElement) return
observer.observe(thresholdElement)
return () => {
observer.unobserve(thresholdElement)
}
}, [observer, thresholdElement])
return { thresholdElementRef }
}
export default useIntersectedElement
export type { UseIntersectedElementProps }
Здесь мы создаем состояние, установщиком которого является ссылка, о которой мы говорили. Помните, что в конечном итоге этот пороговый элемент будет последним постом в списке, как мы упоминали ранее.
Затем мы используем IntersectionObserver
для создания наблюдателя, который будет выполнять callback
функцию, полученную в качестве аргумента (fetchMorePosts
функцию в useLayout
хуке). Если entry
(целевой элемент) не был пересечен, мы ничего не выполняем.
Он также может получить некоторые опции. В нашем случае (useLayout
хук) мы используем только rootMargin: '400px'
, которая увеличивают размер ограничивающей рамки корневого элемента перед вычислением пересечений. В случае возникновения сомнений вы можете просмотреть документацию.
Наконец, в useEffect
мы наблюдаем пороговый элемент и не наблюдаем его, когда компонент размонтируется.
fetchMorePosts
Какое отношение ко всему этому имеет GraphQL? Ну, вот оно!
import { useQuery } from '@apollo/client'
import { useCallback, useMemo } from 'react'
import POSTS from '~/graphql/queries/posts'
import { PostsQuery, PostsQueryVariables } from '~/graphql/types'
import Post from '~/models/post'
import { MAX_NUMBER_OF_POSTS, POSTS_LIMIT } from './constants'
const usePosts = () => {
const { data, fetchMore, loading } = useQuery<
PostsQuery,
PostsQueryVariables
>(POSTS, {
variables: {
options: { paginate: { limit: POSTS_LIMIT, page: 1 } },
},
})
const posts = useMemo(
() => (data ? data.posts?.data?.map(Post.fromDto) ?? [] : []),
[data],
)
const fetchMorePosts = useCallback(() => {
if (posts.length === MAX_NUMBER_OF_POSTS) return
fetchMore({
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev
return Object.assign({}, prev, {
posts: {
data: [
...(prev.posts?.data ?? []),
...(fetchMoreResult.posts?.data ?? []),
],
},
})
},
variables: {
options: { paginate: { page: posts.length / POSTS_LIMIT + 1 } },
},
})
}, [fetchMore, posts])
return { fetchMorePosts, loading, posts }
}
export default usePosts
Нам нужно использовать useQuery
для получения данных из API. Важно, чтобы API поддерживал пагинацию. Пагинация — это процесс, используемый для разделения большого набора данных на более мелкие фрагменты (страницы). Это позволит нам запрашивать «страницу» для каждого запроса (в конечном итоге позволяя реализовать бесконечную прокрутку). В нашем случае мы запросим 10 сообщений на «страницу». Мы будем хранить все эти сообщения в формате posts
.
Итак, у нас есть fetchMorePosts
функция. Он использует fetchMore
функцию, которая позволяет отправлять последующие запросы на наш сервер GraphQL для получения дополнительных страниц.
Поведение будет следующим: если мы достигли максимального количества сообщений, мы прекращаем запрашивать данные. Если нет, fetchMorePosts
запросит следующие 10 постов, добавив их в posts
.
Более подробную информацию о пагинации GraphQL вы можете найти в официальной документации.
Демонстрация
Мы сделали это! Теперь у нас есть бесконечная прокрутка в списке сообщений.
Итак, подведем итог:
- Устанавливаем пороговую ссылку на последний отображаемый пост (сначала это будет 10-й, затем 20-й и т. д.)
- Мы устанавливаем обратный вызов
fetchMorePosts
на пороговое значение и выполняем его при достижении порога. fetchMorePosts
будет запрашивать 10 постов одновременно, добавляя их к уже запрошенным, пока не будет достигнут лимит.
Весь код проекта доступен в этом репозитории Github.
Последние мысли
Решение использовать GraphQL не должно сводиться к добавлению бесконечной прокрутки. Это более серьезное решение. Однако GraphQL — это универсальный инструмент, который позволяет нам применять и реализовывать некоторые замечательные вещи, и бесконечная прокрутка не является исключением.