Sanity GROQ vs GraphQL
The Problem
Sanity’s own is pretty nice, I’ve given it a good go a few times but ran into one particularly annoying problem: Typed Components. Basically all I want to do is whip up a SectionCardsBasic component and define its props something like:
---
import type { SectionCardsBasicType } from "somewhere-the-types-live";
interface Props {
sectionData: SectionCardsBasicType;
}
const { sectionData } = Astro.props;
---As far as I can tell, there are 4 approaches you can take to get SectionCardsBasicType:
- 1: Get the type by finding where its used in an existing query, e.g. the page query, and then drill down into the query to find the relevant section and extract the type from there.
- 2: Write a dummy groq query that queries for the SectionCardsBasic data shape, run it through codegen, and import the resulting type directly.
- 3: Use something like to define the SectionCardsBasic shape in code, and use that for both validation and typing.
- 4: Define the SectionCardsBasic fragment in GraphQL, run it through codegen, and import the resulting type directly.
These are all valid approaches.
The first is the most straightforward, but it couples the SectionCardsBasic component to a specific query.
The second is a wasted query, we’re now doing a dance just to get types. The third is more flexible, but it you’re defining your data shape in code instead of the schema, which can trip you up. Plus it introduces another dependency.
The fourth is clean and decoupled, but it requires setting up GraphQL and codegen.
In all honesty, setting up GraphQL and codegen can be a nightmare… BUT it was maybe half a day down the drain over 2 years ago and I haven’t touched the config since. So what’s it look like:
Example:
First I define the SectionCardsBasic fragment in GraphQL:
fragment SectionCardsBasic on SectionCardsBasic {
__typename
subheading
headingRaw
cards {
...CardBasic
}
}Then I can use this fragment in my page query:
fragment Page on Page {
__typename
_id
title
slug {
...Slug
}
sections {
...SectionCardsBasic
}
meta {
...Meta
}
}
query PageQuery($slug: String!) {
allPage(where: { slug: { current: { eq: $slug } } }) {
...Page
}
}After running this through codegen, I can import the resulting types and use them directly in my component:
---
import type { SectionCardsBasicFragment } from "sanity/generated/graphql";
interface Props {
section: SectionCardsBasicFragment;
}
const { section }: Props = Astro.props;
---The result is very clean, component friendly, fragments play nice and most importantly our entire data flow from Sanity schema to component is guaranteed to be strictly typed and in sync.
Reference files
If you use the graphql function exported by codegen you get automatic type inference based on the query. Which lets your components where you actually fetch the data look suuuper clean like this:
---
import { sanityClient } from "sanity/client";
import { footerQuery } from "sanity/queries/footer";
const footerQueryResult = await sanityClient.request(footerQuery);
const footerData = footerQueryResult.allFooter[0];
---Here’s the codegen file and my package.json for running codegen so I can just go pnpm gen and it does all the things:
import { generate, type CodegenConfig } from "@graphql-codegen/cli";
import { SANITY_PROJECT_ID } from "./config";
const config: CodegenConfig = {
schema: `https://${SANITY_PROJECT_ID}.api.sanity.io/v1/graphql/${process.env.SANITY_DATASET}/default`,
documents: ["fragments/**/*.gql", "queries/**/*.ts"],
ignoreNoDocuments: true,
generates: {
"generated/": {
preset: "client",
config: {
nonOptionalTypename: true,
extractAllFieldsToTypes: true,
useTypeImports: true,
},
presetConfig: {
fragmentMasking: false,
extractAllFieldsToTypes: true,
},
plugins: [],
},
},
};
await generate(config, true);
process.exit(0);{
"scripts": {
"gen": "pnpm pre-gen && sanity graphql deploy"
}
}{
"scripts": {
"gen": "tsx --env-file='.env' codegen.ts"
}
}Call them both in a single gen command from the monorepo root:
{
"scripts": {
"gen": "pnpm --filter cms gen && pnpm --filter sanity gen"
}
}
Published