DevGang
Авторизоваться

Бесконечная прокрутка GraphQL

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

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

Список сообщений

Мы реализуем простой список сообщений, который послужит примером для этой статьи. Мы будем использовать этот API для получения фиктивных сообщений.

В нашем случае мы хотим показать 100 самых рейтинговых публикаций. Изначально мы не хотим загружать все публикации одновременно. Было бы более уместно загрузить несколько сообщений, и когда пользователь меньше всего прокручивает вниз, мы загружаем еще несколько и так далее. В этом и состоит цель бесконечной прокрутки: избавить пользователей от первоначальной полной загрузки страницы.

Итак, у нас есть код, который отображает страницу, на которой мы показываем все сообщения.

src/pages/posts/index.tsx
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.

src/pages/posts/hooks.tsx
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

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

src/lib/use-infinite-scroll/index.ts
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.

src/lib/use-intersected-element/index.ts
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? Ну, вот оно!

src/hooks/usePosts/index.ts
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 — это универсальный инструмент, который позволяет нам применять и реализовывать некоторые замечательные вещи, и бесконечная прокрутка не является исключением.

Источник:

#React #GraphQL
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу