Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace graphql-request with urql #35

Merged
merged 3 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions app/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const getPage = async (slug: string, locale: string, preview = false) => {
items {
topSectionCollection(limit: 10) {
items {
...ComponentHeroBannerFields
...ComponentDuplexFields
...ComponentHeroBannerFields
...ComponentDuplexFields
}
}
}
Expand All @@ -31,16 +31,16 @@ const getPage = async (slug: string, locale: string, preview = false) => {
[ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment]
);
return (
await graphqlClient(preview).request(pageQuery, {
await graphqlClient(preview).query(pageQuery, {
locale,
preview,
slug,
})
).pageCollection?.items?.[0];
).data?.pageCollection?.items?.[0];
};

const getPageSlugs = async () => {
const pageQuery = graphql(/* GraphQL */ `
const pageQuery = graphql(`
query PageSlugs($locale: String) {
# Fetch 50 pages. Ideally we would fetch a good sample of most popular pages for pre-rendering,
# but for the sake of this example we'll just fetch the first 50.
Expand All @@ -52,12 +52,12 @@ const getPageSlugs = async () => {
}
`);

const pages = await graphqlClient(false).request(pageQuery, {
const pages = await graphqlClient(false).query(pageQuery, {
locale: "en-US",
});

return (
pages?.pageCollection?.items
pages?.data?.pageCollection?.items
.filter((page) => page?.slug)
.map((page) => ({
slug: page?.slug === "home" ? "/" : page?.slug,
Expand Down Expand Up @@ -86,7 +86,7 @@ export default async function LandingPage({
);
}

export const revalidate = 60;
export const revalidate = 120;

export async function generateStaticParams() {
return (await getPageSlugs()).map((page) => ({
Expand Down
16 changes: 11 additions & 5 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ export default async function RootLayout({
[NavigationFieldsFragment]
);

const layoutData = await graphqlClient(isDraftMode).request(layoutQuery, {
locale: "en-US",
preview: isDraftMode,
});
const layoutData = await graphqlClient(isDraftMode).query(
layoutQuery,
{
locale: "en-US",
preview: isDraftMode,
},
{ fetchOptions: { next: { revalidate: 60, tags: ["menu"] } } }
);

return (
<html lang="en">
Expand All @@ -47,7 +51,9 @@ export default async function RootLayout({
>
<ContentfulPreviewProvider isDraftMode={isDraftMode}>
<div className="relative flex min-h-screen flex-col">
<SiteHeader navigationData={layoutData.navigationMenuCollection} />
<SiteHeader
navigationData={layoutData.data?.navigationMenuCollection}
/>
<div className="flex-1">{children}</div>
</div>
</ContentfulPreviewProvider>
Expand Down
48 changes: 48 additions & 0 deletions docs/decisions/007-adopt-urql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Adopt urql as a graphql client library.

Date: 2024-04-22

Status: accepted

## Context

While graphql-request was perfectly capable minimal client, it was never meant to be the end-game for a serious data-fetching solution. The problem with graphql-request as a client were evident:

1. No support for Next.js extra fetch parameters like revalidate, tags or data cache in general (Next Data Cache)
2. No support for complex auth ar Automated Persistent Queries middlewares.
3. No path to grow into a hybrid RSC and Client fetching library (with mature client-side caching ecosystem)

Even though the selling point of Client data fetching library for SSRed client components is not strong yet, it's probably a bonus if we have to ever allow client-fetching use-cases, main driver though for adopting a different approach was points (1) and (2).

Since Contentful's GraphQL endpoint has a recent addition of APQ and benefits unique to it (request caching and smaller body size), we had to start using APQ in our starter

There are only 2 libraries that offer out of the box APQ support, and you could roll your own, but why do that? It is Apollo Client and Urql.

Both libraries are well maintained, both are geared towards client-heavy data fetching solutions, both have some support for NextJS server components, but there are a few differences.

Apollo GraphQL has a NextJS experimental package that supports RSC:
https://github.com/apollographql/apollo-client-nextjs

Urql on the other hand has no experimental features and basically lets you create a client and use it server-side with no need to use react hooks or client components with state.

Urql is not as minimal as "graphql-request", but the idea of "exchanges" (middlewares) was appealing, it has APQ exchange and it has debugging tools and history if we ever need it.

On the second note, NextJS Caching is really useful, especially on Vercel where you can track it's usage on the platform and invalidate it natively with revalidateTag, but having to design a solution to use Next Cache without fetch is tricky, since at the time of writing Next Cache without fetch is only available as an experimental API (unstable_cache), it's better to adopt a solution that can use fetch under the hood natively.

## Decision

1. Adopt Urql as replacement for graphql-request.
2. Enable APQ by default.

## Consequences

1. Most of the API changes are minor, but code has to be adjusted to:

- use new "query" method instead of "request"
- use new "data" property on the response as opposed to getting the response shape directly
- use new OperationOptions as third argument to to pass fetchOptions to the fetch directly, this let you pass revalidate time and/or tags for NextJS Data Cache.

2. All request are now cached indefinitely by default (NextJS's fetch has default caching unless opted out with cache: no-store, or revalidate)
3. Along with the decision to adopt Urql, we enable APQ by default via persistedExchange, which can help with request size and caching on the edge in Contentful.

Overall benefits are evident, but there should be extra care now when writing new GraphQL requests, as they are going to be cached indefinitely even for dynamic pages due to the Next Cache.
4 changes: 3 additions & 1 deletion gql/graphql-cache.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ declare module 'gql.tada' {
TadaDocumentNode<{ navigationMenuCollection: { [$tada.fragmentRefs]: { NavigationFields: "NavigationMenuCollection"; }; } | null; }, { preview?: boolean | null | undefined; locale?: string | null | undefined; }, void>;
"\n query PageSlugQuery($slug: String, $locale: String, $preview: Boolean) {\n pageCollection(\n locale: $locale\n preview: $preview\n limit: 1\n where: { slug: $slug }\n ) {\n items {\n slug\n }\n }\n }\n ":
TadaDocumentNode<{ pageCollection: { items: ({ slug: string | null; } | null)[]; } | null; }, { preview?: boolean | null | undefined; locale?: string | null | undefined; slug?: string | null | undefined; }, void>;
"\n query PageQuery($slug: String, $locale: String, $preview: Boolean) {\n pageCollection(\n locale: $locale\n preview: $preview\n limit: 1\n where: { slug: $slug }\n ) {\n items {\n topSectionCollection(limit: 10) {\n items {\n ...ComponentHeroBannerFields\n ...ComponentDuplexFields\n }\n }\n }\n }\n }\n ":
"\n query PageQuery($slug: String, $locale: String, $preview: Boolean) {\n pageCollection(\n locale: $locale\n preview: $preview\n limit: 1\n where: { slug: $slug }\n ) {\n items {\n topSectionCollection(limit: 10) {\n items {\n ...ComponentHeroBannerFields\n ...ComponentDuplexFields\n }\n }\n }\n }\n }\n ":
TadaDocumentNode<{ pageCollection: { items: ({ topSectionCollection: { items: ({ __typename?: "Accordion" | undefined; } | { __typename?: "ComponentCta" | undefined; } | { [$tada.fragmentRefs]: { ComponentDuplexFields: "ComponentDuplex"; }; __typename?: "ComponentDuplex" | undefined; } | { __typename?: "ComponentForm" | undefined; } | { [$tada.fragmentRefs]: { ComponentHeroBannerFields: "ComponentHeroBanner"; }; __typename?: "ComponentHeroBanner" | undefined; } | { __typename?: "ComponentInfoBlock" | undefined; } | { __typename?: "ComponentQuote" | undefined; } | { __typename?: "ComponentTextBlock" | undefined; } | null)[]; } | null; } | null)[]; } | null; }, { preview?: boolean | null | undefined; locale?: string | null | undefined; slug?: string | null | undefined; }, void>;
"\n query PageSlugs($locale: String) {\n # Fetch 50 pages. Ideally we would fetch a good sample of most popular pages for pre-rendering,\n # but for the sake of this example we'll just fetch the first 50.\n pageCollection(locale: $locale, limit: 50) {\n items {\n slug\n }\n }\n }\n ":
TadaDocumentNode<{ pageCollection: { items: ({ slug: string | null; } | null)[]; } | null; }, { locale?: string | null | undefined; }, void>;
"\n fragment AssetFields on Asset {\n __typename\n sys {\n id\n }\n contentType\n title\n url\n width\n height\n description\n }\n":
TadaDocumentNode<{ description: string | null; height: number | null; width: number | null; url: string | null; title: string | null; contentType: string | null; sys: { id: string; }; __typename: "Asset"; }, {}, { fragment: "AssetFields"; on: "Asset"; masked: true; }>;
"\n fragment ComponentDuplexFields on ComponentDuplex {\n __typename\n sys {\n id\n }\n headline\n bodyText {\n json\n }\n ctaText\n targetPage {\n ...PageLinkFields\n }\n image {\n ...AssetFields\n }\n imageStyle\n colorPalette\n containerLayout\n }\n ":
TadaDocumentNode<{ containerLayout: boolean | null; colorPalette: string | null; imageStyle: boolean | null; image: { [$tada.fragmentRefs]: { AssetFields: "Asset"; }; } | null; targetPage: { [$tada.fragmentRefs]: { PageLinkFields: "Page"; }; __typename?: "Page" | undefined; } | null; ctaText: string | null; bodyText: { json: unknown; } | null; headline: string | null; sys: { id: string; }; __typename: "ComponentDuplex"; }, {}, { fragment: "ComponentDuplexFields"; on: "ComponentDuplex"; masked: true; }>;
"\n fragment ComponentFormFields on ComponentForm {\n __typename\n sys {\n id\n }\n headline\n form\n }\n":
TadaDocumentNode<{ form: string | null; headline: string | null; sys: { id: string; }; __typename: "ComponentForm"; }, {}, { fragment: "ComponentFormFields"; on: "ComponentForm"; masked: true; }>;
"\n fragment ComponentHeroBannerFields on ComponentHeroBanner {\n __typename\n sys {\n id\n }\n headline\n bodyText {\n json\n links {\n assets {\n block {\n ...AssetFields\n }\n }\n }\n }\n ctaText\n targetPage {\n ...PageLinkFields\n }\n image {\n ...AssetFields\n }\n imageStyle\n heroSize\n colorPalette\n }\n ":
TadaDocumentNode<{ colorPalette: string | null; heroSize: boolean | null; imageStyle: boolean | null; image: { [$tada.fragmentRefs]: { AssetFields: "Asset"; }; } | null; targetPage: { [$tada.fragmentRefs]: { PageLinkFields: "Page"; }; __typename?: "Page" | undefined; } | null; ctaText: string | null; bodyText: { links: { assets: { block: ({ [$tada.fragmentRefs]: { AssetFields: "Asset"; }; } | null)[]; }; }; json: unknown; } | null; headline: string | null; sys: { id: string; }; __typename: "ComponentHeroBanner"; }, {}, { fragment: "ComponentHeroBannerFields"; on: "ComponentHeroBanner"; masked: true; }>;
"\n fragment MenuGroupFields on MenuGroupFeaturedPagesCollection {\n items {\n ...PageLinkFields\n }\n }\n ":
Expand Down
Loading