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】:

更新答案

原创

您应该在 getStaticPropsgetServerSideProps 中传递该数据

两者都在服务器上执行,都被任何给定页面中的默认导出消耗。

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 可以用作getStaticPropsgetServerSideProps 的返回的包装器,甚至可以在无服务器节点环境(api 路由)中使用。如果您通过调用 Promise&lt;GetServerSideProps&lt;P&gt;&gt;Promise&lt;GetStaticPropsResult&lt;P&gt;&gt;NextApiResult&lt;P&gt; 显式定义返回类型(请参阅我共享的原始代码),它隐式注入带有返回类型的全局缓存,然后可以推断在客户端的默认导出中使用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”

NextJS 中的 Apollo 分页