Custom useQuery and loading/error states
In this lesson, we'll address how we can handle the loading and error state of our GraphQL queries with our custom useQuery Hook.
📝 This lesson's quiz can be found - here. 🗒️ Solutions for this lesson's quiz can be found - here.
Though our useQuery
Hook works as intended, we haven't taken into account the tracking of the loading and error states of our queries.
Loading#
We'll first address the loading state of our request. When we say loading, we're essentially referring to being able to track the status of our asynchronous request. If the request is in flight, the UI should reflect this with a loading indicator of sorts. And when complete, we should be presented with the expected data.
To keep track of loading, we'll introduce a new loading field into the State
interface of the state object tracked in our custom useQuery
Hook. We'll declare the type of the loading
field as boolean
.
interface State<TData> {
data: TData | null;
loading: boolean;
}
We'll initialize the loading
value as false
in our state initialization.
export const useQuery = <TData = any>(query: string) => {
const [state, setState] = useState<State<TData>>({
data: null,
loading: false
});
// ...
};
At the beginning of the fetchApi()
method within the memoized fetch
callback, we'll set the loading
state to true
while also specifying that our state data
is still null
. When a request is complete we'll set loading
back to false
.
export const useQuery = <TData = any>(query: string) => {
const [state, setState] = useState<State<TData>>({
data: null,
loading: false
});
const fetch = useCallback(() => {
const fetchApi = async () => {
setState({ data: null, loading: true });
const { data } = await server.fetch<TData>({
query
});
setState({ data, loading: false });
};
fetchApi();
}, [query]);
// ...
};
We've already used the spread syntax to return everything within state
at the end of useQuery
. We can now destruct the loading
property from the useQuery
Hook used in the <Listings>
component.
If loading
is ever true
in our <Listings>
component, we'll render a simple header tag that says 'Loading...'
. When loading
is set back to false, the title and the listings list is to be shown.
export const Listings = ({ title }: Props) => {
const { data, loading, refetch } = useQuery<ListingsData>(LISTINGS);
// ...
if (loading) {
return <h2>Loading</h2>;
}
return (
<div>
<h2>{title}</h2>
{listingsList}
</div>
);
};
We'll ensure both the Node server and React client apps are running.
server $: npm run start
client $: npm run start
And in the browser, we'll now notice a brief 'Loading...'
message when the query request is in flight.

Errors#
We'll now address what would happen if our server.fetch()
function was to error out since our <Listings>
component isn't currently prepared to handle this.
With Apollo Server and GraphQL, errors can be a little unique. Oftentimes when a query has failed and returns an error - our server may treat that query as successful since the query request was made successfully.
Let's see an example of this. We'll briefly dive back into out Node server application and take a look at the listings
resolver function within the server/src/graphql/resolvers/Listing/index.ts
file.
export const listingResolvers: IResolvers = {
Query: {
listings: async (
_root: undefined,
_args: {},
{ db }: { db: Database }
): Promise<Listing[]> => {
return await db.listings.find({}).toArray();
}
}
// ...
};
The listings
resolver simply returns all the listing documents from the database collection we've set up in MongoDB Atlas. We'll temporarily throw an error before the return
statement of the resolver function to mimic if an error was to occur.
export const listingResolvers: IResolvers = {
Query: {
listings: async (
_root: undefined,
_args: {},
{ db }: { db: Database }
): Promise<Listing[]> => {
throw new Error("Error!");
return await db.listings.find({}).toArray();
}
}
// ...
};
We can refresh our browser to attempt to query the listings
field again. We're not going to get the information we're expected from the API but if we take a look at our browser's Network
tab and find the post API request made, we can see that the request made to /api
was successful with status code 200!

If we take a look at the response from the API request, whether through the browser Network
tab or GraphQL Playground, we can see the server returns data
as null and an errors
array is populated.

When an error is thrown in Apollo Server, the error gets populated inside the errors
array that contains information about the errors added by Apollo Server. This complies with the GraphQL specification - if an error is thrown, the field should return data of null
while the error is to be added to the errors
field of the response.
On this note, we should specify that the response from the server can return an errors
field along with a data
field.
The errors
field from our server is an array where each item has a few different properties to display different information (message
, extensions
, locations
, etc). In the src/lib/api/server.ts
file, we'll create an interface for what an error would look like when returned from the server. We'll create an Error
interface and keep things simple by stating message
as the only property we intend to access from an error.
interface Error {
message: string;
}
In our return statement from the server.fetch()
function, we'll state in our type assertion that the returned object will contain an errors
field of type Error[]
.
export const server = {
fetch: async <TData = any, TVariables = any>(body: Body<TVariables>) => {
const res = await fetch("/api", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
return res.json() as Promise<{
data: TData;
errors: Error[];
}>;
}
};
This page is a preview of TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL