graphql - 我可以将 useSWR 与 apollo-client 一起使用吗?

标签 graphql next.js apollo-client swr

我对 next.js graphQL 世界还很陌生。

我刚刚找到了 useSWR,我想知道是否可以将它与 Apollo 客户端一起使用, 不与 graphql-request 一起使用。

最佳答案

是的,你可以,我也可以。我有两个共存的 headless CMS。一种是通过 OneGraph 与 google 捆绑的 Headless WordPress;另一个是 Headless Booksy,一个闭源 CMS,没有可公开访问的端点——在用户驱动的事件期间剖析网络选项卡,以确定任何所需的 header /参数,并最终对其身份验证流程进行反向工程,以使用异步分区在我的存储库中实现自动化。

也就是说,是的,我同时使用 apollo Client 和 SWR。这是 _app.tsx 配置

  • _app.tsx
import '@/styles/index.css';
import '@/styles/chrome-bug.css';
import 'keen-slider/keen-slider.min.css';

import { AppProps, NextWebVitalsMetric } from 'next/app';
import { useRouter } from 'next/router';
import { ApolloProvider } from '@apollo/client';
import { useEffect, FC } from 'react';
import { useApollo } from '@/lib/apollo';
import * as gtag from '@/lib/analytics';
import { MediaContextProvider } from '@/lib/artsy-fresnel';
import { Head } from '@/components/Head';
import { GTagPageview } from '@/types/analytics';
import { ManagedGlobalContext } from '@/components/Context';
import { SWRConfig } from 'swr';
import { Provider as NextAuthProvider } from 'next-auth/client';
import fetch from 'isomorphic-unfetch';
import { fetcher, fetcherGallery } from '@/lib/swr-fetcher';
import { Configuration, Fetcher } from 'swr/dist/types';

type T = typeof fetcher | typeof fetcherGallery;
interface Combined extends Fetcher<T> {}

const Noop: FC = ({ children }) => <>{children}</>;
export default function NextApp({
    Component,
    pageProps
}: AppProps) {
    const apolloClient = useApollo(pageProps);

    const LayoutNoop = (Component as any).LayoutNoop || Noop;

    const router = useRouter();

    useEffect(() => {
        document.body.classList?.remove('loading');
    }, []);

    useEffect(() => {
        const handleRouteChange = (url: GTagPageview) => {
            gtag.pageview(url);
        };
        router.events.on('routeChangeComplete', handleRouteChange);
        return () => {
            router.events.off('routeChangeComplete', handleRouteChange);
        };
    }, [router.events]);

    return (
        <>
            <SWRConfig
                value={{
                    errorRetryCount: 5,
                    refreshInterval: 43200 * 10,
                    onLoadingSlow: (
                        key: string,
                        config: Readonly<
                            Required<Configuration<any, any, Combined>>
                        >
                    ) => [key, { ...config }]
                }}
            >
                <ApolloProvider client={apolloClient}>
                    <NextAuthProvider session={pageProps.session}>
                        <ManagedGlobalContext>
                            <MediaContextProvider>
                                <Head />
                                <LayoutNoop pageProps={pageProps}>
                                    <Component {...pageProps} />
                                </LayoutNoop>
                            </MediaContextProvider>
                        </ManagedGlobalContext>
                    </NextAuthProvider>
                </ApolloProvider>
            </SWRConfig>
        </>
    );
}

然后,我有这个 api 路由处理评论 + 通过 index.tsx 中的 SWR 对这些评论进行分页

  • pages/api/booksy-fetch.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { BooksyReviewFetchResponse } from '@/types/booksy';
import fetch from 'isomorphic-unfetch';
import { getAccessToken } from '@/lib/booksy';

const API_KEY = process.env.NEXT_PUBLIC_BOOKSY_BIZ_API_KEY ?? '';
const FINGERPRINT =
    process.env.NEXT_PUBLIC_BOOKSY_BIZ_X_FINGERPRINT ?? '';

export default async function (
    req: NextApiRequest,
    res: NextApiResponse<BooksyReviewFetchResponse>
) {

    const {
        query: { reviews_page, reviews_per_page }
    } = req;

    const { access_token } = await getAccessToken();
    const rev_page_number = reviews_page ? reviews_page : 1;
    const reviews_pp = reviews_per_page ? reviews_per_page : 10;

    const response = await fetch(
        `https://us.booksy.com/api/us/2/business_api/me/businesses/481001/reviews/?reviews_page=${rev_page_number}&reviews_per_page=${reviews_pp}`,
        {
            headers: {
                'X-Api-key': API_KEY,
                'X-Access-Token': `${access_token}`,
                'X-fingerprint': FINGERPRINT,
                Authorization: `s-G1-cvdAC4PrQ ${access_token}`,
                'Cache-Control':
                    's-maxage=86400, stale-while-revalidate=43200',
                'User-Agent':
                    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Safari/537.36',
                Connection: 'keep-alive',
                Accept: '*/*',
                'Accept-Encoding': 'gzip, deflate, br'
            },
            method: 'GET',
            keepalive: true
        }
    );

    const booksyReviews: BooksyReviewFetchResponse =
        await response.json();
    res.setHeader(
        'Cache-Control',
        'public, s-maxage=86400, stale-while-revalidate=43200'
    );

    return res.status(200).json(booksyReviews);
}

以及以下 api 路由也在 index.tsx 中处理自定义选取框的图像数据

  • pages/api/booksy-images.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { Gallery } from '@/types/index';
import { getLatestBooksyPhotos } from '@/lib/booksy';

export default async function (
    _req: NextApiRequest,
    res: NextApiResponse<Gallery>
) {
    const response: Response = await getLatestBooksyPhotos();
    const booksyImages: Gallery = await response.json();
    res.setHeader(
        'Cache-Control',
        'public, s-maxage=86400, stale-while-revalidate=43200'
    );

    return res.status(200).json(booksyImages);
}

现在,检查index.tsx。为了清楚起见,我将把代码分为服务器端和客户端

  • index.tsx(getStaticProps——服务器)

export async function getStaticProps(
    ctx: GetStaticPropsContext
): Promise<
    GetStaticPropsResult<{
        other: LandingDataQuery['other'];
        popular: LandingDataQuery['popular'];
        Places: LandingDataQuery['Places'];
        merchandise: LandingDataQuery['merchandise'];
        businessHours: LandingDataQuery['businessHours'];
        Header: DynamicNavQuery['Header'];
        Footer: DynamicNavQuery['Footer'];
        initDataGallery: Partial<
            Configuration<Gallery, any, Fetcher<Gallery>>
        >;
        initialData: Partial<
            Configuration<
                BooksyReviewFetchResponse,
                any,
                Fetcher<BooksyReviewFetchResponse>
            >
        >;
    }>
> {
    console.log(ctx.params ?? '');
    const apolloClient = initializeApollo();
    const { data: DynamicSlugs } = await apolloClient.query<
        DynamicNavQuery,
        DynamicNavQueryVariables
    >({
        query: DynamicNavDocument,
        variables: {
            idHead: 'Header',
            idTypeHead: WordpressMenuNodeIdTypeEnum.NAME,
            idTypeFoot: WordpressMenuNodeIdTypeEnum.NAME,
            idFoot: 'Footer'
        }
    });
    const { data: LandingData } = await apolloClient.query<
        LandingDataQuery,
        LandingDataQueryVariables
    >({
        query: LandingDataDocument,
        variables: {
            other: WordPress.Services.Other,
            popular: WordPress.Services.Popular,
            path: Google.PlacesPath,
            googleMapsKey: Google.MapsKey
        }
    });
    const { other, popular, Places, businessHours, merchandise } =
        LandingData;
    const { Header, Footer } = DynamicSlugs;
    const dataGallery = await getLatestBooksyPhotos();
    const initDataGallery: Gallery = await dataGallery.json();

    const dataInit = await getLatestBooksyReviews({
        reviewsPerPage: 10,
        pageIndex: 1
    });
    const initialData: BooksyReviewFetchResponse =
        await dataInit.json();
    return addApolloState
        ? addApolloState(apolloClient, {
                props: {
                    Header,
                    Footer,
                    other,
                    popular,
                    Places,
                    businessHours,
                    merchandise
                },
                revalidate: 600
          })
        : {
                props: {
                    initialData,
                    initDataGallery
                },
                revalidate: 600
          };
}

注意返回的 props 是如何作为 SWR 与 Apollo 客户端数据的函数来处理的?接下来就很精彩了

  • 注意 getStaticProps 中调用的函数
    const dataGallery = await getLatestBooksyPhotos();
    const initDataGallery: Gallery = await dataGallery.json();

    const dataInit = await getLatestBooksyReviews({
        reviewsPerPage: 10,
        pageIndex: 1
    });
    const initialData: BooksyReviewFetchResponse =
        await dataInit.json();

--它们来自 lib 目录。它们旨在在页面文件服务器上使用,以将初始数据注入(inject) SWR。本质上,它实现了与 api 路由文件相同的方法,但由于这些文件只能在客户端上使用,所以这是一个必要的解决方法。

现在为客户

  • index.tsx(默认导出 - 客户端)

export default function Index<T extends typeof getStaticProps>({
    other,
    popular,
    Header,
    Footer,
    merchandise,
    Places,
    businessHours,
    initialData,
    initDataGallery
}: InferGetStaticPropsType<T>) {
    const GalleryImageLoader = ({
        src,
        width,
        quality
    }: ImageLoaderProps) => {
        return `${src}?w=${width}&q=${quality || 75}`;
    };
    const reviews_per_page = 10;
    const [reviews_page, set_reviews_page] = useState<number>(1);
    const page = useRef<number>(reviews_page);
    const { data } = useSWR<BooksyReviewFetchResponse>(
        () =>
            `/api/booksy-fetch?reviews_page=${reviews_page}&reviews_per_page=${reviews_per_page}`,
        fetcher,
        initialData
    );
    const { data: galleryData } = useSWR<Gallery>(
        '/api/booksy-images',
        fetcherGallery,
        initDataGallery
    );

    // total items
    const reviewCount = data?.reviews_count ?? reviews_per_page;

    // total pages
    const totalPages =
        (reviewCount / reviews_per_page) % reviews_per_page === 0
            ? reviewCount / reviews_per_page
            : Math.ceil(reviewCount / reviews_per_page);

    // correcting for array indeces starting at 0, not 1
    const currentRangeCorrection =
        reviews_per_page * page.current - (reviews_per_page - 1);

    // current page range end item
    const currentRangeEnd =
        currentRangeCorrection + reviews_per_page - 1 <= reviewCount
            ? currentRangeCorrection + reviews_per_page - 1
            : currentRangeCorrection +
              reviews_per_page -
              (reviewCount % reviews_per_page);

    // current page range start item
    const currentRangeStart =
        page.current === 1
            ? page.current
            : reviews_per_page * page.current - (reviews_per_page - 1);

    const pages = [];
    for (let i = 0; i <= reviews_page; i++) {
        pages.push(
            data?.reviews ? (
                <BooksyReviews pageIndex={i} key={i} reviews={data.reviews}>
                    <nav aria-label='Pagination'>
                        <div className='hidden sm:block'>
                            <p className='text-sm text-gray-50'>
                                Showing{' '}
                                <span className='font-medium'>{`${currentRangeStart}`}</span>{' '}
                                to{' '}
                                <span className='font-medium'>{`${currentRangeEnd}`}</span>{' '}
                                of <span className='font-medium'>{reviewCount}</span>{' '}
                                reviews (page:{' '}
                                <span className='font-medium'>{page.current}</span> of{' '}
                                <span className='font-medium'>{totalPages}</span>)
                            </p>
                        </div>
                        <div className='flex-1 inline-flex justify-between sm:justify-center my-auto'>
                            <button
                                disabled={page.current - 1 === 0 ? true : false}
                                onClick={() => set_reviews_page(page.current - 1)}
                                className={cn('landing-page-pagination-btn', {
                                    ' cursor-not-allowed bg-redditSearch':
                                        reviews_page - 1 === 0,
                                    ' cursor-pointer': reviews_page - 1 !== 0
                                })}
                            >
                                Previous
                            </button>

                            <button
                                disabled={page.current === totalPages ? true : false}
                                onClick={() => set_reviews_page(page.current + 1)}
                                className={cn('landing-page-pagination-btn', {
                                    ' cursor-not-allowed bg-redditSearch':
                                        reviews_page === totalPages,
                                    ' cursor-pointer': reviews_page < totalPages
                                })}
                            >
                                Next
                            </button>
                        </div>
                    </nav>
                </BooksyReviews>
            ) : (
                <ReviewsSkeleton />
            )
        );
    }

    useEffect(() => {
        (async function Update() {
            return (await page.current) === reviews_page
                ? true
                : set_reviews_page((page.current = reviews_page));
        })();
    }, [page.current, reviews_page]);
    return (
        <>
            <AppLayout
                title={'The Fade Room Inc.'}
                Header={Header}
                Footer={Footer}
            >
                {galleryData?.images ? (
                    <Grid>
                        {galleryData.images
                            .slice(6, 9)
                            .map((img, i) => {
                                <GalleryCard
                                    key={img.image_id}
                                    media={galleryData}
                                    imgProps={{
                                        loader: GalleryImageLoader,
                                        width: i === 0 ? 1080 : 540,
                                        height: i === 0 ? 1080 : 540
                                    }}
                                />;
                            })
                            .reverse()}
                    </Grid>
                ) : (
                    <LoadingSpinner />
                )}
                {galleryData?.images ? (
                    <Marquee variant='secondary'>
                        {galleryData.images
                            .slice(3, 6)
                            .map((img, j) => (
                                <GalleryCard
                                    key={img.image_id}
                                    media={galleryData}
                                    variant='slim'
                                    imgProps={{
                                        loader: GalleryImageLoader,
                                        width: j === 0 ? 320 : 320,
                                        height: j === 0 ? 320 : 320
                                    }}
                                />
                            ))
                            .reverse()}
                    </Marquee>
                ) : (
                    <LoadingSpinner />
                )}
                <LandingCoalesced
                    other={other}
                    popular={popular}
                    places={Places}
                    businessHours={businessHours}
                    merchandise={merchandise}
                >
                    {data?.reviews ? (
                        <>
                            <>{pages[page.current]}</>
                            <span className='hidden'>
                                {
                                    pages[
                                        page.current < totalPages
                                            ? page.current + 1
                                            : page.current - 1
                                    ]
                                }
                            </span>
                        </>
                    ) : (
                        <ReviewsSkeleton />
                    )}
                </LandingCoalesced>
            </AppLayout>
        </>
    );
}

登陆页面上有两个 useSWR Hook :

    const { data } = useSWR<BooksyReviewFetchResponse>(
        () =>
            `/api/booksy-fetch?reviews_page=${reviews_page}&reviews_per_page=${reviews_per_page}`,
        fetcher,
        initialData
    );
    const { data: galleryData } = useSWR<Gallery>(
        '/api/booksy-images',
        fetcherGallery,
        initDataGallery
    );

initialDatainitDataGallery在各自的 getter 之后列出的值是从服务器传输到客户端并通过 InferGetStaticPropsType<T> 推断的初始数据。 。这为客户端数据获取时的第一个加载数据问题提供了解决方案。

使用 SWR 时,您可以进行一项额外的配置来加快在客户端上获取数据的速度,即指定应在 _document.tsx 中预加载哪些 api 路由。及其相应的提取器的名称

  • _document.tsx

                    <link
                        rel='preload'
                        href={`/api/booksy-fetch?reviews_page=1&reviews_per_page=10`}
                        as='fetcher'
                        crossOrigin='anonymous'
                    />
                    <link
                        rel='preload'
                        href='/api/booksy-images'
                        as='fetcherGallery'
                        crossOrigin='anonymous'
                    />

我在同时使用两者时遇到了 0 个问题,我已经这样做了大约一个月,并且实际上将 SWR 合并到增强 DX/UX 中,并且下一个分析反射(reflect)了这一点(它将 FCP 时间缩短 50% 以上至 0.4 秒,LCP 时间缩短至 0.8 秒左右。

关于graphql - 我可以将 useSWR 与 apollo-client 一起使用吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66851630/

相关文章:

node.js - Apollo 服务器 : How can I add a custom endpoint for Stripe webhook?

javascript - 在命令行上运行 graphql 示例会导致意外的标记

GraphQL 突变为嵌套响应返回 null

reactjs - 为什么当状态更新时渲染中的变化没有更新?

reactjs - GraphQL - Gatsby.js- React 组件。 - 如何查询变量/参数?

reactjs - 如何在 Next.js 上设置健康检查的端点?

node.js - Azure 应用服务中的 Docker 超时 - 使用 Nextjs

javascript - 从 Apollo 商店检索数据

graphql - 如何将图像从 TinyMCE 上传到 Apollo GraphQL 服务器?

reactjs - Apollo 的 proxy.writeQueries 清空缓存(是否有错误?)