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.
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:
These libraries provide hooks like useQuery
and useMutation
for accessing GraphQL APIs
I was using createAsyncThunk
from redux-toolkit
for state management
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.
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 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:
// 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.
// 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.
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
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