React Query v4 + SSR в Next JS

Механизм SSR для получения данных и кэширования немного сложен в next js.

В этой статье мы узнаем, как улучшить время начальной загрузки с помощью SSR и получить высокоскоростную навигацию на стороне клиента с помощью CSR и React Query.

Мы создадим приложение для блога, используя JSON Placeholder API.

Здесь мы рассмотрим только важные разделы. Чтобы увидеть полный исходный код, посетите репозиторий github. Вы также можете посмотреть демонстрацию в реальном времени, чтобы получить более полное представление. React Query devtools доступен в этой демонстрации, так что вы можете проверить поток кэша.

Оглавление

  • 1. Создайте новый проект
  • 2. Настройте Hydration
  • 3. Предварительная выборка и обезвоживание данных
  • 4. Неглубокая маршрутизация
  • 5. с-CSR HOC
  • 6. обработка кода состояния 404
  • 7. Заключение
  • 8. Ссылки

1. Создание нового проекта

Сначала создайте проект nextjs:

yarn create next-app blog-app

or

npx create-next-app blog-app
Войдите в полноэкранный режим Выйти из полноэкранного режима

Установим React Query и Axios:

yarn add @tanstack/react-query axios

or

npm install @tanstack/react-query axios
Войдите в полноэкранный режим Выйти из полноэкранного режима

2. Настройка гидратации

Благодаря документам react query мы настроим гидратацию в _app.js :

//pages/_app.js

import { useState } from 'react';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from 'lib/react-query-config';

function MyApp({ Component, pageProps }) {

    // This ensures that data is not shared 
    // between different users and requests
    const [queryClient] = useState(() => new QueryClient(config))

    return (
        <QueryClientProvider client={queryClient}>
            // Hydrate query cache
            <Hydrate state={pageProps.dehydratedState}>
                <Component  {...pageProps} />
            </Hydrate>
        </QueryClientProvider>
    )

}

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

3. Предварительная выборка и обезвоживание данных

Прежде чем продолжить, отметим, что в версии 3 React Query по умолчанию кэшировал результаты запроса в течение 5 минут, а затем вручную собирал эти данные. Это значение по умолчанию применялось и к React Query на стороне сервера. Это приводило к большому потреблению памяти и зависанию процессов в ожидании завершения ручной сборки мусора. В версии 4 по умолчанию для серверного cacheTime теперь установлено значение Infinity, что фактически отключает ручную сборку мусора (процесс NodeJS очистит все после завершения запроса).

Теперь нам нужно выполнить предварительную выборку данных и обезвоживание queryClient в методе getServerSideProps:

//pages/posts/[id].js

import { getPost } from 'api/posts';
import { dehydrate, QueryClient } from '@tanstack/react-query';

export const getServerSideProps = async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient()

    // prefetch data on the server
    await queryClient.fetchQuery(['post', id], () => getPost(id))

    return {
        props: {
            // dehydrate query cache
            dehydratedState: dehydrate(queryClient),
        },
    }
}

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

P.S : Мы использовали fetchQuery вместо prefetchQuery, потому что prefetchQuery не выдает ошибку и не возвращает никаких данных. Подробнее об этом мы поговорим в разделе 6. Обработка кода состояния 404.

Теперь мы можем легко использовать эти предварительно полученные данные на нашей странице, не передавая никаких данных по реквизитам.

Для ясности давайте рассмотрим реализацию метода getPost и хука usePost:

//api/posts.js

import axios from 'lib/axios';

export const getPost = async id => {
    const { data } = await axios.get('/posts/' + id);
    return data;
}
Вход в полноэкранный режим Выход из полноэкранного режима
//hooks/api/posts.js

import { useQuery } from '@tanstack/react-query';
import * as api from 'api/posts';

export const usePost = (id) => {
    return useQuery(['post', id], () => api.getPost(id));
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем использовать этот хук usePost для получения данных поста.

//pages/posts/[id].js

import { useRouter } from 'next/router';
import { usePost } from 'hooks/api/posts'
import Loader from 'components/Loader';
import Post from 'components/Post';
import Pagination from 'components/Pagination';

const PostPage = () => {

    const { query: { id } } = useRouter();

    const { data, isLoading } = usePost(id);

    if (isLoading) return <Loader />

    return (
        <>
            <Post id={data.id} title={data.title} body={data.body} />
            <Pagination id={id} />
        </>
    )
}


// getServerSideProps implementation ...
// We talked about it in section 2
Войти в полноэкранный режим Выход из полноэкранного режима

4. Неглубокая маршрутизация

Мы хотим управлять механизмом выборки и кэширования данных только на клиенте, поэтому нам нужно использовать shallow = true prop в компоненте Link для навигации между страницами постов, чтобы не вызывать getServerSideProps каждый раз. Это означает, что метод getServerSideProps будет вызываться только тогда, когда пользователи непосредственно перейдут по URL поста, а не при навигации внутри приложения на стороне клиента.

У нас есть компонент Pagination для навигации между страницами, поэтому мы используем shallow = true здесь:

//components/Pagination.jsx

import Link from 'next/link';

function PaginationItem({ index }) {

    return (
        <Link className={itemClassName} href={'/posts/' + index} shallow={true}>
            {index}
        </Link>
    )
}

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

P.S : Мы использовали новый компонент link в nextjs v12.2, поэтому нам не нужно было использовать здесь тег <a>.

5. with-CSR HOC

На данный момент неглубокая маршрутизация nextjs v12.2 работает только для изменений URL на текущей странице. предостережения по неглубокой маршрутизации nextjs
Это означает, что если вы перейдете от /posts/10 к /posts/15 с shallow = true, то getServerSideProps не будет вызван, но если вы перейдете от /home к /posts/15, то getServerSideProps будет вызван, даже если вы используете неглубокую маршрутизацию, и это приведет к получению ненужных данных, даже если они доступны в кэше.

Я нашел решение, которое проверяет, является ли этот запрос к getServerSideProps запросом навигации на стороне клиента или нет. Если это так, то возвращается пустой объект для props и предотвращается выборка данных на сервере.
Мы не можем предотвратить вызов getServerSideProps при навигации между различными страницами, но мы можем предотвратить выборку ненужных данных в getServerSideProps.

Вот реализация сCSR HOC :

//HOC/with-CSR.js

export const withCSR = (next) => async (ctx) => {

    // check is it a client side navigation 
    const isCSR = ctx.req.url?.startsWith('/_next');

    if (isCSR) {
        return {
            props: {},
        };
    }

    return next?.(ctx)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы должны обернуть наш getServerSideProps этим HOC.

//pages/posts/[id].js

import { getPost } from 'api/posts';
import { dehydrate, QueryClient } from '@tanstack/react-query';
import { withCSR } from 'HOC/with-CSR'

export const getServerSideProps = withCSR(async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient()

    await queryClient.fetchQuery(['post', id], () => getPost(id))

    return {
        props: {
            dehydratedState: dehydrate(queryClient),
        },
    }
})
Вход в полноэкранный режим Выход из полноэкранного режима

Если мы переходим с разных страниц на страницы постов, getServerSideProps не будет получать никаких данных и просто вернет пустой объект для props.

6. Обработка кода состояния 404

Хотя Next.js отображает страницу ошибки, если пост недоступен, на самом деле он не отвечает кодом статуса ошибки.

Это означает, что, хотя вы видите ошибку 404, на самом деле страница отвечает кодом 200. Для поисковых систем это, по сути, означает: «Все прошло нормально, и мы нашли страницу». Вместо того чтобы ответить 404, что говорит поисковым системам о том, что страница не существует.

Чтобы решить эту проблему, давайте снова посмотрим на getServerSideProps :

const Page = ({ isError }) => {

    //show custom error component if there is an error
    if (isError) return <Error />

    return <PostPage />

}

export const getServerSideProps = withCSR(async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient();

    let isError = false;

    try {
        await queryClient.fetchQuery(['post', id], () => getPost(id));
    } catch (error) {
        isError = true
        ctx.res.statusCode = error.response.status;
    }

    return {
        props: {
            //also passing down isError state to show a custom error component.
            isError,
            dehydratedState: dehydrate(queryClient),
        },
    }
})

export default Page;
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Мы настроили механизм кэширования с возможностью предварительной выборки данных на сервере в контексте SSR. Мы также узнали, как использовать неглубокую маршрутизацию для более быстрой навигации на стороне клиента.

Вот живая демонстрация нашей реализации и репозиторий github для исходного кода.
Также я добавил React Query devtools в продакшн, чтобы вы могли понять, что происходит под капотом.

Я хотел бы выразить искреннюю благодарность @aly3n.

8. Ссылки

  1. JSON Placeholder API
  2. Настройка гидратации React Query
  3. React Query без ручной сборки мусора на стороне сервера
  4. nextjs неглубокая маршрутизация предостережения
  5. Предотвращение выборки данных в getServerSideProps при навигации на стороне клиента
  6. отвечать ошибкой 404 в Next.js
  7. Исходный код проекта
  8. Живая демонстрация

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