Skip to content

Commit

Permalink
Merge pull request #35 from ffw-us/feat/urql-migration
Browse files Browse the repository at this point in the history
feat: replace graphql-request with urql
  • Loading branch information
asgorobets authored Apr 23, 2024
2 parents 1059095 + 6d5b2d8 commit 6501e5e
Show file tree
Hide file tree
Showing 9 changed files with 431 additions and 307 deletions.
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

0 comments on commit 6501e5e

Please sign in to comment.