【问题标题】:Nextjs graphql apollo cache is always returning nullNextjs graphql apollo 缓存总是返回 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 干杯

【问题讨论】:

    标签: reactjs graphql next.js apollo-client


    【解决方案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({
            ssrMode: 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 是一个很好的资源
    猜你喜欢
    • 2020-09-12
    • 2021-01-15
    • 1970-01-01
    • 2019-04-02
    • 1970-01-01
    • 2022-01-14
    • 2021-08-15
    • 2020-07-07
    • 2021-06-16
    相关资源
    最近更新 更多