Updating Apollo with Nextjs because of getStaticProps and getServerSideProps #11957
Replies: 18 comments 94 replies
-
On SSG-pages you can do something like this.
export async function getStandaloneApolloClient() {
const { ApolloClient, InMemoryCache, HttpLink } = await import(
"@apollo/client"
);
return new ApolloClient({
link: new HttpLink({
uri: "https://..../graphql"
fetch
}),
cache: new InMemoryCache()
});
}
export const getStaticProps = async ({ params }) => {
const client = await getStandaloneApolloClient();
await client.query({
query: getPost,
variables: { input: { slug: params?.slug } }
});
return {
props: {
apolloStaticCache: client.cache.extract()
}
};
};
export default withApollo(Page); while the HOC contains something like: let globalApolloClient: ApolloClient;
function initApolloClient(initialState) {
if (!globalApolloClient) {
globalApolloClient = new ApolloClient({
link: from(
new HttpLink({
uri: ".../graphql",
fetch
})
]),
cache: new InMemoryCache().restore(initialState || {})
});
}
// client side page transition to an SSG page => update Apollo cache
else if (initialState) {
globalApolloClient.cache.restore({
...globalApolloClient.cache.extract(),
...initialState
});
}
return globalApolloClient;
}
export function withApollo(PageComponent) {
const WithApollo = ({
apolloStaticCache,
...pageProps
}) => {
// HERE WE USE THE PASSED CACHE
const client = initApolloClient(apolloStaticCache);
// and here we have the initialized client 🙂
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
);
};
// if you also use it for SSR
if (PageComponent.getInitialProps) {
WithApollo.getInitialProps = async () => {
// Run wrapped getInitialProps methods
let pageProps = {};
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx);
}
return pageProps;
};
}
// Set the correct displayName in development
if (process.env.NODE_ENV !== "production") {
const displayName =
PageComponent.displayName || PageComponent.name || "Component";
WithApollo.displayName = `withApollo(${displayName})`;
}
return WithApollo;
} |
Beta Was this translation helpful? Give feedback.
-
Apollo examples have been updated with a new implementation that can be used with |
Beta Was this translation helpful? Give feedback.
-
@lfades I see |
Beta Was this translation helpful? Give feedback.
-
@johny Sorry I am new to using GraphQL in an app and I have yet to build my client app yet... but I was looking at the example. In the PostList component... is useQuery querying the in memory store and not making an actual network request because that was done at the page level with getStaticProps? |
Beta Was this translation helpful? Give feedback.
-
@lfades I switched to the new with-apollo example and it seems to work quite nice - something I noticed though: is that when navigating to a SSG page, it runs the getStaticProps function (as expected), it then extracts the cached data and that is passed into the useApollo / initialise apollo hook (as expected) but im running into a flashing ui problem. My example is that I have some posts that are generated with getStaticProps, but also a navbar with user links (log in/profile), so I have a useMeQuery() in the nav, this obviously can't be generated at build time so the data is fetched on the client, this is all good. on the initial load the posts are pre-rendered and the nav gets the user data and shows the appropriate links (all good so far). the problem then comes when navigating to another SSG page. it then re-runs the me query even though its already been cached causing the nav links to flash. The problem is because of the code below found in the Apollo Client initialisation:
when we navigate to a SSG page, the getStaticProps data populates pageProps in _app and then the useApollo restores the client's cache but only with the static props data and completely wipes any existing cache items that might have been there. has anyone found a way to solve this? merging the pageProps created by the SSG page and the existing cache? |
Beta Was this translation helpful? Give feedback.
-
As far as I understand |
Beta Was this translation helpful? Give feedback.
-
We decided to finally to a whole refactoring to explicitly query the data we want to SSR. The biggest risk we found is that the query down the component tree could potentially change and the query not hit the cache. In SSR, we enabled the const client = useApollo(
pageProps.initialApolloState,
undefined,
isServer ? 'cache-only' : 'cache-first'
); Now, for the queries that we want to explicitly SSR, we wrote a small hook to check that the data is returned. If the data is not returned in development, we crash the rendering to alert the developer that SSR is broken. This will crash in staging to. Note that we use export function useSSRQuery<T extends (...args: any[]) => any>(
useCallback: T,
...args: Parameters<T>
): ReturnType<T> {
const response = useCallback(...args);
// If not production and not server, crash the application
// to alert the dev that SSR is not working
if (!isProduction) {
const isServer = typeof window === 'undefined';
if (isServer && !response.data) {
throw new Error(
`SSR not functioning properly\n\n${JSON.stringify(
{ hookName: useCallback.name, args },
null,
2
)}\n`
);
}
}
return response;
} To use it we'd do something like this: const {
loading,
data,
error
} = useSSRQuery(useSearchResultsQuery, {
variables
}); This is not perfect, but at least we explicitly tell which queries we expect to SSR, and if they don't work well, we have a way to alert the user. Potentially, we could add tests or a custom error handler. For the time being, this reduces the risk of another developer changing the query down the component tree and forgetting to update the SSR query. I don't like the way Next forces us to duplicate the query logic, which was more elegant with Hopefully, this helps someone. Feel free to share ideas to workaround this limitation. |
Beta Was this translation helpful? Give feedback.
-
I've been following this discussion for an SSR solution for A simple example would be a page for displaying products (paginated), so the initial page load contains the HTML of the first few products fetched and rendered on the server, and load more functionality is then fetched from and rendered on the client. So if I view the generated html in a browser - I'm expecting to see the raw data that was fetched server side, as well as the HTML of the first bunch of products, but I only see the the raw data followed by the Next.js script includes in the body, no HTML of the actual data. The product cards is generated client side. Not sure what I'm missing here - how can I force Next to generate the HTML server side instead of only sending the data. |
Beta Was this translation helpful? Give feedback.
-
I am experimenting with the following approach, so far seems to allow both static rendering and avoids having to manually prefetch queries in getStaticProps. In order to fetch all the nested queries we don't need to render the whole app, we can render the page component. And we can't create a "true" next.js/packages/next/next-server/server/render.tsx Lines 511 to 522 in 48acc47 because getStaticProps doesn't get enough props:next.js/packages/next/next-server/server/render.tsx Lines 598 to 606 in 48acc47 However, I wonder if we really need the whole router or we just need params and locales from it: next.js/packages/next/next-server/lib/router/router.ts Lines 250 to 259 in 48acc47 With that in mind, here's my prototype: import { GetStaticPathsResult, NextPage } from "next";
import { ApolloProvider, gql, useQuery } from "@apollo/client";
import { withApollo } from "components/withApollo";
import CurrentUser from "components/CurrentUser";
import { useRouter } from "next/router";
import {
productPageQueryType,
productPageQueryVariablesType,
} from "./types/query";
import apolloStatic from "components/withApollo/apolloStatic";
type Params = {
seoName: string;
};
const QUERY = gql`
query productPage($seoName: String!) {
product(seoName: $seoName) {
id
name
}
}
`;
function ProductPage() {
const router = useRouter();
const { seoName } = router.query as Params;
const { data } = useQuery<
productPageQueryType,
productPageQueryVariablesType
>(QUERY, {
variables: {
seoName,
},
fetchPolicy: "cache-and-network",
});
const product = data?.product;
if (!product) {
return <div>Not Found</div>;
}
return (
<div>
{/* CurrentUser has "skip: typeof window === 'undefined'" in its query */}
<CurrentUser />
<Title>
Product: {product.id} {product.name}
</Title>
</div>
);
}
export default withApollo({ ssr: false })(ProductPage as NextPage);
export async function getStaticPaths(): Promise<GetStaticPathsResult> {
return {
paths: [],
fallback: "blocking",
};
}
// TODO: extract this boilerplate into a helper
export const getStaticProps = async ({
params,
locales,
locale,
defaultLocale,
}) => {
const { getDataFromTree } = await import("@apollo/react-ssr");
const { RouterContext } = await import(
"next/dist/next-server/lib/router-context"
);
const router = {
query: params,
locales,
locale,
defaultLocale,
};
const apolloClient = apolloStatic();
const PrerenderComponent = () => (
<ApolloProvider client={apolloClient}>
<RouterContext.Provider value={router}>
<ProductPage />
</RouterContext.Provider>
</ApolloProvider>
);
await getDataFromTree(<PrerenderComponent />);
return {
props: {
apolloState: apolloClient.cache.extract(),
},
};
}; One notable omission is the absence of |
Beta Was this translation helpful? Give feedback.
-
As i can see there is no way to solve @yossisp issue with getStaticProps and getServerSideProps graphql cache :( Going to read about gutsby for now, maybe they have a working way with ssg/ssr and graphql. |
Beta Was this translation helpful? Give feedback.
-
Created fuature request to make it work at least somehow: #19611 |
Beta Was this translation helpful? Give feedback.
-
So no solution for getServerSideProps to use the apollo client cache and eliminate the extra extra network call? does this mean we are sticking to getInitialProps ? |
Beta Was this translation helpful? Give feedback.
-
@TheRusskiy Thanks for sharing your example. I was able to modify it to work with export function withGraphQL<P, IP>(Page: NextPage<P, IP>): NextPage<P & { apolloClient?: ApolloClient<NormalizedCacheObject>, apolloState?: NormalizedCacheObject }, IP> {
return (props) => {
const client = props.apolloClient ?? userGraphQLClient()
if (!props.apolloClient)
client.restore(props.apolloState)
return (
<ApolloProvider client={client}>
<Page {...props} />
</ApolloProvider>
)
}
}
export function getServerSidePropsWithGraphQL<P, Q extends ParsedUrlQuery>(Page: NextPage<P>, getServerSideProps: GetServerSideProps<P, Q>): GetServerSideProps<P, Q> {
return async (ctx) => {
const { getDataFromTree } = await import('@apollo/client/react/ssr')
const apolloClient = userGraphQLClient(ctx)
const res = await getServerSideProps(ctx)
if (!('props' in res))
return res
const WrappedPage = withGraphQL(Page)
await getDataFromTree(<WrappedPage {...res.props} apolloClient={apolloClient}/>)
return {
props: {
...res.props,
apolloState: apolloClient.extract()
}
}
}
}
Replace To use it: export default withGraphQL(MyPage)
export const getServerSideProps: GetServerSideProps<{ foo: string }> = getServerSidePropsWithGraphQL(MyPage, async ctx => {
return {
props: {
foo: 'asdf'
}
}
}) |
Beta Was this translation helpful? Give feedback.
-
There is a really annoying problem here! Authors really need to think about it. I don't understand why this issue takes so little attention from community... |
Beta Was this translation helpful? Give feedback.
-
What if you are forced to make a query outside of Next.js page component (./src/pages/page.js), like some nested component (./src/components/SEO.js) which should work Client and SSR? |
Beta Was this translation helpful? Give feedback.
-
After following the discussion I am more confused and lost than I was before I started digging on the matter. I am developping an enterprise platform in the form of a Dynamic Web App. I have already developped the Backend server using Apollo server, GraphQL and PrismaClient as an ORM to an MSSQL Database. The App is 90% dynamic meaning most of the components consist of Lists, Cards and DataTables populated on request from the backend. The dataset itsself is very fluid, meaning the results change a lot during the day, even in an hour-to-hour basis. So coming to the frontend, the stack I decided to go with, was of course react, Apollo client, grommet for UIUX, styled components and all of them wrapped by Next.JS. I deliberately didnt choose a state management library because I wanted to use Apollo client not only for its data fetching and mutation hooks but also as a state management library using its cache. So my question is this: How do we choose between getServerSideProps and Client Side Data fetching using Apollo? How does a page using GetServerSideProps coexist with components that use Apollo hooks to get data from or update data in the database? How can I assure that Apollo's cache will always be my single source of truth concerning state management throughout my application's lifecycle? |
Beta Was this translation helpful? Give feedback.
-
I am looking for a good way to handle GraphQL queries that are needed for the overall layout, e.g. header/footer navigation. The data for the navigation menus needs to come from our GraphQL server. Ideally this query should be run in We have a React component for the layout, The only solution I can see given the current limitations of next.js is to call a helper function from every |
Beta Was this translation helpful? Give feedback.
-
Just to summarize discussion and please correct me if I am wrong. As an example, we have a headless CMS with lots of brands and we dynamically determine what to request for url on components level, so we have only one page for most of page types - because on this level we do not know yet what kind of page it is. So we cannot make queries on page level which will fit all cases because branching is too complex. |
Beta Was this translation helpful? Give feedback.
-
I was reading the example in the Next repo:
https://github.com/zeit/next.js/blob/canary/examples/with-apollo/lib/apollo.js
And in this file (starting at line 107) getInitialProps is still used for ssr... should or could that be changed to getServerSideProps?
And what if we want to use getStaticProps with a query?
Or since it looks like this file is returning an object with the apolloClient on it... would we just use the client in getStaticProps and run the query there?
Beta Was this translation helpful? Give feedback.
All reactions