Skip to content

Fetching Data

Before we do anything too complicated, lets start with some static content pulled from our GraphQL API. Create two files inside of your src/routes directory, +page.gql and +page.tsx+page.jsx:

src/routes/+page.gql
query Info {
species(id: 1) {
pokedexNumber
name
flavor_text
sprites {
front
}
}
}
src/routes/+page.tsx
import { Container, Display, Sprite, Panel } from '~/components'
import type { PageProps } from './$types'
export default function Page({ Info }: PageProps) {
if (!Info.species) {
throw new Error("not found");
}
return (
<Container>
<Panel side="left">
<Display id="species-name">
{Info.species.name}
<span>no.{Info.species.pokedexNumber}</span>
</Display>
<Sprite
id="species-sprite"
src={Info.species.sprites.front}
speciesName={Info.species.name}
/>
<Display id="species-flavor_text">
{Info.species.flavor_text}
</Display>
</Panel>
</Container>
)
}
src/routes/+page.jsx
import { Container, Display, Sprite, Panel } from '~/components'
export default function Page({ Info }) {
if (!Info.species) {
throw new Error('not found')
}
return (
<Container>
<Panel side="left">
<Display id="species-name">
{Info.species.name}
<span>no.{Info.species.pokedexNumber}</span>
</Display>
<Sprite
id="species-sprite"
src={Info.species.sprites.front}
speciesName={Info.species.name}
/>
<Display id="species-flavor_text">{Info.species.flavor_text}</Display>
</Panel>
</Container>
)
}

You’re already starting to see some of the very exciting things Houdini offers. But even in just this one little example there are a few things worth calling out. The first thing to notice is that your project’s directory structure defines your application’s routes. Because you defined this +page.tsx+page.jsx file in src/routes/ it corresponds to the / view of your application. As you’ll see in a few minutes, if you want to define the view of say /hello you would have a +page.tsx+page.jsx defined at src/routes/hello/+page.tsxsrc/routes/hello/+page.jsx.

The second thing to notice is that Houdini isn’t quite like the other GraphQL clients you might have used. Firing a query does not require you to use a hook. Instead, you define your route’s data dependencies right next to the route, in a similarly named +page.gql file. The data for the query is passed as a prop that matches the query’s name (in this case Info). We used it to render some basic information about Bulbasaur using components that were provided in the project’s components directory.

You might have noticed the ~/components import. Houdini configures a ~ path alias that points to your project’s src/ directory, so ~/components is equivalent to src/components. You can use it anywhere in your project to avoid long relative import paths.

And the last thing that might have stuck out to you in this example is that magical ./$types type import. You might be wondering where this file exists, how is typescript resolving it, etc. And the answer is that Houdini leverages a feature in TypeScript known as “type roots”. When Houdini has finished processing your application, it generates types for you that perfectly match your queries and makes them available to you through this magic import. There are a few different types exported from here but for now just know that PageProps defines (as you could have probably guessed) the props for your page.

GraphQL Explained: Queries

A GraphQL query is a string that describes what information you want from the API. For example, the following defines a query named QueryUserInfo. In Houdini, all documents like queries must be named for reasons that will become more clear later.

query CurrentUserInfo {
current_user {
firstName
}
}

The result of the above query might look something like:

{
"data": {
"current_user": {
"firstName": "Bill"
}
}
}

Notice how the fields in the query match the values? The format of the original query is meant to model the response type.

Fields in a query can take arguments to customize their behavior, for example:

query CurrentUserInfo {
current_user {
# specify the format for the date time stamp
lastLogin(format: "YYYY-MM-DD")
}
}

might return something like this:

{
"data": {
"current_user": {
"lastLogin": "2022-12-25"
}
}
}

For more information on GraphQL Queries, this is a good resource.

If everything is set up properly, you should see a message printed in your terminal once you save the file. Behind the scenes, Houdini is constantly validating and processing your queries so you can catch errors as quickly as possible.

Now that you have the necessary files, you should see Bulbasaur’s description. If you are still running into issues, please reach out to us on Discord and we’d be happy to help.

Query Variables

This is a good start but we will need to be able to show information for more species than just Bulbasaur. Let’s set up our application to read the id of the species from the URL. To do that, add a directory named [[id]] and move both +page.gql and +page.tsx+page.jsx inside of it:

Terminal window
## This needs to be run at the root of the project
cd src/routes && mkdir "[[id]]" && mv +page.* "./[[id]]"

The double braces mark an optional parameter so we can render the same view for both / and /1.

GraphQL Explained: Query Variables

All of the queries we’ve seen so far have had static arguments. However, most of the time you will need to give an argument a dynamic value based on something in your application. For example, the text of an input when filtering a list. GraphQL allows us to define references to dynamic values that must be fulfilled when sending the query. These values are known as variables and can be applied to not just queries: mutations and subscriptions too!

Defining variables for your document looks like the following:

query MyQuery($variable1: Boolean) {
myField(argument: $variable1)
}

Notice the ($variable1: Boolean)? That’s how we say that the MyQuery query takes one argument, called $variable1, that is a Boolean. All variables in GraphQL must start with a $ (makes the compiler’s job easier) and are optional by default. In order to mark a variable as required, you have to put ! at the end of the type:

query MyQuery($variable1: Boolean, $variable2: String!) {
myField(arg1: $variable1, arg2: $variable2)
}

Now that we have the actual route defined, we will have to change our query so that it can accept a variable. Doing this is relatively simple, just update the query inside of +page.gql to look like the following:

src/routes/[[id]]/+page.gql
query Info($id: Int! = 1) {
species(id: $id) {
id
pokedexNumber
name
flavor_text
sprites {
front
}
}
}

And that’s it! Notice that the route parameter matched the name of the query variable? Houdini detected that and took care of all of the wiring for you. Pretty cool, huh?

You should be able to navigate to /6 and see Charizard’s information. If you then navigate back to /, there is no value for the [[id]] portion of the url and the query uses its default value of 1.

For completeness, let’s quickly add some buttons to navigate between the different species. First add Link to your $houdini import:

src/routes/[[id]]/+page.tsx
import { Link } from '$houdini'
src/routes/[[id]]/+page.jsx
import { Link } from '$houdini'

Then copy and paste this block as the last child of the Container component:

src/routes/[[id]]/+page.tsx
<Panel side="right">
<nav>
<Link
to="/[[id]]"
params={{ id: Info.species.pokedexNumber - 1 }}
disabled={Info.species.pokedexNumber <= 1}
className={Info.species.pokedexNumber <= 1 ? 'disabled' : undefined}
>
previous
</Link>
<Link to="/[[id]]" params={{ id: Info.species.pokedexNumber + 1 }}>next</Link>
</nav>
</Panel>
src/routes/[[id]]/+page.jsx
;<Panel side="right">
<nav>
<Link
to="/[[id]]"
params={{ id: Info.species.pokedexNumber - 1 }}
disabled={Info.species.pokedexNumber <= 1}
className={Info.species.pokedexNumber <= 1 ? 'disabled' : undefined}
>
previous
</Link>
<Link to="/[[id]]" params={{ id: Info.species.pokedexNumber + 1 }}>
next
</Link>
</nav>
</Panel>

Clicking the next and previous buttons should move you along the list of pokemon.

Before we go too far, it’s worth calling out yet another example of Houdini’s obsession with type safety. That Link component that you imported has been generated to match your specific routes. Houdini knows that the route parameter corresponds to the $id variable in your graphql query and since it’s an Int! in your document, that Link requires you to pass a number for its params. Look at what happens if you pass "1" instead.

Loading State

By default a route doesn’t render until all of its data has resolved, which means the user stares at the previous page (or a blank screen on first load) while the request is in flight. Houdini can do better. With the @loading directive houdini will render the page right away and stream your query data as it arrives. This means the Pokédex shell appears immediately and fills in as the data lands.

It can be kind of tricky to see when you’re running against the local API so before we go about defining the loading state for our app, pass a delay: 500 argument to the species field. This will introduce an artificial delay in the API response so you get a chance to actually see the loading state.

query Info($id: Int! = 1) {
species(id: $id, delay: 500) {
pokedexNumber
name
flavor_text
sprites {
front
}
}
}

Now, to opt-into the loading state behavior, you start to tag fields with @loading. You could tag individual fields if you wanted to tightly control the exact shape that’s generated for you, but most of the time you are perfectly fine to just put a single @loading on the definition:

query Info($id: Int! = 1) @loading {
species(id: $id, delay: 500) {
pokedexNumber
name
flavor_text
sprites {
front
}
}
}

With that in place, we can use isPending from $houdini to identify if we are rendering a loading state or not:

src/routes/[[id]]/+page.tsx
import { isPending } from '$houdini'
import { Shimmer } from '~/components'
// ...
export default function Page({ Info, Info$handle }: PageProps) {
const species = Info.species;
const isLoading = isPending(species);
return (
<Container>
<Panel side="left">
<Display id="species-name">
{isLoading ? (
<>
<Shimmer width="55%" height="30px" />
<Shimmer width="64px" height="20px" />
</>
) : (
<>
{species?.name}
<span>no.{species?.pokedexNumber}</span>
</>
)}
</Display>
<Sprite id="species-sprite" species={species ?? null} />
<Display id="species-flavor_text">
{isLoading ? (
<div
style={{ display: "flex", flexDirection: "column", gap: "6px" }}
>
<Shimmer height="0.8em" />
<Shimmer width="92%" height="0.8em" />
<Shimmer width="60%" height="0.8em" />
</div>
) : (
species?.flavor_text
)}
</Display>
</Panel>
</Container>
);
}
src/routes/[[id]]/+page.jsx
import { isPending } from '$houdini'
import { Shimmer } from '~/components'
// ...
export default function Page({ Info, Info$handle }) {
const species = Info.species
const isLoading = isPending(species)
return (
<Container>
<Panel side="left">
<Display id="species-name">
{isLoading ? (
<>
<Shimmer width="55%" height="30px" />
<Shimmer width="64px" height="20px" />
</>
) : (
<>
{species?.name}
<span>no.{species?.pokedexNumber}</span>
</>
)}
</Display>
<Sprite id="species-sprite" species={species ?? null} />
<Display id="species-flavor_text">
{isLoading ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<Shimmer height="0.8em" />
<Shimmer width="92%" height="0.8em" />
<Shimmer width="60%" height="0.8em" />
</div>
) : (
species?.flavor_text
)}
</Display>
</Panel>
</Container>
)
}

If you refresh your browser with that in place, you should see a loading state while the query resolves. It’s worth keeping in mind that this directive evokes a lot of machinery. Behind the scenes, Houdini sees you have a query with a loading state and wraps the pages that use the query in a suspense boundary with the component as the fallback. When the time comes, Houdini generates a structure for you to build your UI off of, including lists with rows that match your document. isPending can be used at any depth and will walk down until it encounters a leaf field of a loading state. If your document is particularly large you might want to pass a field directly.

Error Handling

Errors are a fact of life when building applications. You might encounter a runtime error from accessing a field on a null or undefined value. Or maybe your API returns an error for a particular input. Regardless of the reason, your application’s error states should be taken just as seriously as your happy path or loading states. To facilitate this, Houdini allows you to define a +error.tsx+error.jsx file in your route tree. This file defines an error boundary that wraps the page (and any subroutes). The component returned from an error file not only receives the same query references as props but it also receives an errors prop containing the thrown error:

import { Container, Display, Panel } from "~/components";
import { ErrorProps } from "./$types";
export default function ErrorView({ errors }: ErrorProps) {
const message =
errors?.map((e) => e.message).join("\n") || "Something went wrong";
return (
<>
<Container>
<Panel side="left">
<Display
id="species-error"
style={{
flexGrow: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
fontSize: "32px",
padding: "24px",
whiteSpace: "pre-wrap",
}}
>
{message}
</Display>
</Panel>
<Panel side="right">{null}</Panel>
</Container>
</>
);
}

With this page defined, navigate to /-1 and confirm that the server’s error has bubbled up to your error component. By default Houdini will return a 500 when it encounters an error in the initial SSR response. To customize the response you can use helpers like notFound() to signal a specific status from within a page. See the Handling Errors guide for the full story.

What’s Next?

Now that you’ve seen the basics of fetching data from the server, we’re going to start to dig a little deeper into how we should be organizing our queries. In the next section we’re going to give our React components the power to define their own data requirements so we don’t have to worry about their concerns when building this route’s query.