Caching Data

Houdini’s cache behavior can be customized with the @cache directive:

query AllItems @cache(policy: CacheAndNetwork) {
	items {
		id
		text
	}
}

There are a few different policies that can be specified:

  • CacheOrNetwork will first check if a query can be resolved from the cache. If it can, it will return the cached value and only send a network request if data was missing. If you have opted into partial data with @cache(partial: true) and the result contains partial data (some but not all of the data is available in the cache), you will get the partial data first and then a network request will trigger - almost like it behaves like CacheAndNetwork.
  • CacheAndNetwork will use cached data if it exists and always send a network request after the component has mounted to retrieve the latest data from the server. The cached portion of this might contain partial data if you opt-in.
  • NetworkOnly will never check if the data exists in the cache and always send a network request
  • CacheOnly will only ever return cache data which can be a partial response
  • NoCache is like NetworkOnly but also will never write to the cache

The default cache policy as well as other parameters can be changed in your config file.

Optimistic Responses

A lot of the time we know the side effects of a mutation assuming everything goes right. For example, a toggleItem mutation in a todo list will invert the value of the checked field of a particular item. In these situations, we don’t have to wait for a mutation to resolve in order to apply the update to the cache. Instead, we can assume that it will succeed and provide an “optimistic response” for the mutation with the second argument to a mutation handler:

<script lang="ts">
    import { ToggleItemStore }  from '$houdini'

    export let itemID: number

    const update = new ToggleItemStore()
</script>

<button
    on:click={() => update.mutate({ id: itemID }, {
            optimisticResponse: {
                toggleItem: {
                    item: {
                        id: '1',
                        checked: true
                    }
                }
            }
        })
    }

>
    toggle item
</button>
<script>
    import { ToggleItemStore } from '$houdini'
    
    /* @type { number } */
    export let itemID
    
    const update = new ToggleItemStore()
</script>

<button
    on:click={() => update.mutate({ id: itemID }, {
            optimisticResponse: {
                toggleItem: {
                    item: {
                        id: '1',
                        checked: true
                    }
                }
            }
        })
    }

>
    toggle item
</button>

When the mutation resolves, the old values will be erased entirely and the new values will be committed to the cache. If instead the mutation fails, the optimistic changes will be reverted and the handler’s promise will reject with the error message as usual.

Remember to always request and specify an id when dealing with optimistic responses so that the cache can make sure to update the correct records. Also, it’s worth mentioning that you don’t have to provide a complete response for an optimistic value, the cache will write whatever information you give it (as long as its found in the mutation body).

Why is typescript missing fields?

If you are using typescript, you might notice that the generated types for optimistic responses do not include any fields from fragments that you might have spread in. While surprising at first, this is by design. We believe that it is a mistake to tightly couple the invocation of the mutation with a fragment that’s defined in some random file and whose definition might change unknowingly. If it did change, there would be a nasty error when the runtime tries to look up the schema information so the generated types are trying to guide you towards a safer practice.

There’s no harm in duplicating a field that is part of a fragment so if you are going to provide an optimistic value, you should explicitly add those fields to the selection set of the mutation.

Partial Data

As your users navigate through your application, their cache will build up with the data that they encounter. This means that a lot of the times, they will have already seen at least some the data that a new view wants. Houdini can be told to render a view even if we have only seen a subset of the necessary data using the @cache directive:

query AllItems @cache(partial: true) {
	items {
		id
		text
	}
}

This means that you will have to deal with a lot of null states in order to accommodate the missing data but it can result in a much snappier interface for your users. Keep in mind that Houdini tries its hardest to keep your data “correct”. This means that if a value is missing that isn’t allowed to be null according to your project’s schema it will turn the entire object null (assuming that’s valid). This behavior is described by the GraphQL spec.

The default partial state can be configured with the defaultPartial value in your config file:

houdini.config.js
export default {
    // ...
    defaultPartial: true
}

Data Retention

Houdini will retain a query’s data for a configurable number of queries (default 10). For a concrete example, consider an example app that has 3 routes. If you load one of the routes and then click between the other two 5 times, the first route’s data will still be resolvable (and the counter will reset if you visit it). If you then toggle between the other routes 10 times and then try to load the first route, a network request will be sent. This number is configurable with the cacheBufferSize value in your config file:

houdini.config.js
export default {
    // ...
    cacheBufferSize: 5
}

Custom IDs

Some applications cannot rely on the Global Object Identification spec for one reason or another. Maybe your types do not have an id field. If it’s the case, you can tell it to Houdini by setting keys to your types in your houdini.config.js, like:

houdini.config.js
export default {
    // ...
    types: {
        User: {
            keys: ['firstName', 'lastName'],
        }
    }
}

Maybe your API does not support the node query field. In this case, Houdini lets you configure both parts of this interaction so you can tell Houdini how to do the right thing.

If all you need to do is change the way that Houdini resolves a particular type, you can use the following configuration:

houdini.config.js
export default {
    // ...
    types: {
        User: {
            resolve: {
                queryField: "user",
            }
        }
    }
}

This tells Houdini that in order to resolve a particular User, it needs to use a query field on our API that looks like: user(id: ID!). By default, Houdini takes the keys of an object and passes each one as the input for the query field. If you need to change this behavior, you can pass an arguments field to the resolve object:

houdini.config.js
export default {
    // ...
    types: {
        User: {
            resolve: {
                queryField: "user",
                arguments: (user) => ({
                    userID: user.id,
                })
            }
        }
    }
}

Note: Houdini’s generator guarantees that your entities will always have its keys so there’s no need to check if user.id is defined.

If you want to configure Houdini to use a different key for computing your records’ ID, you can configure the specific type with the types config value:

houdini.config.js
export default {
    // ...
    types: {
        User: {
            keys: ['firstName', 'lastName'],
            resolve: {
                queryField: 'user'
            }
        }
    }
}

Remember, Houdini will take every key for an object and pass it as an argument to the query with the same name. Ie, the following configuration is equivalent to the one above:

houdini.config.js
export default {
    // ...
    types: {
        User: {
            keys: ['firstName', 'lastName' ],
            resolve: {
                queryField: 'user',
                arguments: (user) => ({
                    firstName: user.firstName,
                    lastName: user.lastName,
                })
            }
        }
    }
}

Stale Data

In some cases it can be useful to mark data as “stale” so that next time it is requested, it will be refetched over the network. This can be done in two ways:

Globally after a timeout

If you set defaultLifetime in your config file then data will get automatically marked stale after a certain time (in milliseconds). For example, you can configure so that any data older than 7 minutes is refreshed (example: defaultLifetime: 7 * 60 * 1000). When this happens, the cached data will still be returned but a new query will be sent (effectively making the cache policy CacheAndNetwork).

Programmatically

If you want more fine-grained logic for marking data as stale, you can use the programmatic api:

import { cache, graphql } from '$houdini'

// Mark everything stale
cache.markStale()

// Mark all type 'UserNodes' stale
cache.markStale('UserNodes')

// Mark all type 'UserNodes' field 'totalCount' stale
cache.markStale('UserNodes', { field: 'totalCount' })

// Mark the User 1 stale
const user = cache.get('User', { id: '1' })
user.markStale()

// Mark the User 1 field name stale
const user = cache.get('User', { id: '1' })
user.markStale('name')

// Mark the name field when the pattern field argument is 'capitalize'
const user = cache.get('User', { id: '1' })
user.markStale('name', { when: { pattern: 'capitalize' } })

Programmatic API

There are times where Houdini’s automatic cache updates or list operation fragments are not sufficient. For those times, Houdini provides a programatic API for interacting with the cache directly. For more information, check out the Cache API reference .

import { cache } from '$houdini'

const user = cache.get('User', { id: '1' })

user.write({
	fragment: graphql(`
		fragment UserInfo on User {
			firstName
		}
	`),
	data: {
		firstName: 'New name'
	}
})

Query Hints

One interesting application of the programmatic API is to provide hints for the runtime before performing a query. This creates a snappier experience since you are more likely to load cached data.

Let’s see this in action. Pretend we are building an application with the following schema:

type Query {
	user(name: String!): User
	users: [User!]!
}

type User {
	id: ID!
	name: String!
}

Now, imagine that there are two different views in our application: a list of users and a user profile page. The list is driven by this query:

query UserList {
	users {
		id
		name
	}
}

And the user profile view uses a query that takes the name and returns the user information:

query UserProfile($name: String!) {
    user(name: $name) {
        id
        name
    }
}

Now we want to show a link from the list view to a detail view of the specific user:

// in a loop over users...

<a href={`/users/${user.name}/`}>

When we render this link, Houdini’s cache already knows the id and name of the user that is providing the data. However, when we click on the link, Houdini won’t be able to use cached data for the UserProfile query because it does not know the value of user(name: "Steve"). Since we know the answer when generating the link, we can provide Houdini with the information before the link resolves:

import { cache, graphql }  from "$houdini"

function primeCache(user) {
    // prime the cache to know the id of the user we are resolving
    cache.write({
        query: graphql(`
            query UserProfileHint($name: String!) {
                user(name: $name) {
                    id
                }
            }
        `),
        data: {
            user: {
                id: user.id,
            }
        },
        variables: {
            name: user.name,
        }
    })
}

// in a loop over users...

<a href={`/users/${user.name}/`} onClick={() => primeCache(user)}>

With this setup, we let the cache know that the value of user(name: "Steve") points to a User with a particular id and we can use the cached name value.