我可以将 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.tsximport '@/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.tsimport 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.tsimport 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
);
initialData
和initDataGallery
值列在它们各自的提取器之后是从服务器传送到客户端并通过InferGetStaticPropsType<T>
推断的初始数据。这为客户端数据获取时的第一个加载数据问题提供了解决方案。
您可以在使用 SWR 时加快在客户端上获取数据的另一个配置是指定应在 _document.tsx
中预加载哪些 api 路由及其对应的 fetcher 的名称
<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].tsx
和pages/repos/[login]/[details].tsx
【参考方案2】:
有一个比较:Comparison | React Query vs SWR vs Apollo vs RTK Query
【讨论】:
与问题无关......表中的错误(休息链接,重试链接,'滞后查询'可以轻松完成)以上是关于我可以将 useSWR 与 apollo-client 一起使用吗?的主要内容,如果未能解决你的问题,请参考以下文章