Hetav Desai

Hetav Desai

Using GraphQL with Axios and Redux

Explained: Why I'm using this combination, how can you use it and how to handle errors?

Using GraphQL with Axios and Redux
Featured on Hashnode

Subscribe to my newsletter and never miss my upcoming articles

Listen to this article

Hello folks, in this article I'll be showing you how you can use GraphQL with Axios and Redux, along with error handling. This is not a beginner tutorial. Hence a basic understanding of how GraphQL, Axios, and Redux work will be helpful. And if you are familiar with these technologies, let me tell you that this combination isn't as difficult as you might be wondering.

A not-so-little context as to why such a different combination

Recently I started working on a project with a primary motive to understand and practice redux-toolkit well. I've been using REST APIs for a while, and now I wanted to try my hands on GraphQL too. So I kicked off this project by setting up CRA with redux-toolkit for the frontend and Hasura for the backend.

Now that these two things were in place, I started exploring different GraphQL client libraries that I could use and from most resources, I found the following options – apollo client, react-query and urql. I soon realized that these libraries won't work for me for the following reasons:

  1. These libraries provide hooks like useQuery and useMutation for accessing GraphQL APIs
  2. I was using createAsyncThunk from redux-toolkit for state management
  3. The hooks provided by libraries won't work with createAsyncThunk as it is not a React component

After further digging, I found a client library called graphql-request which would let me access GraphQL APIs without using hooks. But the issue with this one was that I could not set default global headers and I would need to create multiple instances of GraphQLClient, which seemed rather wasteful.

All of this got me wondering, can't I just use axios for GraphQL? And the answer is YES.

Using Axios for accessing GraphQL API

Axios is an HTTP client library, and hence it can be used to access GraphQL APIs as they're served over HTTP. We'll see how we can do that with a small example.

Let's suppose that we have a huge database of Users and we want to fetch details of a single user based on userId. The GraphQL query for this operation will be:

query ($userId: uuid!) {
  users(where: {id: {_eq: $userId}}) {
    name
    email
  }
}

The above query will take userId as a required variable of type uuid and return the name and email of the user whose userId matches the passed userId.

Let's assume the API endpoint to be https://api.somelink.com/graphql. We can execute our query using axios as mentioned below.

const response = await axios.post("https://api.somelink.com/graphql", {
  query: `
    query ($userId: uuid!) {
      users(where: {id: {_eq: $userId}}) {
        name
        email
      }
    }
  `,
  variables: {
    userId: "user-12345",
  },
});

One thing to pay attention to is that no matter if we're doing a query or a mutation, we'll always make a POST call as we need to pass query and variables as request body.

Yes, that's it.

Let's combine this with Redux

Let's consider a scenario where we have a Profile component to display the name, and email of a user. When this component loads, we want to fetch the details of a user from the API and render it on the screen and maintain the state using redux-toolkit. To do this, we'll follow the below steps:

1. Write state management logic in reducer

// userSlice.js

import axios from "axios";
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";

export const loadUser = createAsyncThunk(
  "post/loadUser",
  async ({ userId }) => {
    const response = await axios.post("https://api.somelink.com/graphql", {
      query: `
        query ($userId: uuid!) {
          users(where: {id: {_eq: $userId}}) {
            name
            email
          }
        }
      `,
      variables: {
        userId: "user-12345",
      },
    });
    return response;
  }
);

export const userSlice = createSlice({
  name: "user",
  initialState: {
    status: "idle",
    user: {},
    errorMessage: "",
  },
  reducers: {},
  extraReducers: {
    [loadUser.pending]: (state, action) => {
      state.status = "loading";
    },
    [loadUser.fulfilled]: (state, { payload }) => {
      state.user = payload.data.data.user;
      state.status = "fulfilled";
    },
    [loadUser.rejected]: (state, action) => {
      state.status = "error";
      state.errorMessage = "Could not fetch data. Please refresh to try again."
    },
  },
});

The loadUser function will be called when dispatched from React component. It'll make a network call to fetch required user from the database.

Based on the state of Promise of network call, the corresponding functions in the extraReducers will be executed.

2. Create a Profile Component

// ProfileComponent.jsx

import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { loadUsers } from './postSlice.js';

export default function ProfileComponent() {
  const dispatch = useDispatch();
  const { user, status, errorMessage } = useSelector((state) => state.user);

  useEffect(() => {
    status === "idle" && dispatch(loadUser());
  }, [])

  return status === "loading" && <p>Loading...</p>

  return status === "error" && <p>{errorMessage}</p>

  return (
    <div>
      <p>{user.name}</p>
      <p>{user.email}</p>
      <p>{user.contact_number}</p>
    </div>
  )
}

The ProfileComponent consumes the redux state via useSelector and fires a dispatch action loadUser() when it is loaded.

It'll show Loading... while the user data is being fetched. Once the data is received, it'll render it on the screen.

Error Handling

In case of error, we would expect that, the [loadUser.rejected] part of extraReducer will be executed and the error will be handled.

But the catch here is that GraphQL doesn't respond with error status codes like REST and hence all the responses end up with status 200.

Here's is the format in which GraphQL API would send response in case of success and error.

/*
GraphQL API Response (Success)
It responds with data object and
it is enclosed in data field of axios response
*/
{
  config: {...},
  data: {
    data: {
      users: [{
        name: "John Doe",
        email: "johndoe@mail.com"
      }],
    },
  },
  headers: {...},
  request: {...},
  status: 200,
  statusText: "OK"
}

/*
GraphQL API Response (Error)
It responds with array of error objects and
it is enclosed in data field of axios response
*/
{
  config: {...},
  data: {
    errors: [{
      "extensions": {
        "path": "$.selectionSet.users.selectionSet.contact",
        "code": "validation-failed"
      },
      "message": "field \"contact\" not found in type: 'users'"
    }]
  },
  headers: {...},
  request: {...},
  status: 200,
  statusText: "OK"
}

Now because of this format of response, even in case of error, [loadUser.rejected] in extraReducers doesn't get executed, instead [loadUser.fulfilled] gets executed. So we need to do some error handling manually.

To handle this, at the end of createAsyncThunk we'll check the contents of the response to see if we're getting data object or array of error objects. If we get array of error objects, we'll explicitly throw an Error or else we'll return the response as it is.

export const loadUser = createAsyncThunk(
  "post/loadUser",
  async ({ userId }) => {
    const response = await axios.post("https://api.somelink.com/graphql", {
      query: `
        query ($userId: uuid!) {
          users(where: {id: {_eq: $userId}}) {
            name
            email
          }
        }
      `,
      variables: {
        userId: "user-12345",
      },
    });
    if(response.data.errors) {
      throw new Error("Could not fetch data. Please refresh to try again.")
    }
    return response.data.data;
  }
);

We can catch this in the extraReducers as below

[loadUser.rejected]: (state, action) => {
  state.status = "error";
  state.errorMessage = action.error.message;
},

That's how we use GraphQL with Axios and Redux

Conclusion

In this blog, we learned how to use GraphQL with Axios and Redux and how to handle errors in this process.

Now the question is – Is it recommended to use this combination in apps? Probably not. I did it as I was creating a project for learning purposes.

For a production app, using one of the well-known libraries like Apollo Client, urql, etc. would be a better choice. But in case you're using REST API across the project, and need to use GraphQL API in one or two places, then this method can come in very handy.

Thank you for reading the blog. Do drop your feedback in the comments below and if you liked it, share it with your developer friends who might find it useful too. If you want to have any discussion around this topic, feel free to reach out to me on Twitter

 
Share this