Skip to content

Query

Load data from the server and subscribe to any changes of fields we detect from mutations, subscriptions, and other queries.

SvelteKit example

src/routes/myRoute/+page.gql
query MyProfileInfo {
viewer {
firstName
avatar
}
}
src/routes/myRoute/+page.svelte
<script lang="ts">
import type { PageData } from './$houdini'
export let data: PageData
// pull the store reference from the route props
$: ({ MyProfileInfo } = data)
</script>
{$MyProfileInfo.data.viewer.firstName}
src/routes/myRoute/+page.svelte
<script>
export let data
// pull the store reference from the route props
$: ({ MyProfileInfo } = data)
</script>
{$MyProfileInfo.data.viewer.firstName}

This example takes advantage of Houdini’s powerful load generator and is just one of many ways to get access to a store that can drive your server-side rendered routes. For more information, check out the Working with GraphQL guide.

Svelte store fields

A query store holds an object with the following fields that accessed like $store.data:

  • data contains the result of the query. It’s value will update as mutations, subscriptions, and other queries provide more recent information.
  • fetching contains the loading state (true or false). See the Loading States section for more info for fine-grained loading states.
  • errors contains any error values that occur for a query found outside of a route component (ie, not defined in src/routes). If you want to use this for managing your errors, you should enable the quietQueryError configuration option.
  • partial contains a boolean that indicates if the result has a partial match
  • variables contains the variables that were sent in the last query, mutation or subscription request.

Svelte store methods

A query store has the following methods that invoked without the $, ie store.fetch(...):

  • fetch expects store.fetch({ variables: {/* variables */} });
  • fetch is a function that can be used to load the most recent data from your API (subject to its Cache Policy). If you want to force the request to always resolve against the API, set policy: 'NetworkOnly'.

Automatic Loading

SvelteKit automatic loading

As described in the Working with GraphQL guide, there are 3 things that you can do to get houdini to create a load function for your route:

  • export _houdini_load from your +page.js file
  • adding @load to an inline query in +page.svelte (remember to use $:)
  • +page.gql file in your route directory

Regardless of which pattern you are using, you will quickly need to customize your query’s behavior. This could be because you need to add variables to the query, or maybe you need to perform some logic before and/or after the data is fetched.

SvelteKit query variables

The simplest way to pass values for your query inputs is to use route parameters. If houdini sees that the name for a route parameter matches the name of a query’s input, it will attempt to parse the value and pass it to your query.

Sometimes your query variables cannot be directly pulled from your route’s path. In those cases, you can export a function named after your query from +page.js files to perform whatever logic you need. This function takes the same arguments passed to the load function described in the SvelteKit docs. To return an error, or force a redirect, you can use the same utilities exported from @sveltejs/kit. Here is a modified example from the source repository:

src/routes/[filter]/+page.ts
import { graphql } from '$houdini'
import { error } from '@sveltejs/kit'
import type { AllItemsVariables } from './$houdini'
export const _houdini_load = graphql(`
query AllItems($completed: Boolean) {
items(completed: $completed) {
id
text
}
}
`)
// This is the function for the AllItems query.
// Query variable functions must be named _<QueryName>Variables.
export const _AllItemsVariables : AllItemsVariables = (event) => {
// make sure we recognize the value
if (!['active', 'completed'].includes(event.params.filter)) {
throw error(400, 'invalid filter')
}
return {
completed: event.params.filter === 'completed'
}
}
src/routes/[filter]/+page.js
import { graphql } from '$houdini'
import { error } from '@sveltejs/kit'
export const _houdini_load = graphql(`
query AllItems($completed: Boolean) {
items(completed: $completed) {
id
text
}
}
`)
// This is the function for the AllItems query.
// Query variable functions must be named _<QueryName>Variables.
export const _AllItemsVariables = (event) => {
// make sure we recognize the value
if (!['active', 'completed'].includes(event.params.filter)) {
throw error(400, 'invalid filter')
}
return {
completed: event.params.filter === 'completed'
}
}

Runtime Scalars

Houdini also supports an experimental API for passing query variables from your application’s session using something we call Runtime Scalars. To enable and configure this feature, you must provide an object to the runtimeScalars feature config:

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

With that defined, you can now use it as a scalar in a query and Houdini will call the configured resolve function to generate the variable value:

+page.gql
query OrganizationInfo($id: OrganizationFromSession!) {
organization(id: $id) {
name
}
}

Please keep in mind that this feature is still considered experimental and could change with any minor version.

SvelteKit hooks

Sometimes you will need to add additional logic to a component’s query. For example, you might want to check if the current session is valid before a query is sent to the server. In order to support this, Houdini will look for hook functions defined in your +page.js files.

_houdini_beforeLoad

Called before Houdini executes load queries against the server. You can expect the same arguments as SvelteKit’s load function.

src/routes/myRoute/+page.ts
import { graphql } from '$houdini'
import { redirect } from '@sveltejs/kit'
import type { BeforeLoadEvent } from './$houdini'
export const _houdini_load = graphql(`
query AllItems($completed: Boolean) {
items(completed: $completed) {
id
text
}
}
`)
export function _houdini_beforeLoad({ page, session }: BeforeLoadEvent) {
if (!session.authenticated) {
throw redirect(302, '/login')
}
return {
message: 'Number of items:'
}
}
src/routes/myRoute/+page.js
import { graphql } from '$houdini'
import { redirect } from '@sveltejs/kit'
export const _houdini_load = graphql(`
query AllItems($completed: Boolean) {
items(completed: $completed) {
id
text
}
}
`)
export function _houdini_beforeLoad({ page, session }) {
if (!session.authenticated) {
throw redirect(302, '/login')
}
return {
message: 'Number of items:'
}
}

_houdini_afterLoad

Called after Houdini executes load queries against the server. You can expect the same arguments as SvelteKit’s load function, plus an additional data property referencing query result data. Keep in mind that if you define this hook, Houdini will have to block requests to the route in order to wait for the result. For more information about blocking during navigation, check out the deep dive in this section of the query store docs.

src/routes/myRoute/+page.ts
import { graphql } from '$houdini'
import { error } from '@sveltejs/kit'
import type { AfterLoadEvent, MyProfileVariables } from './$houdini'
export const _houdini_load = graphql(`
query MyProfile($id: ID!) {
profile(id: $id) {
name
}
}
`)
export const _MyProfileVariables: MyProfileVariables = ({ params }) => {
return { id: params.id }
}
export function _houdini_afterLoad({ data }: AfterLoadEvent) {
if (!data.MyProfile) {
throw error(404)
}
return {
message: "Hello I'm"
}
}
src/routes/myRoute/+page.js
import { graphql } from '$houdini'
import { error } from '@sveltejs/kit'
export const _houdini_load = graphql(`
query MyProfile($id: ID!) {
profile(id: $id) {
name
}
}
`)
export const _MyProfileVariables = ({ params }) => {
return { id: params.id }
}
export function _houdini_afterLoad({ data }) {
if (!data.MyProfile) {
throw error(404)
}
return {
message: "Hello I'm"
}
}

_houdini_onError

If defined, the load function will invoke this function instead of throwing an error when an error is received. It receives three inputs: the load event, the inputs for each query, and the error that was encountered. Just like the other hooks, onError can return an object that provides props to the route. If you do define this hook, Houdini will have to block requests to the route in order to wait for the result. For more information about blocking during navigation, check out the deep dive in this section of the query store docs.

src/routes/myRoute/+page.ts
import { graphql } from '$houdini'
import type { OnErrorEvent } from './$houdini'
export const _houdini_load = graphql(`
query MyProfile {
profile {
name
}
}
`)
export function _houdini_onError({ error }: OnErrorEvent) {
throw this.redirect(307, '/login')
}
src/routes/myRoute/+page.js
import { graphql } from '$houdini'
export const _houdini_load = graphql(`
query MyProfile {
profile {
name
}
}
`)
export function _houdini_onError({ error }) {
throw this.redirect(307, '/login')
}

SvelteKit Manual Loading

If you are writing your route’s load functions by hand, you’ll want to instantiate a store, calls its fetch method and return the store to your route. In order to streamline this, houdini provides a function for each of your stores that you can use to render your route on the server. These functions take the same parameters as fetch:

import { load_MyQuery } from '$houdini'
import type { PageLoad } from './$houdini'
export const load: PageLoad = async (event) => {
return {
...(await load_MyQuery({ event }))
}
}
import { load_MyQuery } from '$houdini'
export const load = async (event) => {
return {
...(await load_MyQuery({ event }))
}
}

In case you were wondering, the load_ prefix is there so you can autocomplete your loads by just typing load_<tab>. Anyway, with this in place, your route will receive props for each of the stores that you have loaded:

src/routes/myRoute.svelte
<script lang="ts">
import type { PageData } from './$houdini'
export let data: PageData
$: ({ MyQuery } = data)
</script>
{$MyQuery.data.value}
src/routes/myRoute.svelte
<script>
export let data
$: ({ MyQuery } = data)
</script>
{$MyQuery.data.value}

If your query has variables, you can pass them straight to the loader:

import { load_MyQuery } from '$houdini'
import type { PageLoad } from './$houdini'
export const load: PageLoad = async (event) => {
return {
...await load_MyQuery({
event,
variables: { variable1: 'value' }
})
}
}
import { load_MyQuery } from '$houdini'
export const load = async (event) => {
return {
...await load_MyQuery({
event,
variables: { variable1: 'value' }
})
}
}

Loading multiple stores simultaneously

Be careful when loading multiple stores at once.

Each of your loads should be performed inside of an await Promise.all so you perform the queries in parallel. This can get rather tedious so houdini provides a loadAll function you can use to simplify this process:

import { loadAll, load_MyQuery, load_MyOtherQuery } from '$houdini'
import type { PageLoad } from './$houdini'
export const load: PageLoad = async (event) => {
return {
...(await loadAll(
load_MyQuery({ event }),
load_MyOtherQuery({ event })
))
}
}
import { loadAll, load_MyQuery, load_MyOtherQuery } from '$houdini'
export const load = async (event) => {
return {
...(await loadAll(load_MyQuery({ event }), load_MyOtherQuery({ event })))
}
}

If you want to load the same store twice or customize the props that will get passed to your route, you can pass any combination of single loaders and objects that map the result to a prop value:

import { loadAll, load_MyQuery, load_MyOtherQuery } from '$houdini'
import type { PageLoad } from './$houdini'
export const load: PageLoad = async (event) => {
return {
...(await loadAll(load_MyQuery({ event }), {
propA: load_MyOtherQuery({ event, variables: { foo: 'A' } }),
propB: load_MyOtherQuery({ event, variables: { foo: 'B' } })
}))
}
}
import { loadAll, load_MyQuery, load_MyOtherQuery } from '$houdini'
export const load = async (event) => {
return {
...(await loadAll(load_MyQuery({ event }), {
propA: load_MyOtherQuery({ event, variables: { foo: 'A' } }),
propB: load_MyOtherQuery({ event, variables: { foo: 'B' } })
}))
}
}

The above example will provide 3 props to your route: MyQuery containing an instance of MyQuery, propA with an instance of MyOtherQuery with the variables {foo: "A"} loaded, and propB with an instance of MyOtherQuery with the variables {foo: "B"} loaded.

Svelte Loading States

By default, SvelteKit blocks navigation while your queries fetch. While this is great for rendering your application for the initial request, it makes it impossible to build loading states for client-side navigation that have been shown to improve perceived loading time.

In order to circumvent this, your query’s do not actually block by default. For some applications, this is not the desired behavior and so Houdini allows you to configure the behavior the blocking nature of your query’s fetches using a combination of directives and configuration values.

Ways to configure blocking behavior

  1. By configuration with the parameter defaultRouteBlocking.
  2. If you have throwOnError set with operations “all” or “query”, Houdini will always block.
  3. If you have a _houdini_afterLoad or _houdini_onError set, Houdini will always block.
  4. By passing the arg blocking in the fetch function you can overwrite the behavior.

Blocking on Load

import { redirect } from '@sveltejs/kit'
import type { PageLoad } from './$houdini'
export const load: PageLoad = async (event) => {
// blocking:true makes this await "real"
const { data } = await MyStore.fetch({ event, blocking: true })
// check the response of the query and redirect when appropriate
if (!data.fieldName) {
throw redirect(300, '/not-field-name')
}
return {}
}
import { redirect } from '@sveltejs/kit'
export const load = async (event) => {
// blocking:true makes this await "real"
const { data } = await MyStore.fetch({ event, blocking: true })
// check the response of the query and redirect when appropriate
if (!data.fieldName) {
throw redirect(300, '/not-field-name')
}
return {}
}

A Simple Loading State

The easiest way to detect when your query is waiting on a network request is to look at the fetching value in the response:

src/routes/myRoute.svelte
<script lang="ts">
import type { PageData } from './$houdini'
export let data: PageData
$: ({ MyQuery } = data)
</script>
{#if $MyQuery.fetching}
loading...
{:else}
{$MyQuery.data....}
{/if}
src/routes/myRoute.svelte
<script>
export let data
$: ({ MyQuery } = data)
</script>
{#if $MyQuery.fetching}
loading...
{:else}
{$MyQuery.data....}
{/if}

Fine-Grain Loading States

For an overview of building reusable loading states that avoid duplicating your applications structure multiple times, please visit the Loading State guide.

Svelte component queries

Not every query will be inside of a route. In this situations, you can either work with your query stores directly, or use the same graphql function to instruct the plugin to generate a client-side equivalent of the load function.

src/lib/MyComponent.svelte
<script lang="ts">
import { graphql } from '$houdini'
import type { MyComponentQueryVariables } from './$houdini'
export const _MyComponentQueryVariables: MyComponentQueryVariables = ({ props }) => {
return { id: props.id }
}
const store = graphql(`
query MyComponentQuery($id: ID!) @load {
user(id: $id) {
id
}
}
`)
</script>
{$store.data.value}
src/lib/MyComponent.svelte
<script>
import { graphql } from '$houdini'
export const _MyComponentQueryVariables = ({ props }) => {
return { id: props.id }
}
const store = graphql(`
query MyComponentQuery($id: ID!) @load {
user(id: $id) {
id
}
}
`)
</script>
{$store.data.value}

Using the @load directive instructs Houdini to load that query automatically by generating the appropriate load function.

SvelteKit server only load

Using a query store inside of server only load function looks very similar to the usual load function: just pass the event you are handed in your route function:

import { MyQueryStore } from '$houdini'
import type { PageServerLoad } from './$houdini'
export const load: PageServerLoad = async (event) => {
const myQuery = new MyQueryStore()
const { data } = await myQuery.fetch({ event })
return { data }
}
import { MyQueryStore } from '$houdini'
export const load = async (event) => {
const myQuery = new MyQueryStore()
const { data } = await myQuery.fetch({ event })
return { data }
}

Note that like this you will not have a store in your svelte file but directly your json data in export let data.

SvelteKit API route / endpoint

Using a query store inside of an API route (or endpoint) looks like this:

import { json } from '@sveltejs/kit';
import { MyQueryStore } from '$houdini'
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async (event) => {
const myQuery = new MyQueryStore()
const { data } = await myQuery.fetch({ event })
return json(data);
}
import { json } from '@sveltejs/kit'
import { MyQueryStore } from '$houdini'
export const GET = async (event) => {
const myQuery = new MyQueryStore()
const { data } = await myQuery.fetch({ event })
return json(data)
}

Note that like this you will not have a store in your svelte files, you will need to fetch data on your own. More info in the SvelteKit Doc.

Passing Metadata

Sometimes you need to do something very custom for a specific route. Maybe you need special headers or some other contextual information. Whatever the case may be, you can pass a metadata parameter to fetch:

import type { PageLoad } from './$houdini'
export const load: PageLoad = async (event) => {
return {
...(await load_MyStore({event, metadata: { key: 'value' }}))
}
}
export const load = async (event) => {
return {
...(await load_MyStore({ event, metadata: { key: 'value' } }))
}
}

This value will get forwarded to the network function in your client definition, usually found in src/client.js:

import type { RequestHandler } from './$houdini'
const requestHandler: RequestHandler = async ({ fetch, text, variables, session, metadata }) => {
// do anything with metadata inside of here
}
// Export the Houdini client
export default new HoudiniClient(requestHandler)
const requestHandler = async ({ fetch, text, variables, session, metadata }) => {
// do anything with metadata inside of here
}
// Export the Houdini client
export default new HoudiniClient(requestHandler)

Paginated Queries

If the query contains the pagination directive then the generated store will have extra fields/methods according to the pagination strategy and direction. For more information about Svelte pagination in general, check out this guide.

Cursor-based pagination

If the decorated field implements cursor-based pagination the query store will be generated with an extra methods that loads more data in both directions and a field pointing to a store with the current pageInfo object. This extra field can be used to track if there are more pages to load:

Paginated query

query MyFriends {
viewer {
friends(first: 10) @paginate(name: "My_Friends") {
edges {
node {
id
}
}
}
}
}

Generated store additions

type MyFriendsStore = QueryStore & {
loadNextPage(
pageCount?: number,
after?: string | number,
houdiniContext?: HoudiniContext,
): Promise<void>
loadPreviousPage(
pageCount?: number,
before?: string | number,
houdiniContext?: HoudiniContext,
): Promise<void>
pageInfo: Readable<PageInfo>
}

Offset/limit Pagination

If the decorated field implements offset/limit pagination and provides a limit argument, the query store will be generated with an extra methods that lets it load more pages after the one the current one:

Paginated query

query MyFriends {
viewer {
friends(limit: 10) @paginate(name: "My_Friends") {
id
}
}
}

Generated store additions

type MyFriendsStore = QueryStore & {
loadNextPage(
limit?: number,
offset?: number,
houdiniContext?: HoudiniContext,
): Promise<void>
}

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