Reputation: 155
I'm trying to update the cache and create an item on [post page]. After the item is created, retrieve the latest created item on [list page].
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>
);
}
Really appreciate anyone could help me figure out why cache.readQuery returns null Cheers
Upvotes: 0
Views: 2251
Reputation: 1158
You should be passing that data inside of getStaticProps
or getServerSideProps
Both are executed on the server, both are consumed by the default export in any given page.
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)
);
};
}
Use the nextjs utility types (imported from Next) for strong type inference.
also, I think you need to initialize apollo on the server before you start calling for the cache, etc..
Here is an example of a page from a repo I'm building out at the moment
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>
</>
);
}
There is some global SWR to api route stuff going on in the default export, but the initial getStaticProps with apollo + graphql remains the same. you could also try executing your mutation serverside in the api routes and using a post fetch with variables on a given job creation event.
to update the cache it has to be locally invoked. The only time you're really in the client is in JSX returning components or .tsx files. So you instantiate it as a local function whenever you want to access it (be it through useApollo (client tap) or initializeApollo (server tap)). AddApolloState can be used as a wrapper around the return of getStaticProps
, getServerSideProps
, and even in a serverless node environment (api routes). If you define the return types explicitly by calling Promise<GetServerSideProps<P>>
or Promise<GetStaticPropsResult<P>>
or NextApiResult<P>
(see my original code shared), it implicitly injects the global cache with return types, that can then be inferred in the default export on the client using InferGetStaticPropsType
etc..
Here are the contents of my apollo.ts
file
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;
}
And the contents of the default export in _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>
</>
);
}
So useApollo(pageProps) is called in app, Module Augmentation to inject
intellisense indicates it has the following types
to do that, you can use augmentation in a root @/types/*
directory or something similar. Contents of @/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;
};
};
}
Upvotes: 1