So you've heard about React Query. Be it from a coworker, a friend or a blog, you're ready to dive into the docs and get to it. Awesome! That's how I started using it too. In this post I will attempt to show you how you can set up a basic query and mutation flow to use in your projects.
Small disclaimer: the examples provided reflect the way we use react-query at my company and might differ from your use case and/or needs.
What is react-query?
react-query
is a library for React/NextJS apps that allows us to effectively use the server as our state. Meaning: whenever stuff changes on the back-end, update the front-end state. This is extremely useful in cases where data tends to change regularly.
Another feature that makes this library amazing is its caching system. By configuring it correctly, it caches queries and only updates the cache when needed (ie: when the cache is stale and no longer in sync with the server).
It's a very basic description, but for this post it should suffice. Be sure to read through the official docs if you want to know more details.
react-query
is network-tool-agnostic, meaning you can use GraphQL, fetch, Axios, whichever works for you. In this example we'll be using GraphQL and Axios.
Other packages
In this example we require 2 other packages to be installed:
-
graphql-request
. This is a very lightweight package that includes tools to create and send GraphQL queries and mutations. -
axios
. A great promise based HTTP tool.
Step 1 - Setting up the library and endpoints
I'm going to assume you know how to install a NPM package and include it in your project. The most important part is, of course, installing the react-query
package and making sure your main App component file looks something like this.
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
// These are the react-query devtools.
// Not required, but very useful
<ReactQueryDevtools initialIsOpen={false} />
// ... the rest of your app structure here
</QueryClientProvider>
);
}
After completing this, you'll have connected react-query to your application succesfully.
The next step would be to create a constants file where your API endpoints reside. It's generally a good idea to keep these in a seperate file instead of hardcoding your endpoints everywhere in your codebase.
// /constants/endpoints.constants.js
// In this example we append /graphql to the base URL
// because our back-end exposes a special endpoint
// for GraphQL stuff.
// Please check with your own back-end / API / CMS
// what the correct approach is.
export const ENDPOINTS = {
GRAPHQL: `${
process.env.NODE_ENV === "development"
? process.env.REACT_APP_DEV_API_URL
: process.env.REACT_APP_API_URL
}/graphql`,
REST: `${
process.env.NODE_ENV === "development"
? process.env.REACT_APP_DEV_API_URL
: process.env.REACT_APP_API_URL
}`
... your other endpoints here
};
Step 2 - Setting up GraphQL
If you're not using GraphQL and prefer to use regular API requests, you can skip this step.
-
Create a GraphQL folder structure:
/graphql/client
/graphql/queries
/graphql/mutations
Create a client file.
// /graphql/client/GraphQLClient.js
import { GraphQLClient } from "graphql-request";
import { ENDPOINTS } from "../constants/endpoints.constants";
const client = new GraphQLClient(ENDPOINTS.GRAPHQL);
// This function will be used to send queries via GraphQL
export const queryWithoutAuthToken = async (query, variables) => {
return await client.request(query, variables);
};
Great! That's the config part done! Onto the cool stuff...
Step 3 - Creating a page
To demonstrate how everything works, we'll be creating a detail page. This could be a detail page for a blog, a news article or something else. I'm leaving the routing up to you as it's beyond the scope of this guide. Just make sure the detail page receives a param in the form of an ID (ie. /blog/post/:unique-id
).
Let's start with the basics and set up the component.
export default function DetailPage({params}) {
const { id: postID } = params;
return (
<main>
<header>
<h1>This is a detail page</h1>
</header>
<section>
<p>Post ID: {postID}</p>
</section>
</main>
)
}
Awesome. Now to fetch the post data.
You might be used to doing that like this:
// A very rudimentary example of
// fetching data on a detail page.
export default function DetailPage({params}) {
const { id: postID } = params;
const [postData, setPostData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
const fetchPostData = async () => {
try {
setLoading(true);
const response = await axios.get(`<URL>/${postID}`);
setPostData(response.data.data);
} catch (error) {
console.error(error);
setError(true);
} finally {
setLoading(false);
}
}
fetchPostData();
}, [postID]);
if (loading) {
return (
<p>Loading post data...</p>
);
}
if (error) {
return (
<p>There was an error loading the post...</p>
);
}
return (
<main>
<header>
<h1>{postData.title}</h1>
</header>
<section>
<p>Post ID: {postID}</p>
<p>{postData.description}</p>
</section>
</main>
)
}
As you can see in the code example above, each time the postID
param changes, the useEffect will trigger resulting in a refetch of the page data. But how can we make this more elegant using react-query
?
Step 4 - Creating queries
First, we'll need a query for fetching the post data.
-
In
/graphql/queries
we create a queries file namedblog.queries.js
This file will be used to store all the different queries related to blog posts.
You can expand this with multiple different files for different content types, resulting in a nice looking structure:
/graphql/queries/blog.queries.js /graphql/queries/articles.queries.js /graphql/queries/videos.queries.js /graphql/queries/comments.queries.js
Again, this is completely up to you but we prefer to do it this way.
Example query:
// blog.queries.js
import { gql } from "graphql-request";
export const queryBlogpostByID = gql`
query ($id: ID!) {
blogposts(id: $id) {
id
date_created
date_updated
author
title
view_count
description
// ... the rest of your blogpost fields
}
}
`;
Step 5 - Implementing React-Query in the page component
With GraphQL
// Demonstrating the use of react-query to refactor the previous example
import { useQuery } from "react-query";
import { queryWithoutAuthToken } from '/graphql/client/GraphQLClient'
import { queryBlogpostByID } from '/graphql/queries/blog.queries.js'
export default function DetailPage({params}) {
const { id: postID } = params;
// The useQuery hook returns an object important keys
// - isLoading - the query is being executed and therefore loading is true
// - error - there was an error in the request
// - data - if succesful, the data returned from the query
const {
isLoading: postDataLoading,
error: postDataError,
data: postData,
// The first argument of the hook is a query key.
// react-query uses this key to differentiate between different queries.
// In this case we've used the postID.
} = useQuery(`fetchBlogPost-${postID}`, () =>
// Here we execute the function we created back in step 2,
// taking the query we created as the first argument
// and an object containing the ID as the second.
queryWithoutAuthToken(queryBlogpostByID, {
id: postID,
})
);
// ... the rest of the detail page component, omitted for brevity.
}
With Axios
// Demonstrating the use of react-query
// to refactor the previous example
import { useQuery } from "react-query";
import { ENDPOINTS } from "/constants/endpoints.constants.js"
export default function DetailPage({params}) {
const { id: postID } = params;
const {
isLoading: postDataLoading,
error: postDataError,
data: postData,
} = useQuery(`fetchBlogPost-${postID}`, () =>
// Here we return the axios call
// to the endpoint that returns a blogpost
axios.get(ENDPOINTS.REST + `/blog/posts/${postID}`)
);
// ... the rest of the detail page component, omitted for brevity.
}
If all is set up correctly, the implementation will be done. Your data will be fetched on mount and subsequent reloads will return the cached data instead of refetching again and again. Only when the postID changes will a new query be executed. With built-in loading and error states, react-query is a very neat solution for fetching data and working with cache. No local state is needed.
Step 6 - Creating mutations
If you're not using GraphQL, you can skip this step.
There are cases where you'd want to update the data. For this we have access to the useMutation
hook, allowing us to update the data and invalidate any queries.
But first we'll need to add a mutation. In the same vain as adding a query, we create a mutation file.
// /graphql/mutations/blog.mutations.js
import { gql } from "graphql-request";
// Note that the type of the $data param is of type update_blogpost_input.
// This type is probably different depending on
// how your backend has set this up.
// Refer to their docs to get the proper type.
export const UpdateBlogpostMutation = gql`
mutation ($id: ID!, $data: update_blogpost_input!) {
update_blogpost(id: $id, data: $data) {
id
}
}
`;
In GraphQLClient.js
we add another function below the queryWithoutAuthToken
function.
// /graphql/client/GraphQLClient.js
import { GraphQLClient } from "graphql-request";
import { ENDPOINTS } from "../constants/endpoints.constants";
const client = new GraphQLClient(ENDPOINTS.GRAPHQL);
// This function will be used to send queries via GraphQL
export const queryWithoutAuthToken = async (query, variables) => {
return await client.request(query, variables);
};
// This function will be used to send mutations via GraphQL
export const mutateWithoutAuthToken = async (mutation, variables) => {
return await client.request(mutation, variables);
};
Step 7 - Adding the mutation
Back in our page component, we'll add a mutation for updating the view count.
Note that in this example we're leaving out more detailed code on when to trigger this mutation (for example on page load, on page leave, etc.).
With GraphQL
import { useQuery, useMutation, useQueryClient } from "react-query";
import { queryWithoutAuthToken, mutateWithoutAuthToken } from '/graphql/client/GraphQLClient'
import { queryBlogpostByID } from '/graphql/queries/blog.queries.js'
import { UpdateBlogpostMutation } from '/graphql/mutations/blog.mutations.js'
export default function DetailPage({params}) {
// We need to add the useQueryClient hook to access the query client itself
const queryClient = useQueryClient();
const { id: postID } = params;
const {
isLoading: postDataLoading,
error: postDataError,
data: postData,
} = useQuery(`fetchBlogPost-${postID}`, () =>
queryWithoutAuthToken(queryBlogpostByID, {
id: postID,
})
);
// The useMutation hook returns (among others)
// the mutate key, which is a function that triggers
// the mutation and receives a single param.
// In this example we've named the param "payload".
const { mutate: updateBlogpostMutation } = useMutation(
async (payload) => {
await mutateWithoutAuthToken(UpdateBlogpostMutation, {
id: payload.id,
data: payload.data,
});
},
{
onSuccess: () => {
// By providing the invalidateQueries method
// with an array of keys, react-query will invalidate the
// cache of queries associated with those keys
// and refetch them.
// Note that you can add multiple keys here,
// even from different content types if you'd like.
queryClient.invalidateQueries([`fetchBlogPost-${postID}`]);
// success handling here...
},
onError: (error) => {
console.log(error);
// other error handling here...
},
}
);
// ... the rest of the detail page component, omitted for brevity.
}
With Axios
import { useQuery, useMutation, useQueryClient } from "react-query";
export default function DetailPage({params}) {
const queryClient = useQueryClient();
const { id: postID } = params;
const {
isLoading: postDataLoading,
error: postDataError,
data: postData,
} = useQuery(`fetchBlogPost-${postID}`, () =>
axios.get(ENDPOINTS.REST + `/blog/posts/${postID}`)
);
const { mutate: updateBlogpostMutation } = useMutation(
async (payload) => {
axios.post(ENDPOINTS.REST + `/blog/posts/${postID}`, {
id: postID
})
},
{
onSuccess: () => {
queryClient.invalidateQueries([`fetchBlogPost-${postID}`]);
// success handling here...
},
onError: (error) => {
console.log(error);
// other error handling here...
},
}
);
// ... the rest of the detail page component, omitted for brevity.
}
Once everything is set up correctly and the mutation is triggered, you'll notice the data updating immediately. Magic!
Step 8 - Adding authentication
If your application relies on users being authenticated and having a valid authentication token, we'd suggest expanding the GraphQLClient.js file with the following functions.
// /graphql/client/GraphQLClient.js
import { GraphQLClient } from "graphql-request";
import { ENDPOINTS } from "../constants/endpoints.constants";
const client = new GraphQLClient(ENDPOINTS.GRAPHQL);
// For queries that don't require a token
export const queryWithoutAuthToken = async (query, variables) => {
return await client.request(query, variables);
};
// For queries that require serverside authentication
export const queryWithAuthToken = async (query, token, variables) => {
if (!token) throw new Error("No Token provided in query handler");
const requestHeaders = {
authorization: `Bearer ${token}`,
};
return await client.request(query, variables, requestHeaders);
};
// For mutations that don't require a token
export const mutateWithoutAuthToken = async (mutation, variables) => {
return await client.request(mutation, variables);
};
// For mutations that require serverside authentication
export const mutateWithAuthToken = async (mutation, token, variables) => {
if (!token) throw new Error("No Token provided in mutation handler");
const requestHeaders = {
authorization: `Bearer ${token}`,
};
return await client.request(mutation, variables, requestHeaders);
};
Closing remarks
Using react-query
for our query and mutation logic has proved to be a great developer experience. We were able to reduce the codebase of certain projects by at least 40% using this amazing library.
The API is simple and intuitive and provides much more features than described in this post. Be sure to dive into the official docs as there are many different configuration options available.
Cool next steps would be to create your own custom hooks based on your content for easy reuse and maintenance purposes.
I hope this post has proven useful to you as it is my first ever dev blogpost! Of course, your approach to using react-query may differ from ours, so if you have any suggestions feel free to send them my way.
Thanks for your time! Happy coding!
Top comments (0)