React Framework
This page is currently the best place to go for documentation on Houdini’s React framework. While most of the guides apply to both React and SvelteKit, the API section is very much geared towards SvelteKit users. Eventually this content will be merged in with the rest of this site but that will take time. Until then, use this document as the initial place in your search before looking at other options. Also keep in mind this document is not organized as an introduction but more of a reference. Don’t worry - there will be a more guided introduction soon.
What is Houdini?
Houdini is a web application framework built from the ground up for GraphQL. It provides everything you need to build state-of-the-art apps. Not only does that include basic features like routing and SSR’d queries but also advanced graphql patterns like paginated fragments and even optimizations for your application’s bundle.
Getting Started
The easiest way to start a new project is to run the following command and answer the prompts:
npm create houdini@latest
Once that’s completed, navigate into the newly created directory, install the dependencies using your favorite package manager and then run dev
:
cd <project name> && npm i && npm run dev
Routing
At the core of a Houdini application is a filesystem-based router located at src/routes
. You will use it to encode your applications
visual skeleton as well as its data requirements. Routes in houdini are directory-based. That is to say:
src/routes
is the root routesrc/routes/shows
corresponds to a route at/shows
src/routes/show/[id]
defines a route with a parameter,id
, that matches a pattern such as/show/1234
Each route is defined by the presence of any number of magic files (all of which are conventionally prefixed with +
).
Page Views
Page views (defined by +page.jsx
) are components that are unique to just that route and in many ways define a route. For example, to
define a view at the root of your application you just need to define a component at src/routes/+page.jsx
:
export default () => {
return <div> Hello Houdini! </div>
}
Layout Views
Layout views (defined by +layout.jsx
) are components that are shared across multiple routes and are designed to wrap
a page view. They are defined very similiarly to pages except they accept a children
prop and “apply” to all child routes
and a sibling page if the appropriate +page.jsx
is defined.
export default ({ children }) => {
return (
<>
<NavBar />
{children}
</>
)
}
Links
Links between pages are defined using standard <a>
tags.
Preloading
You can opt-into preloading links on a case by case basis by adding data-houdini-preload
:
<a href="/shows" data-houdini-preload>
Houdini will detect when the user hovers over this element and fetch whatever is necessary to render the page before they click on the link. By default, this means loading both the page component source as well sending the actual query.
You can customize this by setting the prop to one of 3 values:
"data"
: only request the page’s data"component"
: only request the page’s component source"page"
: request all of the assets for the page. This is the default.
For example,
<a href="/shows" data-houdini-preload="data">
Route Parameters
Dynamic portions of a route can be labeled using square brackets. For example src/routes/show/[id]/+page
creates a page that matches /shows/1
or /show/abc
.
Route Groups
docs TBD
Loading data
Just like views are defined in page and layout variants, your applications queries
are defined in +page.gql
and +layout.gql
files. Queries must have a unique name:
query ShowList {
shows {
title
}
}
Just like their equivalent view component, layout queries are designed to “wrap” child routes and are accessible by any child (or sibling) page or layout component.
To access the value of a query, your component just needs to accept a prop with the appropriate name. This could be one query or as many as you have defined. Keep in mind that since this works off of very simple static analysis, your component props must be spread out from the argument like so:
export default function ({ ShowList }) {
return (
<>
{ShowList.shows.map(show => (
<div>
{show.title}
</div>
))}
</>
)
}
An arrow function would have also worked as long as it had { ShowList }
or { ShowList, AnotherQuery }
Query Variables
If your query contains variables with the same name as a route parameter, Houdini will wire the two up.
For example, all you need to do is define this query at src/routes/show/[id]/+page.gql
and you can visit /show/123
or /show/abc
:
query ShowInfo($id: ID!) {
show(id: $id) {
name
}
}
Runtime Scalars
An unforunate reality is that not all query variables can be embeded as route paramters. A common example of this is information that’s embedded in your application’s session. To support this, we are working on an experimental API known as Runtime Scalars.
Imperative Handles
Sometimes you need to perform some imperative task on a query (refetching, loading the next page, etc). For these
situations, you should use the $handle
variant on the query prop. For example:
export default function ({ ShowList$handle }) {
return (
<>
<button onClick={ShowList$handle.loadNextPage}>
Load Next
</button>
{ShowList.shows.map(show => (
<div>
{show.title}
</div>
))}
</>
)
}
Loading States
Loading states are one of Houdini’s most powerful features. Apart from one important difference, all of the information in the
guide on loading states applies to the react framework. Hopefully you didn’t just click that link because
it’s important to remember that you have to use isPending
from the $houdini
package when identifying a pending value. What you
see in that guide (checking if === PendingValue
) won’t work with React 18.2.
Another thing to keep in mind is that for the React framework, the presence of @loading
implies the existence of a suspense boundary
in your component hierarchy. I know that last sentence might not be totally clear but explaining it more thoroughly will take time so
please be patient.
Mutations
Mutations are defined by simply wrapping the result of graphql
in useMutation
:
import { graphql, useMutation } from "$houdini";
export default function EditShow({ AllShows }) {
const [one, setOne] = React.useState("");
const [two, setTwo] = React.useState("");
const mutate = useMutation(graphql(`
mutation OneTwo($one: String!, $two: String!) {
do(one: $one, two: $two)
}
`))
return (
<form onSubmit={() => mutate({one, two})}>
<input value={one} onChange={(e) => setOne(e.target.value)} />
<input value={two} onChange={(e) => setTwo(e.target.value)} />
<button type=“submit">submit</button>
</form>
)
}
Fragments
There are two ways to use fragments in Houdini. The first is defining it inside of the useFragment
hook and this
approach will always work in all situations:
import { graphql, useFragment } from "$houdini";
export function ShowCard(props: { show: ShowCardInfo }) {
const data = useFragment(props.show, graphql(`
fragment ShowCardInfo on Show {
name
}
`))
return (
<div>
{data.name}
</div>
)
}
And then can be passed to your graphql query:
query ShowInfo($id: ID!) {
show(id: $id) {
...ShowCardInfo
}
}
And then threaded through to the component:
import { ShowCard } from '...'
import type { PageProps } from './$types'
export default function ShowInfoView({ ShowInfo }:PageProps) {
return (
<ShowCard show={ShowInfo.show} />
)
}
Component Fields
When building reusable components, it is very common to find yourself repeatedly importing the same component and mixing in the same fragment over and over. This can get really tedious and so Houdini tries to address this by blurring the lines between your queries and your component library.
The idea is pretty simple on the surface: components can register themselves as fields on a type. But
before we can use this feature, we need to enable the feature flag in houdini.config.js
. Keep in mind this
means that this API might change with any minor version. We understand this isn’t semantic versioning by the book
but hopefully you understand.
export default {
// ...
features: {
componentFields: true
}
}
With that enabled, your components can define fragments using a slightly different API:
import { GraphQL } from '$houdini'
type Props = {
user: GraphQL<`{
... on User @componentField(field: "Avatar") {
avatarURL
}
}`>
}
export default function UserAvatar({ user } : Props) {
return <img src={user.avatarURL} />
}
import { graphql } from '$houdini'
graphql(`{
... on User @componentField(field: "Avatar", prop: "user") {
avatarURL
}
}`)
export default function UserAvatar({ user } : Props) {
return <img src={user.avatarURL} />
}
This example registers the Avatar
field on the User
type:
query Profile {
currentUser {
Avatar
}
}
Which can be used directly as a component in your page source. Notice there’s no need to import the component or remember the name of the prop that needs to be passed:
import type { PageProps } from './$types'
export default function ShowInfoView({ Profile }: PageProps) {
return (
<div>
<Profile.currentUser.Avatar />
</div>
)
}
export default function ShowInfoView({ Profile }) {
return (
<div>
<Profile.currentUser.Avatar />
</div>
)
}
Component Field Arguments
@componentField
can be mixed with @arguments
to define arguments on the field added for the component:
import { GraphQL } from '$houdini'
type Props = {
user: GraphQL<`{
... on User
@componentField(field: "Avatar")
@arguments(size: { type: "Int" })
{
avatarURL(size: $size)
}
}`>
}
export default function UserAvatar({ user } : Props) {
return <img src={user.avatarURL} />
}
import { graphql } from '$houdini'
graphql(`{
... on User
@componentField(field: "Avatar")
@arguments(size: { type: "Int" })
{
avatarURL(size: $size)
}
}`)
export default function UserAvatar({ user } : Props) {
return <img src={user.avatarURL} />
}
query Profile {
currentUser {
Avatar(size: 150)
}
}
Named Exports
If your file exports the component by a specific name (instead of a default export) then you must tell houdini which export it should use:
import { GraphQL } from '$houdini'
type Props = {
user: GraphQL<`{
... on User @componentField(field: "Avatar", export: "UserAvatar" ) {
avatarURL(size: $size)
}
}`>
}
export function UserAvatar({ user } : Props) {
return <img src={user.avatarURL} />
}
import { graphql } from '$houdini'
graphql(`{
... on User @componentField(field: "Avatar", export: "UserAvatar" ) {
avatarURL(size: $size)
}
}`)
export function UserAvatar({ user } : Props) {
return <img src={user.avatarURL} />
}
Authentication
The challenge when building user sessions in a modern app is how to share that information between
the server and client. Really, there is only one answer once security starts mattering: httpOnly
cookies. While traditionally
apps could use local storage for this, the initial request (the one that gets rendered on the server) doesn’t have access to
the client’s local storage and so we must rely on something that’s automatically included in the initial response.
Wiring up everything up by hand is possible in Houdini but it can be cumbersome and error prone. In order to help, Houdini provides a few strategies out of the box that we hope covers most situations. Once you have configured your strategy, you can use the session in your client as shown here.
- Redirect Based Authentication - Users authorize with a third party provider which then redirects the user back to the Houdini application with any number of query parameters that define the session
- Mutation Based Authentication (not yet implemented) - Users authorize by sending a mutation to the api. The session is defined by one of the fields in the response
Redirect Based Authentication
To configure your application to use a redirect-based strategy, you must set the auth
field of the router config to an object
like so:
/** @type {import('houdini').ConfigFile} */
const config = {
router: {
auth: {
// the URL that the user will be redirected to by the third-party provider
redirect: '/auth/token',
// the secret to use for signing/unsigning the session
sessionKeys: ['supersecret'],
},
},
}
export default config
To access the current session, you can use the useSession
hook:
import { useSession } from '$houdini'
const [ session, setSession ] = useSession()
Calling setSession
updates the client-side session as well as perists the new values in the application
cookie so that its available on the next load.
Local APIs
For many applications, the API can live in the same codebase as the UI. When that happens, there is usually a considerable amount of boilerplate to wire things up (you have to make sure the URLs match, thread session values around, etc). When things move to the edge, things get even more complex if you want to ensure that the server-side requests don’t cause more resources to be spun up.
To streamline this, Houdini lets you export an executable schema
from src/api/+schema
and Houdini takes care of the rest. It wraps up the schema in an instance of yoga,
makes that server available at a configurable endpoint, and automatically configures your client to use the correct endpoint.
When making requests from your server, those queries are resolved against the schema in memory instead of sending actual network requests.
If you want to customize the yoga instance, you can define a file at src/api/+yoga
with a default export of a yoga server and that will get used instead
Some things to keep in mind:
src/api/+schema
must return an executable schema (but not necssarily one created from@graphql-tools/schema
)- If you have a
+yoga
file, you still need a+schema
file so the codegen pipeline uses your schema - Make sure you don’t pass a value for
url
to your client in+client
- You don’t need to watch this schema for changes since vite’s file change detection does the trick. This means you shouldn’t have
watchSchema
configured inhoudini.config.js
- Your schema and yoga files can be written in typescript (they get independently compiled by vite before codegen)
You can change the endpoint using the apiEndpoint
configuration value of the router
entry:
/** @type {import('houdini').ConfigFile} */
const config = {
router: {
apiEndpoint: '/_graphql'
},
}
export default config
Deployment
Deployment of your application is handled by an “adapter” - a simple function that prepares your project for deployment. Adapters are passed to your application through the vite plugin:
import { sveltekit } from '@sveltejs/kit/vite'
import houdini from 'houdini/vite'
import adapter from 'houdini-adapter-cloudflare'
/** @type {import('vite').UserConfig} */
const config = {
plugins: [houdini( { adapter } ), ... ]
}
export default config
Here is a list of the available adapters:
houdini-adapter-cloudflare
: builds your application to run on Cloudflare Pageshoudini-adapter-auto
: tries to install the appropriate adapter for the current situation