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

Posted

技术标签:

【中文标题】我可以将 useSWR 与 apollo-client 一起使用吗?【英文标题】:Can I use useSWR with apollo-client? 【发布时间】:2021-06-25 07:55:21 【问题描述】:

我对所有 next.js graphQL 世界都很陌生。

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

【问题讨论】:

Apollo 不是 fetcher,它是一个完整的带有规范化缓存的 graphql 客户端 ... SWR[+fetcher] 和 Apollo 也是如此 为什么不用graphql-request? 【参考方案1】:

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

也就是说,是的,我同时使用了 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-route 处理评论 + 通过 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 目录。它们旨在在页面文件的服务器上使用,用于向 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 钩子:

    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 值列在它们各自的提取器之后是从服务器传送到客户端并通过InferGetStaticPropsType&lt;T&gt; 推断的初始数据。这为客户端数据获取时的第一个加载数据问题提供了解决方案。

您可以在使用 SWR 时加快在客户端上获取数据的另一个配置是指定应在 _document.tsx 中预加载哪些 api 路由及其对应的 fetcher 的名称

_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 和下一个分析中反映了这一点(它将 FCP 时间缩短 50% 以上至 0.4 秒,将 LCP 时间缩短至约 0.8 秒)。

【讨论】:

如果您希望使用 SWR 作为 apolloClient 的扩展,例如为其获取数据,那么我会这么说。如果您想查看它,我将在此 repo 中的客户端上合并 SWR 以缓存 getServerSideProps 数据。应该在第二天的某个时间做。 github.com/DopamineDriven/github-search-graphql 查看pages/repos/[login].tsxpages/repos/[login]/[details].tsx【参考方案2】:

有一个比较:Comparison | React Query vs SWR vs Apollo vs RTK Query

【讨论】:

与问题无关......表中的错误(休息链接,重试链接,'滞后查询'可以轻松完成)

以上是关于我可以将 useSWR 与 apollo-client 一起使用吗?的主要内容,如果未能解决你的问题,请参考以下文章

useSWR Hook 返回 undefined

useSWR 未加载数据。数据保持加载状态

我应该在这个包装 useSWR 的自定义钩子中测试啥?

如何将 redux 与 graphql 一起使用

是否可以将服务注入 apollo-client 的中间件?

如何为 apollo-client 的每个请求设置变量?