How to use GraphQL in React using hooks

What are Hooks?

React Hooks, introduced in version 16.8.0, are reusable stateful logic functions. They aim to simplify the development of complex components by splitting them into small functional blocks that are easier to manage, test and reuse. 

Using hooks removes the need for many abstractions like Higher Order Components (HOC) and render props. They allow you to add functionality to the application without having to change the component hierarchy and without having to encapsulate components.

Also, it often makes the code more readable and maintainable.

As an example, here is a simple React clock component written using classes:

import React from 'react'

class Clock extends React.Component {
  constructor(props) {
    super(props)
    this.state = {date: new Date(), interval: 0}
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    )
  }

  componentDidMount() {
    const interval = setInterval(() => {
      this.setState({date: new Date()})
    }, 1000)

    this.setState({interval})
  }

  componentWillUnmount() {
    clearInterval(this.state.interval);
  }
}

And here the same component is written using hooks:

import React from 'react'

function Clock() {
  const [date, setDate] = React.useState(new Date())

  React.useEffect(() => {
    const interval = setInterval(() => {
      setDate(new Date())
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {date.toLocaleTimeString()}.</h2>
    </div>
  )
}

In our example, both useState and useEffect are React hooks.

What is GraphQL?

GraphQL is a data query language designed for API. The language is meant to be declarative, strongly typed and exhaustive. 

The design has two main types of operations: queries and mutations. The former is used when retrieving data, the latter for updating data.

Another important improvement of GraphQL is that multiple operations can be sent and retrieved using a single endpoint (usually /graphql) and a single network request. This reduces the number of roundtrips and overall data transfer, which is very important on mobile devices and bad network situations.

Here’s an example of a GraphQL mutation which adds a new user (and chooses which field to get a result):

mutation CreateUser($name: String!){
  createUser(name: $name) {
    name
  }
}

And here’s an example of a GraphQL query which gets the list of users:

{
  users {
    name
  }
}

Introducing graphql-hooks

In order to use GraphQL in a React application using hooks, we are going to use graphql-hooks, a small library with hooks support and optional Server-Side Rendering and caching support.

Here’s an example of how a real-world component might look like:

import { useQuery } from 'graphql-hooks'
 
const HOMEPAGE_QUERY = `query HomePage($limit: Int) {
  users(limit: $limit) {
    id
    name
  }
}`
 
function MyComponent() {
  const { loading, error, data } = useQuery(HOMEPAGE_QUERY, { variables: { limit: 10 } })
 
  if (loading) return 'Loading...'
  if (error) return 'Something Bad Happened'
 
  return (
    <ul>
      {data.users.map(({ id, name }) => (
        <li key={id}>{name}</li>
      ))}
    </ul>
  )
}

Getting started on the server

To get started, let’s create an application server which serves a React application.

const appShellHandler = require('./handlers/app-shell')

function main() {
  const app = fastify({
    logger: true
  })

  app.register(require('fastify-static'), {
    root: path.join(process.cwd(), 'build/public')
  })

  app.register(graphqlPlugin)

  app.get('/', appShellHandler)
  app.get('/listUsers', appShellHandler)

  app.listen(3000)
}

main()

The app-shell.js file is responsible for Server Side Rendering, the initial version injects the Javascript file created by webpack inside a bare metal HTML page:

const { getBundlePath } = require('../helpers/manifest')

function renderHead() {
  return `
    <head>
      <title>Hello World!</title>
    </head>
  `
}

async function renderScripts() {
  const appShellBundlePath = await getBundlePath('app-shell.js')
  return `
    <script src="${appShellBundlePath}"></script>
  `
}

async function appShellHandler(req, reply) {
  const head = renderHead()
  const scripts = await renderScripts()

  const html = `
      <!DOCTYPE html>
      <html>
        ${head}
        <body>
          <div id="app-root"></div>
          ${scripts}
        </body>
      </html>
    `

  reply.type('text/html')
  return html
}

module.exports = appShellHandler

The graphql.js  file is where our tutorial will focus on. Here’s the starting version:

const fastifyGQL = require('fastify-gql')

const userList = [
  {
    name: 'Brian'
  },
  {
    name: 'Jack'
  },
  {
    name: 'Joe'
  },
  {
    name: 'Kristin'
  }
]

const schema = `
  type User {
    name: String
  }

  type Query {
    users: [User]
  }
`

const resolvers = {
  Query: {
    users() {
      return userList
    }
  }
}

function registerGraphQL(fastify, opts, next) {
  fastify.register(fastifyGQL, {
    schema,
    resolvers,
    graphiql: true
  })

  next()
}

module.exports = registerGraphQL

The GraphQL adapter we chose on the server is fastify-gql. We evaluated other solutions but it had higher performances, as you can see in the comparison below.

graph-ql

Before moving on, let’s analyze the options passed to the adapter.

The very first thing to define when creating a GraphQL API is the schema. The schema must define all the queries, mutation and types supported by the API. In particular, Query and Mutation are considered “entry-point” types and of them should be present in every schema.

In our case, we define only Query (for now) and we define a single query, users, which will return a list (which is denoted using square brackets) of User. The User is the only other type in the schema, which contains a single string field called name. As you might wonder, GraphQL is language-agnostic, so the definition of language syntax tries to be similar to most used languages.

Once we have the schema ready, we need to pass the resolvers. As the name might suggest, resolvers is a Javascript mapping object which allows the adapter to map GraphQL queries or mutation to real application call. In our case, when performing the users GraphQL query (which we can rewrite using the dotted notation as schema.Query.users) it will execute the resolvers.Query.users (note that the path is the same) and the return value will be the result of the query.

In our example, we also passed the graphiql option set to true. This will add graphiql (note the i), an in-browser IDE for GraphQL to the server, at the /graphiql route.

Getting started on the client

The initial version of the client application is very simple: only two routes, one of them is a simple “hello world” one. Let’s see what they look like.

Here’s the application main file:

import React from 'react'
import { Link, Router } from '@reach/router'

// components
import ListUsers from './pages/ListUsers'

function HelloWorld() {
  return (
    <div>
      <h1>Hello World</h1>
    </div>
  )
}

class AppShell extends React.Component {
  render() {
    return (
      <div className="app-shell-component">
        <nav>
          <Link to="/">Hello World</Link> |{" "}
          <Link to="/listUsers">List Users</Link>
        </nav>
        <Router>
          <HelloWorld path="/" />
          <ListUsers path="/listUsers" />
        </Router>
      </div>
    )
  }
}

AppShell.propTypes = {}

render(AppShell, document.getElementById('app-root'))

And here’s the initial version of the ListUsers route:

import React, { useState } from 'react'

const users = [
  { name: 'John' },
  { name: 'Sally' }
]

export default function ListUsers() {
  const [name, setName] = useState('')

  function createNewUser() {
    users.push({name})
    setName('')
  }

  return (
    <div>
      <h1>Users List</h1>
      <ul>
        {users.map((user, i) =>
          <li key={i}>{user.name}</li>
        )}
      </ul>
      <label>Create User<br />
        <input
          type='text'
          onChange={e => setName(e.target.value)}
          value={name}
        /><br/>
      </label>
      <button onClick={createNewUser}>Save</button>
    </div>
  )
}

As you can see, the first version already uses hooks, but it’s not connected to GraphQL. Yet. Let’s fix this!

Add a mutation to persist the data

In order to let the client update persisted data, we first have to modify the server. 

Let’s start by adding a mutation to our schema and resolvers in graphql.js:

const schema = `
  type User {
    name: String
  }

  type Query {
    users: [User]
  }

  type Mutation {
    createUser(name: String!): User
  }
`

const resolvers = {
  Query: {
    users() {
      return userList
    }
  },
  Mutation: {
    createUser(_, user) {
      userList.push(user)
      return user
    }
  }
}

The server is now able to handle a GraphQL mutation, which is the only way to modify the data.

This can be tested in graphiql by running this operation:

mutation CreateUser($name: String!){
  createUser(name: $name) {
    name
  }
}

Make sure you don’t forget to pass the operation variables:

{
  "name": "John"
}

And then you can verify that the data has been persisted by running the following query:

{
  users {
    name
  }
}

Close the loop: connect the client

As said earlier, the first implementation of the ListUsers route was only storing data in browser memory without using the server at all.

Since the server is now capable of persisting data, let’s connect it to the client.

First, let’s modify the main application file to instantiate a graphql-hooks client and add the context provider to the application:

import React from 'react'
import { Link, Router } from '@reach/router'
import { ClientContext, GraphQLClient } from 'graphql-hooks'

// ...
// Components definitions unchanged
// ...

const client = new GraphQLClient({ url: '/graphql' })

const App = (
  <ClientContext.Provider value={client}>
    <AppShell />
  </ClientContext.Provider>
)

render(App, document.getElementById('app-root'))

Then, modify the route to use the useQuery and useMutation hook. Here’s how the new ListUsers route will look like:

import React, { useState } from 'react'
import { useQuery, useMutation } from 'graphql-hooks'

const LIST_USERS_QUERY = `
  query ListUsersQuery {
    users {
      name
    }
  }
`
const CREATE_USER_MUTATION = `
  mutation CreateUser($name: String!) {
    createUser(name: $name) {
      name
    }
  }
`

export default function ListUsers () {
  const [name, setName] = useState('')

  const {
    data = { users: [] },
    refetch: refetchUsers
  } = useQuery(LIST_USERS_QUERY)

  const [createUser] = useMutation(CREATE_USER_MUTATION)

  async function createNewUser() {
    await createUser({ variables: { name } })
    setName('')
    refetchUsers()
  }

  return (
    <div>
      <h2>Users List</h2>
      <ul>
        {data.users.map((user, i) => <li key={i}>
          {user.name}
        </li>)}
      </ul>
      <label>Create User<br />
        <input
          type='text'
          onChange={e => setName(e.target.value)}
          value={name}
        /><br/>
      </label>
      <button onClick={createNewUser}>Save</button>
    </div>
  )
}

There are many values returned by the useHook query, but for now, let’s focus on the main ones. data contains the results of the query returned by the server, while refetch is a callback that enables the client to ask for a data refresh on-demand.

The useMutation hook instead simply returns an async function which will trigger a mutation on the server.

Notice that the component uses both React Hooks (useState) and graphql-hooks hooks. This is an example of where the hooks approach makes it really simple to add new functionality to components (even via external libraries) without having to reorganize the component hierarchy. 

And there it is: a fully working client-server application using GraphQL and React hooks.

Pagination

So far, the users GraphQL has returned all the users stored in the system. As you might imagine, this is not useful in the real world where you usually have thousands or millions of them. So, let’s implement pagination. 

To achieve this, we are going to use a mechanism you have already seen: query variables. If you look at the createNewUser method above, you will notice that it passes variables to the mutation. This is also possible for queries.

Let’s start by modifying the schema and the resolvers in the graphql.js file of the server:

const schema = `
  type User {
    name: String
  }

  type Query {
    users(skip: Int, limit: Int): [User]
  }

  type Mutation {
    createUser(name: String!): User
  }
`

const resolvers = {
  Query: {
    users (_, { skip = 0, limit }) {
      return limit ? userList.slice(skip, skip + limit) : userList.slice(skip)
    }
  },
  Mutation: {
    createUser(_, user) {
      userList.push(user)
      return user
    }
  }
}

As you can see, the user’s query is now parameterized. That’s all we need to modify on the server, so let’s switch back on the client.

Let’s create a new PaginationPage, which will be rendered under the /users path.

import React, { useState } from 'react'
import { useQuery } from 'graphql-hooks'

const USERS_QUERY = `
  query UsersQuery($skip: Int, $limit: Int) {
    users(skip: $skip, limit: $limit) {
      name
    }
  }
`

export default function PaginationPage() {
  const [page, setPage] = useState(1)
  const { data } = useQuery(USERS_QUERY, { variables: { limit: 1, skip: page - 1}})

  return (
    <div>
      <h2>Pagination</h2>
      <ul>
        {data &&
          data.users &&
          data.users.map((user, i) =>
            <li key={I}>{user.name}</li>
          )}
      </ul>
      <button onClick={() => setPage(page - 1)}>Prev</button>
      <button onClick={() => setPage(page + 1)}>Next</button>
    </div>
  )
}

Once again, we mix up React and GraphQL hooks in order to implement our component.

The biggest change is on useQuery call, where we now pass query variables. 

Finally, we can add the page to the client application index:

// ...
import PaginationPage from './pages/PaginationPage'
// ...

class AppShell extends React.Component {
  render() {
    return (
      <div className="app-shell-component">
        <nav>
          <Link to="/">Hello World</Link> |{" "}
          <Link to="/listUsers">List Users</Link>
          <Link to="/users">PaginationPage</Link>
        </nav>
        <Router>
          <HelloWorld path="/" />
          <ListUsers path="/listUsers" />
     <PaginationPage path="/users" />
        </Router>
      </div>
    )
  }
}

Let’s also add it to the server index for SSR:

// ...

module.exports = () => {
  const app = fastify({
    logger: true
  })

  app.register(require('fastify-static'), {
    root: path.join(process.cwd(), 'build/public')
  })

  app.register(graphqlPlugin)

  app.get('/', appShellHandler)
  app.get('/listUsers', appShellHandler)
  app.get('/users', appShellHandler)

  app.listen(3000)
}

Caching

To ensure the best user experience, we don’t want to make server queries that we already performed, we should use caching.

graphql-hooks has support for custom caching plugins which solve this problem for us.

In this example, we are going to use the graphql-hooks-memcache plugin.

Despite what the name suggests, this plugin is not a server plugin backed by MemCache storage, but it is a client plugin which caches every operation by creating a hash of it. The cache is held in memory using a Least Recently Used (LRU) policy.

To use it, simply pass it the client application main file when instantiating the graphql-hooks client:

// ...

import memCache from 'graphql-hooks-memcache'
const client = new GraphQLClient({ url: '/graphql', cache: memCache() })

// ...

That’s it, it’s that simple!

SSR

The last step of this tutorial to have a fully working real-case example is to hook up graphql-hooks in a server-side rendered (SSR) application.

Doing this will allow the same client code to be reused on the server to return fully rendered pages. This improves browser boot and time to be interactive time and also improves search engine optimisation.

Also, by using state passing and rehydration, we can make sure no cache miss happens on the client after initial loading.

To get started, we are going to modify the client initialization to use isomorphic-unfetch. This enables fetch support on server (Node.js) and polyfills on the client if needed.

The newly modified server app shell will look like this:

const React = require('react')
const ReactDOMServer = require('react-dom/server')

// graphql-hooks
const { getInitialState } = require('graphql-hooks-ssr')
const { GraphQLClient, ClientContext } = require('graphql-hooks')
const memCache = require('graphql-hooks-memcache')

// components
const { default: AppShell } = require('../../app/AppShell')

// helpers
const { getBundlePath } = require('../helpers/manifest')

function renderHead() {
  return `
    <head>
      <title>Hello World!</title>
    </head>
  `
}

async function renderScripts({ initialState }) {
  const appShellBundlePath = await getBundlePath('app-shell.js')
  return `
    <script type="text/javascript">
      window.__INITIAL_STATE__=${JSON.stringify(initialState).replace(
        /</g,
        '\\u003c'
      )};
    </script>
    <script src="${appShellBundlePath}"></script>
  `
}

async function appShellHandler(req, reply) {
  const head = renderHead()

  const client = new GraphQLClient({
    url: 'http://127.0.0.1:3000/graphql',
    cache: memCache(),
    fetch: require('isomorphic-unfetch'),
    logErrors: true
  })

  const App = (
    <ClientContext.Provider value={client}>
      <AppShell />
    </ClientContext.Provider>
  )

  const initialState = await getInitialState({ App, client })
  const content = ReactDOMServer.renderToString(App)
  const scripts = await renderScripts({ initialState })

  const html = `
      <!DOCTYPE html>
      <html>
        ${head}
        <body>
          <div id="app-root">${content}</div>
          ${scripts}
        </body>
      </html>
    `

  reply.type('text/html').send(html)
}

module.exports = appShellHandler

The main modification is the introduction of the same GraphQL client on the server, with slightly different options: first, we have to specify the full client URL, then we have to specify the fetch method to use.

Then we use getInitialState from graphql-hooks-ssr in order to get the state to send to the client, rendered as serialized JSON inside the renderScripts function.

Once the server is ready, let’s modify the client. We have to modify the application main file in order to account for the initialState of the cache and the rehydration of the server-side rendered page.

// ...

const initialState = window.__INITIAL_STATE__
const client = new GraphQLClient({
  url: '/graphql',
  cache: memCache({ initialState })
})

const App = ( 
  <ClientContext.Provider value={client}>
    <AppShell />
  </ClientContext.Provider>
)

hydrate(App, document.getElementById('app-root'))

The main modification are passing the initialState to the cache plugin and to replace the react-dom render with hydrate.

Conclusions

As shown in this post, graphql-hooks is a powerful package to easily add GraphQL to your React application using a very easy to use approach. Also, its plugin-based approach makes it very trivial to add complex features like caching and SSR without any cumbersome abstractions and without revolutionizing your components or application structure.

If you want to see the full application, you can check it out on GitHub, we use it as part of a workshop exercise for graphql-hooks.

At NearForm, we have vast experience in building solutions across a broad tech stack to deliver reduced complexities and overcome common hurdles. If you are creating modern applications and leveraging web technologies, contact us to learn more about how we can help.

You might also like some of our previous blog posts on React:

Managing React state with Render Props

Exploring React Portals

Sharing React components with Lerna

Top