❄️ GraphQL

Query Hooks

Call queries and mutations dynamically in the browser

Sometimes you might need to perform queries at runtime, rather than as part of views or blocks. This is particularly useful for things like searching and filtering.

To do so, you can create .graphql files inside the queries/ folder.

React Hooks are automatically generated, based on the name of the query.

Naming Conventions

For conventions sake, all queries/*.graphql files (except for queries/fragments/*.graphql) should be start with use (eg. queries/UseLatestPosts.graphql). Our dev tooling will actually complain if this isn't the case!

Similarly, the name of your query file and query operation name should also match! Again, you'll get a nice error message if they don't asking you to rename either the operation name, or the file name.

For example:

queries/UseLatestPosts.graphql
query UseLatestPosts {
  posts(first: 10) {
    nodes {
      id
      title(format: RENDERED)
    }
  }
}

Using your hook

Generated hooks are stored in hooks/queries.ts, and can be imported from there using the name of the query file.

import { useLatestPosts } from "@hooks/queries"
import { PostTile } from "@components/PostTile"

export function LatestPosts() {
  const results = useLatestPosts()

  if (results.loading) {
    return <p>Loading...</p>
  }

  if (results.errors) {
    return <p>Error!</p>
  }

  return (
    <div>
      {results.data?.posts?.nodes?.map((post) => (
        <PostTile key={post.id} post={post} />
      ))}
    </div>
  )
}

Easy as!

Types of hooks

There are three types of hooks which are automatically generated.

Regular hooks

The useLatestPosts example above is a regular hook. The hook will return an object with the following properties:

  • loading (boolean) — whether the query is currently loading
  • errors (array of error objects) — an array of errors, or undefined if there are none. The errors may either be HTTP errors, or GraphQL errors.
  • data (object) — the data returned by the query, which should be statically typed already based on your GraphQL schema and query.
  • refresh (function) — a function you can call to re-run the query.

Infinite hooks

Infinite hooks are a special kind of hook which we've invented, which support infinite loading patterns, such as infinite scrolling and 'load more' buttons.

Any query which has Infinite in the name (eg. UseInfinitePosts) will be automatically generated as an infinite hook. These hooks have a loadMore function, and make use of the nodes and pageInfo fields provided by WPGraphQL, and use 'cursor based' pagination.

There are a few special requirements for infinite hooks, but we've configured the dev tooling to warn you if you're not following these conventions (on top of the usual naming convention requirements)

  • The query name must contain the word 'Infinite' (eg. UseInfinitePosts)
  • There must be a $limit: Int variable defined on the query, with a default value. This is the number of items to load per page.
  • There must be a $cursor: String variable defined on the query, with no default value. This is used for controlling pagination.
  • The $cursor variable must be used in the after field of the query.
  • The $limit variable must be used in the first field of the query.
  • Your query must select nodes
  • Your query must select the endCursor and hasNextPage fields from the pageInfo object

Here's an example query which fits all of those requirements (it's pretty simple!):

queries/UseInfinitePosts.graphql
query UseInfinitePosts($limit: Int = 2, $cursor: String) {
  posts(first: $limit, after: $cursor) {
    nodes {
      title(format: RENDERED)
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}

The return value of the hook will be an object with the following properties:

  • loading (boolean) — whether the hook is performing it's first load
  • loadingMore (boolean) — whether the hook is loading additional entries, in response to loadMore()
  • errors (array of error objects) — an array of errors, or undefined if there are none. The errors may either be HTTP errors, or GraphQL errors.
  • items (array of objects) — an array of items, which are the result of the query, automatically extracted from the nodes field present in the query
  • hasMore (boolean) — whether or not there are more items which can be loaded
  • loadMore (function) — function which can be called to load more items.
  • refresh (function) — clears out all items, and re-runs the query.

In essense, you can use .items to grab the current items, and .loadMore() to load more items when a button is clicked, or an IntersectionObserver intersects.

Heres' an example component which uses the query above:

components/LatestPosts.tsx
import { useInfinitePosts } from "@hooks/queries"

function InfiniteExample() {
  const posts = useInfinitePosts()

  if (posts.loading) {
    // The initial load is in progress, show some loading indicator
    return <div>Loading...</div>
  }

  return (
    <div>
      {/* The list of items, which will grow as more items are loaded */}
      <ul>
        {posts.items?.map((item, key) => {
          return <li key={key}>{item.title}</li>
        })}
      </ul>
      {/* A 'Load more' button, which is disabled and shows 'Loading...' when more items are loading */}
      {posts.hasMore && (
        <button disabled={posts.loadingMore} onClick={() => posts.loadMore()}>
          {posts.loadingMore ? "Loading..." : "Load more"}
        </button>
      )}
    </div>
  )
}

It looks something like this:

Infinite Example
Infinite Example

Mutation hooks

Mutation hooks are hooks generated when your operation uses mutation in instead of query. Mutations are used in GraphQL when the query has some kind of side effect, such as creating a new comment, or submitting a form. Unlike query operations, mutations are never cached on the server.

Mutation hooks return an object with the following properties:

  • submitting (boolean) — whether the mutation is currently running
  • submitted (boolean) — whether or not the last mutation which was submitted completed successfully
  • errors (array of error objects) — an array of errors, or undefined if there are none. The errors may either be HTTP errors, or GraphQL errors.
  • data (object) — the result of the last submitted mutation, if it was successful.
  • submit(args) (function) — the function which you must trigger to actually run the mutation. Pass in required parameters as an object.

An example mutation operation looks like this:

queries/UseCreateComment.graphql
mutation UseSubmitComment($input: CreateCommentInput!) {
  createComment(input: $input) {
    success
  }
}

Unlike query hooks, the query won't run automatically when using the hook — rather, it needs to be triggered manually using an event or an effect using the submit method, provided by the hook.

components/CreateComment.tsx
import { useSubmitComment } from "@hooks/queries"
import { useState } from "react"

type Props = {
  postID: number
}

export function CreateComment(props: Props) {
  const [message, setMessage] = useState("")
  const [authorName, setAuthorName] = useState("")
  const comment = useSubmitComment()

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        comment.submit({
          input: {
            commentOn: props.postID,
            content: message,
            approved: null,
            author: authorName,
            authorEmail: null,
            authorUrl: null,
            clientMutationId: null,
            date: null,
            parent: null,
            type: null,
          },
        })
      }}
    >
      {comment.errors && <div>ERROR: {comment.errors[0].message}</div>}
      {comment.submitted && <div>Thanks for your comment!</div>}
      <input
        placeholder="Enter your name"
        type="text"
        value={authorName}
        onChange={(e) => setAuthorName(e.currentTarget.value)}
      />
      <textarea placeholder="Enter your comment" value={message} onChange={(e) => setMessage(e.currentTarget.value)} />
      <button disabled={comment.submitting} type="submit">
        Submit comment
      </button>
    </form>
  )
}

It looks something like this:

Mutation Example
Mutation Example

Does this use the GraphQL endpoint?

Nope it doesn't! It uses the WP-JSON REST endpoint, and the query file is loaded from the theme folder and executed by the server.

For example, if you've got a GraphQL file called queries/UseLatestPosts.graphql in your theme folder, you'll can actually see it in action by accessing http://my-site.local/wp-json/ed/v1/query/usePosts?params={%22limit%22:1}, noting that variables are JSON encoded into the URL — this can be great for debugging GraphQL schema extensions. Use Chrome's Network tab to see the request and responses in action.

The reason for this is so that the query results can be cached by Flywheel or CloudFlare, and also keeps our /graphql hidden from hackers who might want to DDoS the site, or otherwise attempt to scrape data directly from us. The result is that only the data we chose to make public is publicly accessible.

Mutations also follow this pattern, however they use POST requests, so that data will never accidentally be cached.