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.

Careful!

Houdini’s React bindings are still considered unstable and are subject to change with every patch update (the third number). If you are building stuff with it, please check the changelog for any updates or pin to a specific version.

We recommend installing the @canary tag which will give you the latest version merged to main and might include fixes that haven’t yet been deployed.

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 route
  • src/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:

src/routes/+page.tsx
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.

src/routes/+layout.tsx
export default ({ children }) => {
    return (
        <>
            <NavBar />
            {children}
        </>
    )
}

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:

src/routes/shows/+page.gql
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:

src/routes/shows/+page.tsx
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:

src/routes/show/[id]/+page.gql
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:

src/routes/shows/+page.tsx
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:

src/routes/shows/[id]/edit/+page.jsx
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:

src/components/ShowCard.tsx
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:

src/routes/show/[id]/+page.gql
query ShowInfo($id: ID!) {
    show(id: $id) {
        ...ShowCardInfo
    }
}

And then threaded through to the component:

src/routes/show/[id]/+page.tsx
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.

houdini.config.js
export default {
    // ...
    features: {
        componentFields: true
    }
}

With that enabled, your components can define fragments using a slightly different API:

src/components/UserAvatar.tsx
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} />
}
src/components/UserAvatar.jsx
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:

src/routes/profile/+page.gql
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:

src/routes/profile/+page.tsx
import type { PageProps } from './$types'

export default function ShowInfoView({ Profile }: PageProps) {
    return (
        <div>
            <Profile.currentUser.Avatar />
        </div>
    )
}
src/routes/profile/+page.jsx
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:

src/components/UserAvatar.tsx
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} />
}
src/components/UserAvatar.jsx
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} />
}
src/routes/profile/+page.gql
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:

src/components/UserAvatar.tsx
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} />
}
src/components/UserAvatar.jsx
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:

vite.config.js
/** @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 in houdini.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:

vite.config.js
/** @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:

vite.config.js
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 Pages
  • houdini-adapter-auto: tries to install the appropriate adapter for the current situation