Nextjs graphql apollo 缓存总是返回 null
Posted
技术标签:
【中文标题】Nextjs graphql apollo 缓存总是返回 null【英文标题】:Nextjs graphql apollo cache is always returning null 【发布时间】:2021-08-08 07:31:39 【问题描述】:我正在尝试更新缓存并在 [帖子页面] 上创建一个项目。创建项目后,在 [列表页面] 上检索最新创建的项目。
mutation CREATE_JOB($input: jobInputType!)
createJob(input: $input)
_id,
address
post.tsx
const updateCache = (cache: any, mutationResult: any) =>
const newTask = mutationResult.data.createJob;
const data = cache.readQuery(
query: LIST_JOBS,
);
console.log(data) // this is null
cache.writeQuery(
query: LIST_JOBS,
variables: input: newTask.type ,
data: tasks: [...data.jobs, newTask]
)
export default function postJob()
const [
createJob,
loading: mutationLoading, error: mutationError ,
] = useMutation(CREATE_JOB, update: updateCache );
const onFinish = (values: any) =>
const newJob = ...values, status: "POSTED"
newJob.status = "POSTED"
console.log('Success ONFINISH:', newJob);
createJob(variables: input: newJob)
.then(_data => console.log(_data))
list.tsx
const data, loading, error = useQuery(LIST_JOBS);
app.tsx
const client = new ApolloClient(
uri: "http://localhost:3005/graphql",
cache: new InMemoryCache(),
);
function MyApp( Component, pageProps : AppProps)
return (
<ApolloProvider client=client>
<UserProvider>
<Component ...pageProps />
</UserProvider>
</ApolloProvider>
);
非常感谢有人能帮我弄清楚为什么 cache.readQuery 返回 null 干杯
【问题讨论】:
【参考方案1】:更新答案
原创
您应该在 getStaticProps
或 getServerSideProps
中传递该数据
两者都在服务器上执行,都被任何给定页面中的默认导出消耗。
export async function getStaticProps(
ctx: GetStaticPropsContext
): Promise<
GetStaticPropsResult<
updateCache: typeof updateCache;
>
>
// initialize apollo? I think you need to initialize apollo here.
const updateCache = (cache, mutationResult) =>
const newTask = mutationResult.data.createJob;
const data = cache.readQuery(
query: LIST_JOBS
);
console.log(data); // this is null
await cache.writeQuery(
query: LIST_JOBS,
variables: input: newTask.type ,
data: tasks: [...data.jobs, newTask]
);
;
return
props:
updateCache
,
revalidate: 60
;
export default function postJob<T extends typeof getStaticProps>( updateCache
: InferGetStaticPropsType<T>)
const [
createJob,
loading: mutationLoading, error: mutationError
] = useMutation(CREATE_JOB, update: updateCache );
const onFinish = (values: any) =>
const newJob = ...values, status: 'POSTED' ;
newJob.status = 'POSTED';
console.log('Success ONFINISH:', newJob);
createJob( variables: input: newJob ).then(_data =>
console.log(_data)
);
;
使用 nextjs 实用程序类型(从 Next 导入)进行强类型推断。
另外,我认为您需要在开始调用缓存等之前在服务器上初始化 apollo。
这是我目前正在构建的回购中的一个页面示例
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'];
>
>
console.log('getStaticProps Index: ', ctx.params?.index ?? );
const apolloClient = initializeApollo(
headers: ctx.params ??
);
await apolloClient.query<
DynamicNavQuery,
DynamicNavQueryVariables
>(
query: DynamicNavDocument,
variables:
idHead: 'Header',
idTypeHead: WordpressMenuNodeIdTypeEnum.NAME,
idTypeFoot: WordpressMenuNodeIdTypeEnum.NAME,
idFoot: 'Footer'
);
await apolloClient.query<
LandingDataQuery,
LandingDataQueryVariables
>(
query: LandingDataDocument,
variables:
other: WordPress.Services.Other,
popular: WordPress.Services.Popular,
path: Google.PlacesPath,
googleMapsKey: Google.MapsKey
);
return addApolloState(apolloClient,
props: ,
revalidate: 600
);
export default function Index<T extends typeof getStaticProps>(
other,
popular,
Header,
Footer,
merchandise,
places,
businessHours
: InferGetStaticPropsType<T>)
const reviews_per_page = 10;
const [reviews_page, setCnt] = 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`
);
const reviewCount =
data.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.data?.reviews !== undefined ? (
<BooksyReviews
pageIndex=i
key=i
reviews=data.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=reviews_page - 1 === 0 ? true : false
onClick=() => setCnt(reviews_page - 1)
className=cn(
'm-3 relative inline-flex items-center px-4 py-2 border border-olive-300 text-sm font-medium rounded-md text-olive-300 bg-redditBG hover:bg-redditSearch',
' cursor-not-allowed bg-redditSearch':
reviews_page - 1 === 0,
' cursor-pointer': reviews_page - 1 !== 0
)
>
Previous
</button>
<button
disabled=reviews_page === totalPages ? true : false
onClick=() => setCnt(reviews_page + 1)
className=cn(
'm-3 relative inline-flex items-center px-4 py-2 border border-olive-300 text-sm font-medium rounded-md text-olive-300 bg-redditBG hover:bg-redditSearch',
' cursor-not-allowed bg-redditSearch':
reviews_page === totalPages,
' cursor-pointer': reviews_page < totalPages
)
>
Next
</button>
</div>
</nav>
</BooksyReviews>
) : (
<div className='loading w-64 h-32 min-w-full mx-auto min-h-full'>
<LoadingSpinner />
</div>
)
);
useEffect(() =>
if (page.current)
setCnt((page.current = reviews_page));
, [page.current, reviews_page, setCnt, data]);
return (
<>
<AppLayout
title='The Fade Room Inc.'
Header=Header
Footer=Footer
>
<LandingCoalesced
other=other
popular=popular
places=places
businessHours=businessHours
merchandise=merchandise
>
data.data?.reviews ? (
<>
<>pages[page.current]</>
<p className='hidden'>
pages[
page.current < totalPages
? page.current + 1
: page.current
]
</p>
</>
) : (
<div className='fit mb-48 md:mb-0'>
<Fallback />
</div>
)
</LandingCoalesced>
</AppLayout>
</>
);
在默认导出中有一些全局 SWR 到 api 路由的东西,但是使用 apollo + graphql 的初始 getStaticProps 保持不变。您还可以尝试在 api 路由中执行您的变异服务器端,并在给定的作业创建事件上使用带有变量的 post fetch。
更新
要更新缓存,它必须在本地调用。您真正在客户端中的唯一一次是在 JSX 返回组件或 .tsx 文件中。因此,只要您想访问它(无论是通过 useApollo(客户端点击)还是 initializeApollo(服务器点击)),您都可以将它实例化为本地函数。 AddApolloState 可以用作getStaticProps
、getServerSideProps
的返回的包装器,甚至可以在无服务器节点环境(api 路由)中使用。如果您通过调用 Promise<GetServerSideProps<P>>
或 Promise<GetStaticPropsResult<P>>
或 NextApiResult<P>
显式定义返回类型(请参阅我共享的原始代码),它隐式注入带有返回类型的全局缓存,然后可以推断在客户端的默认导出中使用InferGetStaticPropsType
等。
这是我的apollo.ts
文件的内容
import useMemo from 'react';
import
ApolloClient,
InMemoryCache,
NormalizedCacheObject,
ApolloLink,
HttpLink
from '@apollo/client';
import fetch from 'isomorphic-unfetch';
import IncomingHttpHeaders from 'http';
import setContext from '@apollo/client/link/context';
import onError from '@apollo/client/link/error';
import AppInitialProps, AppProps from 'next/app';
export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__';
let apolloClient:
| ApolloClient<NormalizedCacheObject>
| undefined;
const wpRefresh = process.env.WORDPRESS_AUTH_REFRESH_TOKEN ?? '';
const oneEndpoint =
process.env.NEXT_PUBLIC_ONEGRAPH_API_URL ?? '';
function createApolloClient(
headers: IncomingHttpHeaders | null = null
// ctx?: NextPageContext
): ApolloClient<NormalizedCacheObject>
// isomorphic unfetch -- pass cookies along with each GraphQL request
const enhancedFetch = async (
url: RequestInfo,
init: RequestInit
): Promise<
Response extends null | undefined ? never : Response
> =>
return await fetch(url,
...init,
headers:
...init.headers,
'Access-Control-Allow-Origin': '*',
Cookie: headers?.cookie ?? ''
).then(response => response);
;
const httpLink = new HttpLink(
uri: `$oneEndpoint`,
// fetchOptions:
// mode: 'cors'
// ,
credentials: 'include',
fetch: enhancedFetch
);
const authLink: ApolloLink = setContext(
(_, ...headers : Headers) =>
let token: any;
// const token = localStorage.getItem('token' ?? '') ?? '';
if (!token)
return ;
return
headers:
...headers,
'Accept-Encoding': 'gzip',
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json; charset=utf-8',
auth: `Bearer $wpRefresh`,
'x-jwt-auth': token ? `Bearer $token` : ''
// ...(typeof window !== undefined && fetch )
;
);
const errorLink: ApolloLink = onError(
( graphQLErrors, networkError ) =>
if (graphQLErrors)
graphQLErrors.forEach(( message, locations, path ) =>
console.log(
`[GraphQL error]: Message: $message, Location: $locations, Path: $path`
)
);
if (networkError)
console.log(
`[Network error]: $networkError. Backend is unreachable. Is it running?`
);
);
return new ApolloClient(
s-s-rMode: typeof window === 'undefined',
link: authLink.concat(httpLink) ?? errorLink,
connectToDevTools: true,
cache: new InMemoryCache(
typePolicies:
Query:
fields:
// typeof: definition
// merge?: boolean | FieldMergeFunction<TExisting, TIncoming> | undefined;
// mergeObjects: FieldFunctionOptions<Record<string, any>, Record<string, any>>
wordpress:
merge(existing, incoming, mergeObjects )
// Invoking nested merge functions
return mergeObjects(existing, incoming);
,
google:
merge(existing, incoming, mergeObjects )
// Invoking nested merge functions
return mergeObjects(existing, incoming);
)
);
type InitialState = NormalizedCacheObject | any;
type IInitializeApollo =
headers?: null | IncomingHttpHeaders;
initialState?: InitialState | null;
;
export function initializeApollo(
headers, initialState : IInitializeApollo =
headers: null,
initialState: null
)
const _apolloClient =
apolloClient && headers
? createApolloClient(headers)
: createApolloClient();
if (initialState)
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract();
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
const data = ...existingCache, ...initialState ;
// deep merge approach doesn't seem to play well with invoked nested merge functions in MemoryCache
// const data = merge(initialState, existingCache,
// arrayMerge: (destinationArray, sourceArray) => [
// ...sourceArray,
// ...destinationArray.filter(d =>
// sourceArray.every(s => !isEqual(d, s))
// )
// ]
// );
// Restore the cache with the merged data
_apolloClient.cache.restore(data);
// always create a new Apollo Client
// server
if (typeof window === 'undefined') return _apolloClient;
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient;
return _apolloClient;
export function addApolloState(
client: ApolloClient<NormalizedCacheObject> | any,
pageProps: (AppInitialProps | AppProps)['pageProps'] | any
)
if (pageProps?.props)
pageProps.props[APOLLO_STATE_PROP_NAME] =
client.cache.extract();
return pageProps;
export function useApollo(
pageProps: (AppInitialProps | AppProps)['pageProps'] | any
)
const state = pageProps[APOLLO_STATE_PROP_NAME];
const store = useMemo(
() => initializeApollo( initialState: state ),
[state]
);
return store;
以及_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 from '@/lib/swr-fetcher';
import Configuration, Fetcher from 'swr/dist/types';
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, Fetcher<typeof fetcher>>>
>
) => [key, ...config ]
>
<ApolloProvider client=apolloClient>
<NextAuthProvider session=pageProps.session>
<ManagedGlobalContext>
<MediaContextProvider>
<Head />
<LayoutNoop pageProps=pageProps>
<Component ...pageProps />
</LayoutNoop>
</MediaContextProvider>
</ManagedGlobalContext>
</NextAuthProvider>
</ApolloProvider>
</SWRConfig>
</>
);
所以在app中调用useApollo(pageProps),进行Module Augmentation注入
intellisense 表示它有以下类型
为此,您可以在根 @/types/*
目录或类似目录中使用扩充。 @/types/augmented/next.d.ts
的内容
import type NextComponentType, NextPageContext from 'next';
import type Session from 'next-auth';
import type Router from 'next/router';
import DynamicNavQuery from '@/graphql/generated/graphql';
import APOLLO_STATE_PROP_NAME from '@/lib/apollo';
declare module 'next/app'
type AppProps<P = Record<string, unknown>> =
Component: NextComponentType<NextPageContext, any, P>;
router: Router;
__N_SSG?: boolean;
__N_SSP?: boolean;
pageProps: P &
session?: Session;
APOLLO_STATE_PROP_NAME: typeof APOLLO_STATE_PROP_NAME;
;
;
【讨论】:
非常感谢@Andrew 的解释!我更新了创建 Apollo 客户端的 app.tsx。 .这是我第一次尝试 Next.js、Typescript 和 graphQL。如果这是一个愚蠢的问题,请道歉。我们是否必须在每个页面中初始化 Apollo?我正在阅读这些概念。 没问题,Next 很有趣,绝对是我最喜欢的工作框架!可能性是无限的。我用解释更新了答案 非常感谢您的好意!只需要一些时间来消化这个:) 但是我从你的回答中学到了很多超出缓存问题的东西。向你致敬!除了 Next 文档,还有其他学习 Next.js 的建议吗? 我确实建议查看react2025.com 我实际上专注于自定义无头 WordPress + 无头任何构建,所以在过去的一年里我花了很多时间来解决所有这些问题,哈哈。我会说从 React2025 之类的东西开始(lee rob 有很好的教程,他是高级 vercel 工程师);在您对 Next 在各种环境(服务器端、客户端和无服务器)中的使用更加熟悉之后,我会说开始对更复杂的 repos 进行逆向工程。还强烈建议查看 SWR! 除了 react2025 和 bulletproof 之外,还可以查看 getstarted.sh/bulletproof-next,nextjs 官方示例 repo 是一个很好的资源以上是关于Nextjs graphql apollo 缓存总是返回 null的主要内容,如果未能解决你的问题,请参考以下文章
如何在客户端 NextJS 中使用 Apollo GraphQL 订阅?
删除突变后不重新获取查询(Apollo Graphql & Nextjs)
如何从 NextJS 服务器为 Apollo 客户端补充水分
设计表单构建器的数据库和状态突变和请求,以与 graphQL、动物数据库、nextJS 和 Apollo 做出反应
将 Strapi 连接到 NextJS/Apollo 客户端 GraphQL - 未提供所需类型“XXX”的变量“$input”