Writing GraphQL ​
In gql.tada
, we write our GraphQL documents using the graphql()
function.
NOTE
In the following examples, we’ll import graphql()
from gql.tada
. However, if you’ve previously followed the steps on the “Installation” page to initialize gql.tada
manually, you’ll instead have to import your custom graphql()
function, as returned by initGraphQLTada()
.
Queries ​
When passing a query to graphql()
, it will be parsed in TypeScript’s type system and the schema that’s set up is used to map this document over to a type.
import { graphql } from 'gql.tada';
const PokemonsQuery = graphql(`
query PokemonsList($limit: Int = 10) {
pokemons(limit: $limit) {
id
name
}
}
`);
The PokemonsQuery
variable will have an inferred type that defines the type of the data result of the query. When adding variables, the types of variables are added to the inferred type as well. The resulting type is known as a TypedDocumentNode
and is supported by most GraphQL clients.
When passing a gql.tada
query to a GraphQL client, the type of input variables and result data are inferred automatically. For example, with urql
and React, this may look like the following:
import { useQuery } from 'urql';
import { graphql } from 'gql.tada';
PokemonsQuery carries a type for data and variables:const PokemonsQuery = graphql(`
query PokemonsList($limit: Int = 10) {
pokemons(limit: $limit) {
id
name
}
}
`);
const PokemonsListComponent = () => {
Types for data and variables are applied from PokemonsQuery: const [result] = useQuery({
query: PokemonsQuery,
variables: { limit: 5 },
});
return (
<ul>
{result.data?.pokemons?.map((pokemon) => (
<li key={pokemon?.id}>{pokemon?.name}</li>
))}
</ul>
);
};
The same applies to mutation operations, subscription operations, and fragment definitions.
The graphql()
function will parse your GraphQL definitions, take the first definition it finds and infers its type automatically.
import { graphql, ResultOf, VariablesOf } from 'gql.tada';
The first definition’s types are inferred:const MarkCollectedMutation = graphql(`
mutation MarkCollected($id: ID!) {
markCollected(id: $id) {
id
name
collected
}
}
`);
Inferring the definition’s variables…type variables = VariablesOf<typeof MarkCollectedMutation>;
…and the definition’s result type.type result = ResultOf<typeof MarkCollectedMutation>;
The above example uses the ResultOf
and VariablesOf
types for illustrative purposes. These type utilities may be used to manually unwrap the types of a GraphQL DocumentNode
returned by graphql()
.
Fragments ​
The graphql()
function allows for fragment composition, which means we’re able to create a fragment and spread it into our definitions or other fragments.
Creating a fragment is the same as any other operation definition. The type of the first definition, in this case a fragment, will be used to infer the result type of the returned document:
import { graphql } from 'gql.tada';
const PokemonFragment = graphql(`
fragment Pokemon on Pokemon {
id
name
collected
}
`);
Spreading this fragment into another fragment or operation definition requires us to pass the fragment into a tuple array on the graphql()
function’s second argument.
const PokemonsList = graphql(`
query PokemonsList {
pokemons(limit: 10) {
id
...Pokemon
}
}
`, [PokemonFragment]);
Here we spread our PokemonFragment
into PokemonsList
by passing it into the graphql()
function and then using its name in the GraphQL document.
Fragment Masking ​
However, in gql.tada
a pattern called “Fragment Masking” applies. PokemonsList
’s result type does not contain the name
and collected
field from the spread fragment and instead contains a reference to the PokemonFragment
.
This forces us to unwrap, or rather “unmask”, the fragment first.
import { useQuery } from 'urql';
import { graphql, readFragment } from 'gql.tada';
const PokemonFragment = graphql(`
fragment Pokemon on Pokemon {
id
name
collected
}
`);
const PokemonsList = graphql(`
query PokemonsList {
pokemons(limit: 10) {
id
...Pokemon
}
}
`, [PokemonFragment]);
const PokemonsListComponent = () => {
const [result] = useQuery({ query: PokemonsList });
The data here does not contain our fragment’s fields:const { pokemons } = result.data!;
return pokemons?.map((item) => {
Calling readFragment() unwraps the type of the fragment: const pokemon = readFragment(PokemonFragment, item);
return pokemon?.name;
});
};
When spreading a fragment into a parent definition, the parent only contains a reference to the fragment. This means that we’re isolating fragments. Any spread fragment data cannot be accessed directly until the fragment is unmasked.
import { graphql, readFragment } from 'gql.tada';
const PokemonFragment = graphql(`
fragment Pokemon on Pokemon {
id
name
collected
}
`);
const PokemonQuery = graphql(`
query Pokemon($id: ID!) {
pokemon(id: $id) {
id
...Pokemon
}
}
`, [PokemonFragment]);
const result = await client.query(PokemonQuery, { id: '001' });
const pokemon = readFragment(PokemonFragment, result.data?.pokemon);Pokemon’s data is only accessible once unmasked with readFragment()
PokemonFragment
’s fragment mask in PokemonQuery
is only unmasked and accessible as its plain result type once we call readFragment()
on the fragment mask. In this case, we’re passing data.pokemon
, which is an object containing the fragment mask.
This all only happens and is enforced at a type level, meaning that we don’t incur any overhead during runtime for masking our fragments.
Fragment Composition ​
Fragment Masking is a concept that only exists to enforce proper Fragment Composition.
In a componentized app, fragments may be used to define the data requirements of UI components, which means, we’ll define fragments, colocate them with our components, and compose them into other fragments or our query.
Since all fragments are masked in our types, this colocation is enforced and we maintain our data requirements to UI component relationship.
For example, our PokemonFragment
may be associated with a Pokemon
component rendering individual items:
import { graphql, readFragment, FragmentOf } from 'gql.tada';
export const PokemonFragment = graphql(`
fragment Pokemon on Pokemon {
id
name
collected
}
`);
interface Props {
The component accepts a fragment mask of PokemonFragment: data: FragmentOf<typeof PokemonFragment>;
}
export const PokemonComponent = ({ data }: Props) => {
In the component body we unwrap the fragment mask: const pokemon = readFragment(PokemonFragment, data);
return <li>{pokemon.name}</li>;
};
The FragmentOf
type is used as an input type above. This type accepts our fragment document and creates the fragment mask that a fragment spread would create as well.
We can then use our new PokemonComponent
in our PokemonsListComponent
and compose its PokemonFragment
into our query:
import { graphql } from 'gql.tada';
import { useQuery } from 'urql';
import { PokemonFragment, PokemonComponent } from './Pokemon';
const PokemonsListQuery = graphql(`
query PokemonsList {
pokemons(limit: 10) {
id
...Pokemon
}
}
`, [PokemonFragment]);
export const PokemonsListComponent = () => {
const [result] = useQuery({ query: PokemonsListQuery });
return (
<ul>
{result.data?.pokemons?.map((pokemon) => (
The masked fragment data is accepted as defined by FragmentOf: <PokemonComponent data={pokemon!} key={pokemon?.id} />
))}
</ul>
);
};
Meaning, while we can unmask and use the PokemonFragment
’s data in the PokemonComponent
, the PokemonsListComponent
cannot access any of the data requirements defined by and meant for the PokemonComponent
.