Monday, May 10, 2021

Playing With Graphql

In the first junior dev sprint at work I've been looking into GraphQL and Apollo Client. My learning approach to feeling a bit more comfortable with these technologies was as follows:

going through tutorials and reading documentation on the different libraries I'd need to use and then building a small application

The application I chose to build was a classic todo CRUD app. Here is the client and server code. I decided to stick with languages I already felt comfortable with and therefore I'm using a Rails backend and React client (JavaScript). I could have implemented the React client in TypeScript but felt that it wasn't really necessary.

I knew fairly little about GraphQL coming into work at 99 but feel somewhat comfortable with some of the basics now. These basics include GraphQL schemas, queries, mutations and resolvers.

Setting up a basic GraphQL schema with Rails was fairly intuitive using the GraphQL ruby gem. I followed a Digital Ocean tutorial to get up to speed and soon had a basic backend implemented.

I was able to execute full CRUD actions on the graph via GraphiQL which was awesome to test out the API before even starting to implement the frontend. I like that you can basically type any letter and get some autocompletion whether it's a mutation or a query.

Some of the big changes moving from REST to GraphQL were:

  • No routes
  • No controllers, controller actions basically became resolvers
  • No mapping through arrays to create JSON data structures when you're joining between tables

I then came to the client implementation. This is what things look like right now.

Setting up Apollo Client is pretty simple. You create a client and pass it the uri and then wrap your entire <App /> in an <ApolloProvider /> which is the same as using something like React Context or Redux.

To query your database from any component you can invoke the useQuery hook, passing it a gql template string. This returns a number of things but all we need to know here is that it gives you data that you can then feed into your jsx. A mutation is similar; you get the useMutation hook, pass it a gql template string and then you can a function that receives variables that will perform the mutation for you.

Where I started to get a little bit confused with Apollo was when I tried to fire a createTask mutation and then redirect back to my homepage expecting that new task to appear in the list. It was not there on first try 😤!

It turned out that Apollo has a cache which kind of reminded me of having a global React Context state. In order for me to be able to write back to the cache and have the freshest data from that cache available on the homepage I needed to do something like this:

function CreateTask() {
  const [addTask] = useMutation(ADD_TASK, {
    update: (cache, { data }) => {
      const cachedData = cache.readQuery({
        query: TASKS,
      });
      cache.writeQuery({
        query: TASKS,
        data: {
          fetchTasks: [data.addTask.task, ...cachedData.fetchTasks],
        },
      });
    },
    onCompleted: () => {
      history.push("/");
    },
  });

  // jsx for form
}

It basically reads the old cache, gets the latest task from data and creates a new cache before redirecting back to the homepage.

Using the cache like this to me seemed like not a great solution for a simple problem. I did a bit more reading and realized that the useQuery hook accepts an object of options and one of those options is fetchPolicy.

This is what my current implementation looks like for the useQuery on the homepage:

const { data, loading, error } = useQuery(TASKS, {
  fetchPolicy: "network-only",
});

This ensures that the cache isn't used and data will always be fetched from the network. My addTask mutation is also now super simple.

const [addTask] = useMutation(ADD_TASK, {
  onCompleted: () => {
    history.push("/");
  },
});

There are a number of other fetch polices documented here. I currently don't understand what all of them do and will need to do some more testing.

My update and delete mutations were also pretty interesting and slightly different. The <Tasks /> component (homepage) also houses the removeTask mutation.

const [removeTask] = useMutation(REMOVE_TASK, {
  refetchQueries: [
    {
      query: TASKS,
    },
  ],
});

You can see here that the useMutation hook accepts a object of options one of which being refetchQueries. It takes an array of queries that are fired when the mutation has resolved. This is nice as when I remove a task I want the UI to update with the latest data in the database and refetch achieves this for me.

Updating resources with Apollo is almost the simplest solution to wire up.

const [editTask] = useMutation(EDIT_TASK, {
  onCompleted: () => {
    history.push("/");
  },
});

The mutation just needs to fire and Apollo magic then kicks in. This is taken straight from the docs:

If a mutation updates a single existing entity, Apollo Client can automatically update that entity's value in its cache when the mutation returns. To do so, the mutation must return the id of the modified entity, along with the values of the fields that were modified. Conveniently, mutations do this by default in Apollo Client.

I'm not exactly sure how this works in my instance as my data is from the network however I did notice that when I didn't have the fetchPolicy set in useQuery my updated entries were correct after redirecting.

Everything is working decently well but I'm having a couple of issues which I documented in my README.

  1. <React.StrictMode> throwing a warning of state update on unmounted component
  2. Had to add a typePolicy for my delete request to work correctly which I addressed by creating this file

An update to these issues was found today after a lot of debugging.

I used fetchPolicy: "no-cache" on my useQuery to ensure that the unmounted component state error disappeared. I think that I can use the cache if I pass more options into it but I don't quite understand them as yet. My addTask mutation is still using a refetch query to update the tasks array with the latest data from the server.

My removeTodo mutation now is just invoked and following it I use the refetch function that you get from a useQuery to again update the tasks array with the latest data.

I learnt a lot from this little project and it has definitely made me feel more comfortable working with GraphQL.