Skip to content

Queries

Load data from the server and subscribe to any changes that mutations, subscriptions, and other queries bring in.

Defining a Query

Queries can be defined inline using graphql from $houdini, or in an external .gql file that Houdini picks up automatically and generates a typed store class for.

Inline:

src/lib/MyComponent.svelte
<script>
import { graphql } from '$houdini'
const store = graphql(`
query MyProfileInfo {
viewer {
firstName
}
}
`)
</script>

External .gql file:

src/routes/myRoute/+page.gql
query MyProfileInfo {
viewer {
firstName
}
}
src/routes/myRoute/+page.svelte
<script>
import { MyProfileInfoStore } from '$houdini'
const store = new MyProfileInfoStore()
</script>

Store Fields

A query store holds the following fields, accessed as $store.fieldName:

  • data: the result of the query, updated as mutations, subscriptions, and other queries bring in more recent values
  • fetching: true while a network request is in flight
  • errors: any errors returned by the server
  • partial: true if the result is a partial cache hit
  • variables: the variables used in the last request

Store Methods

store.fetch(params?) sends the query and returns a promise that resolves with the result. It accepts variables, policy, metadata, and blocking.

Fetching Data

Since fetch is just an async function, you can call it anywhere Svelte supports async.

The cleanest pattern for loading data in a component:

src/routes/myRoute/+page.svelte
<script>
import { graphql } from '$houdini'
const store = graphql(`
query MyProfileInfo {
viewer {
firstName
}
}
`)
</script>
{#await store.fetch()}
Loading...
{:then}
{$store.data.viewer.firstName}
{/await}

Using $effect

When the query depends on reactive state, such as a prop, a search input, or a selected filter, use $effect to re-fetch whenever the dependency changes. This is the right pattern for non-SSR components like typeahead search or prop-driven queries:

src/lib/UserProfile.svelte
<script>
import { graphql } from '$houdini'
let { id } = $props()
const store = graphql(`
query UserProfile($id: ID!) {
user(id: $id) {
name
}
}
`)
$effect(() => {
store.fetch({ variables: { id } })
})
</script>
{#if $store.fetching}
Loading...
{:else}
{$store.data.user.name}
{/if}

Using SvelteKit load

For routes where you want fetching tied to navigation, use the generated load_<QueryName> helper in a universal +page.ts+page.js load function:

src/routes/myRoute/+page.ts
import { load_MyProfileInfo } from '$houdini'
import type { PageLoad } from './$houdini'
export const load: PageLoad = async (event) => {
return {
...(await load_MyProfileInfo({ event }))
}
}
src/routes/myRoute/+page.svelte
<script lang="ts">
import type { PageData } from './$houdini'
let { data }: { data: PageData } = $props()
let { MyProfileInfo } = $derived(data)
</script>
{$MyProfileInfo.data.viewer.firstName}

For multiple queries in parallel, use loadAll:

src/routes/myRoute/+page.ts
import { loadAll, load_QueryA, load_QueryB } from '$houdini'
import type { PageLoad } from './$houdini'
export const load: PageLoad = async (event) => {
return {
...(await loadAll(
load_QueryA({ event }),
load_QueryB({ event })
))
}
}

Query Variables

Pass variables directly to fetch:

{#await store.fetch({ variables: { id } })}
Loading...
{:then}
{$store.data.user.name}
{/await}

When using load_<QueryName>, pass them there instead:

src/routes/myRoute/+page.ts
export const load: PageLoad = async (event) => {
return {
...(await load_MyQuery({
event,
variables: { id: event.params.id }
}))
}
}

Runtime Scalars

Runtime scalars let you define custom scalar types whose values are resolved at runtime (from the current session, request context, or any other source) rather than passed explicitly by the caller. This is useful for variables like the current user’s organization ID that are always available from session state and would otherwise need to be threaded through every query manually.

Define runtime scalars in your houdini.config.tshoudini.config.js under features.runtimeScalars:

houdini.config.js
export default {
features: {
runtimeScalars: {
OrganizationFromSession: {
type: 'ID',
resolve: ({ session }) => session.organization
}
}
}
}

The resolve function receives the same context as your client’s fetchParams, including session, metadata, and fetch.

Once defined, use the scalar as a variable type in any query. Houdini calls the configured resolve function automatically, so there’s no need to pass the value at the call site:

query OrganizationInfo($id: OrganizationFromSession!) {
organization(id: $id) {
name
}
}

The scalar value is resolved fresh on every fetch, so if the session changes the next request picks up the new value automatically.

Loading States

Use $store.fetching to show a loading indicator during refetches:

{#if $store.fetching}
Loading...
{:else}
{$store.data.viewer.firstName}
{/if}

For more sophisticated skeleton UIs that avoid duplicating your layout, see the Loading States guide.

Passing Metadata

Pass metadata to fetch to forward custom values to your network function:

<script>
store.fetch({ metadata: { key: 'value' } })
</script>
src/client.ts
export default new HoudiniClient({
url: '...',
fetchParams({ metadata }) {
// use metadata here
}
})

Deduplication

By default, nothing stops the same operation from running multiple times simultaneously. The @dedupe directive gives us control over that behavior:

query UserProfile($id: ID!) @dedupe {
user(id: $id) {
name
}
}

With no arguments, @dedupe prevents a second request from firing if an identical one is already in-flight. To cancel the first request instead of dropping the second, pass cancelFirst: true:

query UserProfile($id: ID!) @dedupe(cancelFirst: true) {
user(id: $id) {
name
}
}

The match argument controls what counts as "identical":

  • Operation: dedupe if any execution of this operation is pending, regardless of variables (default)
  • Variables: dedupe only if the variables also match
  • None: never dedupe