GraphQL: Client Side Integration with Apollo Hooks blog post

GraphQL: Client Side Integration with Apollo Hooks

Zach Silveira

24 Jun, 2019

GraphQL

16 minutes read

Now that we’ve got a solid grasp on how resolvers work, we can get into integrating a web application frontend. For this tutorial, we’ll be using React, and Apollo’s new hooks library. For those unfamiliar with hooks, these are a new way to build out logic inside React components. To get more up to speed on hooks, take a look at the React docs on them. This library is in beta at the time of writing, but very stable at this point and it is unlikely the features are using will change. I wanted to make sure that we are writing React code that’s as future-proof as possible, and hooks are here to stay. It will also make our code more concise for easier understanding.

Follow this GraphQL Series:

Setup

Since we are adding a separate app into our repo, for those of you following along using the resolver-end branch on the course github. First we will make a new folder in the root of the repo called server and move every file into it, except for the .gitignore. Now, we can continue with creating the web app.

For those just joining in, please clone this repo and git checkout 3-client-integration-start.

Let’s get started by creating a new react app in the root folder:

npx create-react-app web

You should now have server and web inside the cloned repository. Now we can go inside the web folder, and install the required dependencies for Apollo Client:

rm -rf node_modules
npm install apollo-client apollo-link-http apollo-cache-inmemory graphql graphql-tag @apollo/react-hooks
npm install

Since we started with npm on the server, this will keep it the same inside create-react-app, so that its easy to continue following along.

Next, let’s delete every file inside of web/src except for index.js and App.js. We’ll replace the index.js with the following:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

Next up, let’s create a file called apolloSetup.js:

import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';

const httpLink = new HttpLink({
  uri: 'http://localhost:4000',
});

export default new ApolloClient({
  cache: new InMemoryCache(),
  link: httpLink,
});

Apollo tries to support a lot of use cases, so they have split out the setup into a few different packages. We will be adding a couple more later on, but for now this is a pretty simple setup.

We create a link that tells Apollo Client where our server is located. Then, when we create the client. We pass it the httpLink and an in memory cache. Apollo supports other caching options which can be useful for offline apps. With this setup, a page refresh will clear our cache, but clicking back and forth between pages would be able to use the cache, instead of making calls to our server for data.

With our src/apolloSetup.js in place, we can now update our App.js:

import React from 'react';
import apolloClient from './apolloSetup';
import { ApolloProvider } from '@apollo/react-hooks';

import Books from './pages/Books';

const App = () => (
  <ApolloProvider client={apolloClient}>
    <Books />
  </ApolloProvider>
);

export default App;

This wraps our app with Apollo, so that we can start using it. For this tutorial, we will make a few components and put them inside the Provider. In a more realistic React app, we would probably place some sort of router, like @reach/router inside of our provider.

Querying our Server

With our setup out of the way, let’s make our first query. Make a file called web/src/pages/useBooksQuery.js:

import gql from 'graphql-tag';
import { useQuery } from '@apollo/react-hooks';

export const query = gql`
  query Books {
    books {
      title
      author
    }
  }
`;

export default () => useQuery(query);

Now we can make Books.js inside of the same pages folder:

import React from 'react';
import useBooksQuery from './useBooksQuery';

const Books = () => {
  let { data } = useBooksQuery();
  if (!data || !data.books) return null;

  return data.books.map(book => (
    <div>
      <h3>{book.title}</h3>
      <p>{book.author}</p>
    </div>
  ));
};

export default Books;

At this point, your web/src directory should look like this:

├── App.js
├── apolloSetup.js
├── index.js
└── pages
    ├── Books.js
    └── useBooksQuery.js

The Power of Apollo

Let’s open up the server directory and run npm start. Next, open a new terminal window to start our web app. Type npm start inside the web directory. You should see the two books on screen that our server returned!

Now we can take a step back and compare a REST request like we did in the first post. This time we understand how a real query looks with real code.

A REST call for this same data may have looked like this inside of a React component:

import { useState, useEffect } from 'react';

const useBooksRequest = () => {
  let [books, setBooks] = useState();

  useEffect(() => {
    const getBooks = async () => {
      let response = await fetch('http://localhost:3000/books');
      let books = await response.json();
      setBooks(books);
    };

    getBooks();
  }, [setBooks]);

  return { books };
};

This would be a hook that runs when the component mounts, and sets the state of books when the response comes back. It would look very similar to our current Books:

const Books = () => {
  let { books } = useBooksRequest();
  if (!books) return null;

  return books.map(book => (
    <div>
      <h3>{book.title}</h3>
      <p>{book.author}</p>
    </div>
  ));
};

So what are we getting out of using Apollo Client? Some of this goes back to that first article. But there is a lot more I can talk about this time around.

Comparing Redux / REST

Imagine if we had a delete book REST call. And also imagine that we have the latest book title in the navbar at the top of our site. This means, if we are on our books page, and we delete the newest book, what needs to happen?

  • Call delete book endpoint
  • Remove the book from the books array in our state
  • If that book was the first book, we need to update the nav bar, since it is now deleted

Now let’s imagine we also care about showing a loading spinner while the delete is taking place. If we are using something like Redux, we are probably firing off this stuff in order:

  • START_LOADING
  • DELETE_BOOK
  • SET_NAVBAR_BOOK_TITLE
  • STOP_LOADING

At this point, this is feeling pretty complicated. This is where Apollo client comes in, and makes a huge difference. All of this code that you’d be used to writing with redux is no longer needed. Let’s see how.

In web/src let’s make a new file called Nav.js.

import React from 'react';
import useBooksQuery from './pages/useBooksQuery';

const Nav = () => {
  let { data } = useBooksQuery();
  if (!data || !data.books) return null;

  return (
    <div>
      This is our amazing nav bar. The latest book in our collection is{' '}
      {data.books[0].title}
    </div>
  );
};

export default Nav;

Next, we can edit src/App.js to render the nav at the top:

import React from 'react';
import Nav from './Nav';
import apolloClient from './apolloSetup';
import { ApolloProvider } from '@apollo/react-hooks';

import Books from './pages/Books';

const App = () => (
  <ApolloProvider client={apolloClient}>
    <React.Fragment>
      <Nav />
      <Books />
    </React.Fragment>
  </ApolloProvider>
);

export default App;

Now, if we load up the page, you’ll see that text at the top. Once again, I am trying to demonstate an area where some state is needed in two different places, in a global element (nav bar) and on a single screen (books edit page). This is very realistic for many apps. You could imagine an edit user screen and showing user info in the nav, for instance.

The Benefits of Redux, With Less Code

Let’s go step by step showing each piece of the puzzle.

Adding the delete mutation

First, we need to add a delete mutation to our server. We will make this really simple and fake deleting a book. What I mean by this is, our server will respond saying the book is deleted, but refreshing the page will bring it back.

Let’s open up server/src/resolvers.js and add a mutation at the bottom:

Mutation: {
  //existing one
  addAuthor: (_, { input: { name, twitter } }) => {
    return {
      name,
      twitter,
    };
  },
  deleteBook: (_, { title }) => true,
},

Next, open up typeDefs and add this to the existing Mutation type:

type Mutation {
  ...
  deleteBook(title: String!): Boolean
}

Now we can restart our server. What we just did will add a mutation that takes a book title, and returns true every time. Realistically, we would want a book id to be sent, and then actually delete it from our database.

Adding the mutation to the frontend

Let’s make useDeleteBookMutation.js inside web/src/pages:

import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';

export const mutation = gql`
  mutation DeleteBook($title: String!) {
    deleteBook(title: $title)
  }
`;

export default () => {
  let [deleteBook] = useMutation(mutation);
  return deleteBook;
};

You’ll notice this time we are importing useMutation instead of useQuery. The rest is pretty straight forward. This hook will give us a mutate function that we can call when we want to hit the server. Let’s open up Books.js and import it:

import React from 'react';
import useBooksQuery from './useBooksQuery';
import useDeleteBookMutation from './useDeleteBookMutation';

const Books = () => {
  let { data } = useBooksQuery();
  let deleteBook = useDeleteBookMutation();

  if (!data || !data.books) return null;

  return data.books.map(book => (
    <div>
      <h3>{book.title}</h3>
      <p>{book.author}</p>
      <button onClick={() => deleteBook({ variables: { title: book.title } })}>
        Delete Book
      </button>
    </div>
  ));
};

export default Books;

At this point, if we load up our app, and hit delete, it hits the server, but nothing happens. This is expected. Since we are returning only a boolean, it’s up to us to tell Apollo what to do next.

Updating the local store

We can update the local cache after any request for immediate UI updates. Here’s how it looks, if we edit the button code in Books:

// Update the import at the top for `useBooksQuery to be:
import useBooksQuery, { query } from './useBooksQuery';
...

<button
  onClick={() =>
    mutate({
      variables: { title: book.title },
      update: store => {
        const data = store.readQuery({
          query,
        });

        store.writeQuery({
          query,
          data: {
            books: data.books.filter(
              currentBook => currentBook.title !== book.title,
            ),
          },
        });
      },
    })
  }
>
  Delete Book
</button>

Well… that looks like a lot doesn’t it! If we run it, and click delete, you will notice that the navbar and the list of books both automatically update with the correct data, having the book removed. Before I explain this further, let’s refactor it slightly. Change Books to this:

import React from 'react';
import useBooksQuery from './useBooksQuery';
import useDeleteBookMutation from './useDeleteBookMutation';

const Books = () => {
  let { data } = useBooksQuery();
  let deleteBook = useDeleteBookMutation();

  if (!data || !data.books) return null;

  return data.books.map(book => (
    <div>
      <h3>{book.title}</h3>
      <p>{book.author}</p>
      <button onClick={() => deleteBook(book.title)}>Delete Book</button>
    </div>
  ));
};

export default Books;

You’ll notice that deleteBook is only taking a book title. We can update useDeleteBookMutation to do all of the logic we previously had in our render. This keeps our Books much more readable. Open useDeleteBookMutation and replace it with:

import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';
import { query as booksQuery } from './useBooksQuery';

export const mutation = gql`
  mutation DeleteBook($title: String!) {
    deleteBook(title: $title)
  }
`;

export default () => {
  let [deleteBook] = useMutation(mutation);

  return title => {
    return deleteBook({
      variables: { title },
      update: store => {
        const data = store.readQuery({
          query: booksQuery,
        });

        store.writeQuery({
          query: booksQuery,
          data: {
            books: data.books.filter(
              currentBook => currentBook.title !== title,
            ),
          },
        });
      },
    });
  };
};

Ahh, this looks much better. Our GraphQL logic is now co-located with its query. As a developer it is much easier to come in and see what is going on by keeping this section together.

So, how does it work? Any mutation can have an optional update function. It gets the store (local cache from all queries) and the result of the mutation as a second parameter, which we are not using. Since we want to update the UI after this mutation, we first read a query:

const data = store.readQuery({
  query: booksQuery,
});

This gives us the data just like running a query with useQuery except it is grabbed from our InMemoryCache that we setup. Next, all we do is tell apollo to write a new object to the store for that query:

store.writeQuery({
  query: booksQuery,
  data: {
    books: data.books.filter(currentBook => currentBook.title !== title),
  },
});

In the code above, I am telling apollo to update the booksQuery data to filter out any book that has the title we passed into the mutation.

Loading indicators

Apollo also gives us a loading indicator built in to our queries. If you want to show a spinner or some other UI element when something is loading, it would look like this, inside Books.js

const Books = () => {
  let { data, loading, refetch } = useBooksQuery();
  let mutate = useDeleteBookMutation();

  if (loading) return <div>loading...</div>;
  if (!data || !data.books) return null;

  return data.books.map(book => (
    <div>
      <h3>{book.title}</h3>
      <p>{book.author}</p>
      <button onClick={() => refetch()}>Reload Books</button>
      <button onClick={() => mutate(book.title)}>Delete Book</button>
    </div>
  ));
};

In this example, I even threw in the refetch function. By default, the first time this component loads, it will show a loading indicator. If we trigger a refetch, it will load it behind the scenes, and not show the loading div. This is something that can be changed if needed. You can pass notifyOnNetworkStatusChange to our deleteBook mutation if you wanted it to set loading to true during refetches:

return deleteBook({
  variables: { title },
  notifyOnNetworkStatusChange: true
  ...
})

Apollo also gives us the flexibility to choose when to automatically refetch data. The default will load it once, then use the cache, until you manually call refetch, or in our case, reloading the page since we are using the in memory cache. You can specify other policies, as seen here. In a typical redux type application, or even the fetch example I wrote above, you wouldn’t have this level of control without bringing in another library. You would always be fetching when the component mounted.

Automatic Updates

You may be wondering… can Apollo be smarter? With the GraphQL type system, it can. We just need to do a couple easy things before I can show you this in action.

Open resolvers in the server folder, and let’s add some unique id’s to the books:

books: () => {
  return [
    {
      id: 1,
      title: 'Harry Potter and the Chamber of Secrets',
      author: 'J.K. Rowling',
    },
    {
      id: 2,
      title: 'Jurassic Park',
      author: 'Michael Crichton',
    },
  ];
};

Open typeDefs and add it to the book type:

type Book {
  id: Int!
  title: String!
  author: String!
}

Restart the server, and open our useBooksQuery in the web folder, and add it to the query:

books {
  id
  title
  author
}

Add Change Book Mutation

Cool, we are all set. Apollo client is smart enough to see something of the same type (Book) and the same id (1, or 2 in our case) it will update the local cache. So if we make a new resolver, called changeBookTitle in server/resolvers.js we can see this in action. Replace the contents of resolvers:

const books = [
  {
    id: 1,
    title: 'Harry Potter and the Chamber of Secrets',
    author: 'J.K. Rowling',
  },
  {
    id: 2,
    title: 'Jurassic Park',
    author: 'Michael Crichton',
  },
];

export const resolvers = {
  Query: {
    books: () => {
      return books;
    },
    authors: () => {
      return [
        { name: 'Todd', twitter: 'toddmotto' },
        { name: 'React', twitter: 'reactjs' },
      ];
    },
  },
  Mutation: {
    addAuthor: (_, { input: { name, twitter } }) => {
      return {
        name,
        twitter,
      };
    },
    deleteBook: (_, { title }) => true,
    changeBookTitle: (_, { input }) => {
      let { id, title } = input;

      let book = books.find(book => book.id === id);

      //Return the new book title
      return {
        ...book,
        title,
      };
    },
  },
};

I pulled out the books array so that we can mimic finding it in our database or elsewhere, and then returning the book object with the new title we pass in.

Now, we need to update our typeDefs:

 input ChangeBookInput {
    id: Int!
    title: String!
  }

  type Mutation {
    changeBookTitle(input: ChangeBookInput!): Book
    ...
  }

Create client side mutation

Our changeBookTitle resolver will take a book id, and a title, and return a Book object. Now we can create the client side query. Let’s name it src/pages/useChangeBookTitleMutation.js

import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';

export const mutation = gql`
  mutation ChangeBookTitle($input: ChangeBookInput!) {
    changeBookTitle(input: $input) {
      id
      title
    }
  }
`;

export default () => {
  let [mutate] = useMutation(mutation);

  return ({ id, title }) => {
    return mutate({
      variables: { input: { id, title } },
    });
  };
};

Hopefully you’re starting to see the pattern here.

  • Create a typeDef for a resolver
  • Make the resolver
  • Create the client side query / mutation
  • Use it!

Let’s make an input field called ChangeTitle.js inside src/pages:

import React, { useState } from 'react';
import useChangeBookTitleMutation from './useChangeBookTitleMutation';

const ChangeTitle = ({ book }) => {
  let changeTitle = useChangeBookTitleMutation();
  let [title, setTitle] = useState(book.title);

  return (
    <div>
      <input value={title} onChange={e => setTitle(e.target.value)} />
      <button onClick={() => changeTitle({ id: book.id, title })}>
        Change it!
      </button>
    </div>
  );
};

export default ChangeTitle;

Now, let’s add it to Books.js:

import React from 'react';
import useBooksQuery from './useBooksQuery';
import useDeleteBookMutation from './useDeleteBookMutation';
import ChangeTitle from './ChangeTitle';

const Books = () => {
  let { data } = useBooksQuery();
  let mutate = useDeleteBookMutation();

  if (!data || !data.books) return null;

  return data.books.map(book => (
    <div>
      <h3>{book.title}</h3>
      <p>{book.author}</p>
      <button onClick={() => mutate(book.title)}>Delete Book</button>
      <ChangeTitle book={book} />
    </div>
  ));
};

export default Books;

Phew! We did it. If you change some text in the first input field, and hit save. You will see that the title is automatically updated in the navbar, and the book itself. We didn’t have to do any state updates ourself.

This is why I love Apollo / GraphQL. Before, I would need a state management system that was a very manual process for doing anything, and it was a lot of code. Now, I will use Apollo for everything, and React Context, or Apollo’s local resolvers for local state.

Conclusion

To recap, we covered setting up Apollo in a React frontend. We compared it to a REST endpoint now that we can see all of the benefits. We also learned that the intelligent updating due to the type system, and ease of manual store updates, cuts down on the UI logic we had to write in the past.

You can clone the course repo and git checkout 3-client-integration-end to see the final code if you made a mistake along the way.

About the author

I've been using React for 5 years, React Native since release, and GraphQL for almost as long. I get excited about new technologies and being the first to figure them out. Serverless, penetration testing, and automating development are the things I am most excited about, other than React hooks! If you want to discuss anything I write here, please tweet me @zachcodes

Love the post? Share it!

Lots of time and effort go into all our blogs, resources and demos,
we'd love it if you'd spare a moment to share them!

Explore our Angular courses

Get started today and join over 60,000 developers.