DEV Community

Cover image for React-Query basics - Setting up a basic query & mutation flow (with GraphQL and Axios)
Evane89
Evane89

Posted on

React-Query basics - Setting up a basic query & mutation flow (with GraphQL and Axios)

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:

  1. graphql-request. This is a very lightweight package that includes tools to create and send GraphQL queries and mutations.
  2. 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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 named blog.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
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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)