Reusing Parts of a Query
As you’ve seen, we can get pretty far by just using GraphQL queries to define our route’s data requirements. However, as our application grows these would quickly become unmanageable. To illustrate this point, look at how we used the Sprite component earlier (copied below without the unrelated bits):
query Info($id: Int! = 1) { species(id: $id) { name sprites { front } }}<Sprite id="species-sprite" src={Info.species.sprites.front} speciesName={Info.species.name}/><Sprite id="species-sprite" src={Info.species.sprites.front} speciesName={Info.species.name}/>At first it might not be clear what the problem is. Sprite defines some props so we had to look up the necessary information in order to give those props their value. However, for the sake of argument, imagine that Sprite is used in many different views across our application. Every place where it shows up, we will have to remember to ask for these two pieces of data so that we can provide the props with the correct value. On top of that, as Sprite evolves to do more, we will have to make sure that everywhere we use it also asks for the new data. Our route is now tightly coupled to Sprite’s implementation.
Wouldn’t it be nice if we had some way of defining these requirements inside of Sprite so we didn’t have to worry about the exact details and could ensure that the query included whatever information Sprite needs? Well, that’s where GraphQL fragments come to the rescue.
GraphQL Explained: Fragments
It’s safe to skip this section if you are familiar with GraphQL Fragments.
Fragments are a super powerful tool in GraphQL that is commonly overlooked. In short, they allow us to describe a selection of fields of a given type without having a concrete instance of that type. They look like this:
fragment MyFragment on Species { name flavor_text}This fragment acts as a reusable bit of query so anywhere we have a field in our document that returns a Species we can ask for this data by appending ... to the fragment name in a selection.
Using Fragments
Defining a fragment inside of your component looks a lot like the query from before. Let’s see this in action by updating the Sprite component to look like this:
import { useFragment, graphql } from '$houdini'import type { SpriteInfo } from '$houdini'
export function Sprite({ species, id, className,}: { species: SpriteInfo | null id?: string className?: string}) { const info = useFragment(species, graphql(` fragment SpriteInfo on Species { name sprites { front } } `))
if (!info) return null
return ( <div id={id} className={`sprite${className ? ` ${className}` : ''}`}> <img height="100%" src={info.sprites.front} alt={`${info.name} sprite`} /> </div> )}import { useFragment, graphql } from '$houdini'
export function Sprite({ species, id, className, }) { const info = useFragment(species, graphql(` fragment SpriteInfo on Species { name sprites { front } } `))
if (!info) return null
return ( <div id={id} className={`sprite${className ? ` ${className}` : ''}`}> <img height="100%" src={info.sprites.front} alt={`${info.name} sprite`}/> </div> )}Next we have to go back to the route and put this fragment to use. Update the query inside of src/routes/[[id]]/+page.gql to look like:
query Info($id: Int! = 1) { species(id: $id) { id name flavor_text ...SpriteInfo }}And change the instance of Sprite to look like:
<Sprite id="species-sprite" species={Info.species} /><Sprite id="species-sprite" species={Info.species}/>Once that’s all done, there shouldn’t be any noticeable change in your browser.
What Just Happened?
That was pretty quick so let’s review what we just did:
-
We defined a new fragment in the
Spritecomponent which ensured that its parent always delivered the two pieces of information it needs: the front image source and the name of the species. -
Instead of asking for those bits of data directly as two individual props, the component now has a single prop,
species, which we pass touseFragmentin order to get the data we need. Notice we don’t use this prop for anything in our component except to pass it into Houdini. This might seem surprising but it ensures we are only using data we asked for in our fragment. -
We then updated the route’s query to use the fragment we defined in the component and passed the
Info.speciesreference we got from the query into ourSpritecomponent as the new prop.
Notice how our route is no longer tightly coupled to any of Sprite’s implementation? All that the route knows is that Sprite needs a Species, so it just had to connect the dots and let Sprite take care of the rest.
Houdini enables us to use fragments as a way to colocate our component’s data requirements next to the actual logic that relies on the fields. This not only saves us the extra typing every time we render a Sprite but it also lets us grow this component without worrying about updating every instance of it.
Composing Fragments
It’s worth mentioning explicitly that you are free to mix and match fragments however you want. Fragments can have fragments inside of them and the same fragment can show up multiple times in a single component. To illustrate this, let’s add a section in our Pokédex that shows the different evolved forms of the species we’re looking at.
Before we add anything to our route, let’s update the component defined in src/components/SpeciesPreview to use the new fragment we just added to Sprite. You might want to give it a try without looking ahead but either way, here’s what it should look like now:
import { useFragment, graphql, Link } from '$houdini'import type { SpeciesPreview as SpeciesPreviewFragment } from '$houdini'import { Sprite, Display } from '.'import { SpeciesPreviewNumber } from './SpeciesPreviewNumber'
export function SpeciesPreview({ species, number }: { species: SpeciesPreviewFragment, number: number }) { const data = useFragment(species, graphql(` fragment SpeciesPreview on Species { name id pokedexNumber
...SpriteInfo } `))
if (!data) return null
return ( <Link to="/[[id]]" params={{ id: data.pokedexNumber }} className="no-underline"> <SpeciesPreviewNumber value={number} /> <Sprite species={data} className="h-[102px] w-[102px]" /> <Display>{data.name}</Display> </Link> )}import { useFragment, graphql, Link } from '$houdini'import { Sprite, Display } from '.'import { SpeciesPreviewNumber } from './SpeciesPreviewNumber'
export function SpeciesPreview({ species, number }) { const data = useFragment(species, graphql(` fragment SpeciesPreview on Species { name id pokedexNumber
...SpriteInfo } `))
if (!data) return null
return ( <Link to="/[[id]]" params={{ id: data.pokedexNumber }} className="no-underline"> <SpeciesPreviewNumber value={number}/> <Sprite species={data} className="h-[102px] w-[102px]"/> <Display>{data.name}</Display> </Link> )}Once that’s done, go back to the route we’ve been working with and update the query to look like this:
query Info($id: Int! = 1) { species(id: $id) { id name flavor_text ...SpriteInfo evolution_chain { id ...SpeciesPreview } }}Next, add imports for SpeciesPreview and SpeciesPreviewPlaceholder from the component directory, then add the two const declarations at the top of your component function and place the div above the nav in the right panel:
// add these at the top of the Page function bodyconst evolutionChain = Info.species?.evolution_chain ?? []const placeholderCount = Math.max(0, 3 - evolutionChain.length)
// ...
{/* add this above the nav in the right panel */}<div id="species-evolution-chain"> {evolutionChain.map((s, i) => ( <SpeciesPreview key={s.id} species={s} number={i + 1} /> ))} {/* if there are fewer than three species in the chain, leave a placeholder */} {Array.from({ length: placeholderCount }).map((_, i) => ( <SpeciesPreviewPlaceholder key={evolutionChain.length + i} number={evolutionChain.length + i + 1} /> ))}</div>// add these at the top of the Page function bodyconst evolutionChain = Info.species?.evolution_chain ?? []const placeholderCount = Math.max(0, 3 - evolutionChain.length)
// ...
{ /* add this above the nav in the right panel */ }<div id="species-evolution-chain"> {evolutionChain.map((s, i) => (<SpeciesPreview key={s.id} species={s} number={i + 1}/>))} {/* if there are fewer than three species in the chain, leave a placeholder */} {Array.from({ length: placeholderCount }).map((_, i) => (<SpeciesPreviewPlaceholder key={evolutionChain.length + i} number={evolutionChain.length + i + 1}/>))}</div>You should now go and confirm that you can see all of the forms associated with a species’ evolution chain. Pretty cool, huh?
Fragments are your building blocks
This point is kind of hard to get across but maybe you can already see what we mean. In this last example we built a component that needed a particular selection of data and also used another component which itself had its own requirements. By giving each component their own fragment, we were able to maintain a very loose coupling between our components and not have to leave it up to coincidence that the correct information was part of the route’s query.
You should think of fragments as the main building block for describing your component’s data requirements in Houdini. As your project grows, your component library will be filled with components that are smart enough to ask for exactly the information they need. Can you imagine how this could apply to a UserAvatar component that seems to constantly evolve from needing just first and last initials, to also wanting the user’s email, and finally some user-configured favorite color?
What’s Next?
This is just the tip of the iceberg when it comes to fragments but hopefully you can appreciate just how big of an upgrade they are to your component library. It’s time to show off a few of Houdini’s “advanced” features. We’re going to start by looking at how Houdini keeps our UI up to date as we trigger mutations that update our server state.