React Query: How to Fetch Data on Click?

By Hemanta Sundaray on 2022-10-31

So, here is what we want.

We have a Fetch Posts button.

FetchPosts Button

When we click on this button, we want to fetch and display a list of blog posts from a fake REST API using the useQuery hook from React Query. In other words, we want to disable the query until we click on the FetchPosts button.

We can achieve this using two steps.

First, we set the enabled key to false.

Remember the signature of the useQuery hook? It looks like this: useQuery(queryKey, queryFn, config). We can pass a config object as the third argument to the useQuery hook. This is where we add enabled: false.

Second, we use the refetch function returned from useQuery to manually trigger the fetch.

Let's look at our code:

src/components/Posts.js
import React from "react"
import { useQuery } from "@tanstack/react-query"
import axios from "axios"

const Posts = () => {
  const {
    data: posts,
    isFetching,
    isLoading,
    isError,
    error,
    refetch,
  } = useQuery(
    ["posts"],
    async () => {
      const { data } = await axios.get(
        `https://jsonplaceholder.typicode.com/posts`
      )
      return data
    },
    { enabled: false }
  )

  return (
    <>
      <button
        onClick={() => refetch()}
        className="bg-gray-200 hover:bg-gray-300 rounded m-6 px-1 py-0.5 "
      >
        Fetch Posts
      </button>

      <h1 className="mb-6 text-blue-400 text-center">
        {isFetching && "Fetching posts...."}
      </h1>

      {posts ? (
        posts.map(post => (
          <h1 className="w-1/2 m-auto" key={post.title}>
            {post.title}
          </h1>
        ))
      ) : isLoading ? (
        <h1 className="text-blue-400 text-center">Loading...</h1>
      ) : isError ? (
        <h1>{error.message}</h1>
      ) : (
        ""
      )}
    </>
  )
}

export default Posts

We have PERMANENTLY disabled the query from automatically running. The query will now run when we click on the Fetch Posts button, which will call the refetch function, which in turn will trigger the fetch.

Note that Permanently disabling the query has several consequences. One of them is:

  • The query will not refetch in the background.

You can find out what happens when we set enabled to false in the offical React Query docs here.

We have run into another issue: we have the text Loading... on the screen.

Loading State

This happens because when we disable the query, the query is in status: loading state right from the start, because loading means there is no data yet. For scenarios like these, React Query provides a isInitialLoading flag, which we can use to show a spinner. The flag will be true if the query is fetching for the first time.

Let's change isLoading to isInitialLoading:

src/components/Posts.js
import React from "react"
import { useQuery } from "@tanstack/react-query"
import axios from "axios"

const Posts = () => {
  const {
    data: posts,
    isFetching,
    isInitialLoading,
    isError,
    error,
    refetch,
  } = useQuery(
    ["posts"],
    async () => {
      const { data } = await axios.get(
        `https://jsonplaceholder.typicode.com/posts`
      )
      return data
    },
    { enabled: false }
  )

  return (
    <>
      <button
        onClick={() => refetch()}
        className="bg-gray-200 hover:bg-gray-300 rounded m-6 px-1 py-0.5 "
      >
        Fetch Posts
      </button>

      <h1 className="mb-6 text-blue-400 text-center">
        {isFetching && "Fetching posts...."}
      </h1>

      {posts ? (
        posts.map(post => (
          <h1 className="w-1/2 m-auto" key={post.title}>
            {post.title}
          </h1>
        ))
      ) : isInitialLoading ? (
        <h1 className="text-blue-400 text-center">Loading...</h1>
      ) : isError ? (
        <h1>{error.message}</h1>
      ) : (
        ""
      )}
    </>
  )
}

export default Posts

Click on the FetchPosts button and we have our posts:

Blog Posts

Now, if we click on a different tab and come back to the tab where our React app is running, we will not see the text Fetching posts..... That's because we have set enabled to false, which has disabled background refetching of our query.

We are now successfully fetching data on the click of a button.

We have achieved our objective. Or have we?

Remember what we said at the start? We said that we want to disable the query UNTIL we click on the FetchPosts button. In other words, the query should be enabled after we click on the button. But we have permanently disabled our query. So, we have got to make some changes.

All we need to do is set the value of the enabled key to a boolean.

src/components/Posts.js
import React, { useState } from "react"
import { useQuery } from "@tanstack/react-query"
import axios from "axios"

const Posts = () => {
  const [fetch, setFetch] = useState(false)

  const {
    data: posts,
    isFetching,
    isInitialLoading,
    isError,
    error,
    refetch,
  } = useQuery(
    ["posts"],
    async () => {
      const { data } = await axios.get(
        `https://jsonplaceholder.typicode.com/posts`
      )
      return data
    },
    { enabled: fetch }
  )

  const handleFetchPosts = () => {
    setFetch(true)
    refetch()
  }

  return (
    <>
      <button
        onClick={handleFetchPosts}
        className="bg-gray-200 hover:bg-gray-300 rounded m-6 px-1 py-0.5 "
      >
        Fetch Posts
      </button>

      <h1 className="mb-6 text-blue-400 text-center">
        {isFetching && "Fetching posts...."}
      </h1>

      {posts ? (
        posts.map(post => (
          <h1 className="w-1/2 m-auto" key={post.title}>
            {post.title}
          </h1>
        ))
      ) : isInitialLoading ? (
        <h1 className="text-blue-400 text-center">Loading...</h1>
      ) : isError ? (
        <h1>{error.message}</h1>
      ) : (
        ""
      )}
    </>
  )
}

export default Posts

The initial value of enabled is false. When we click the Fetch Posts button, we change the value to true.

Now, if we go to a different tab and come back to the tab where our app is running, we see the text Fetching posts..... Background refetching on window refocus is working.

Great.

We have one last change to make before we are done.

We never keep the data fetching logic and the UI logic in the same component. What we will do is: we will extract the data fetching logic to a custom hook and use that hook in the Posts component.

import { useQuery } from "@tanstack/react-query"
import axios from "axios"

export const usePostHook = fetch => {
  return useQuery(
    ["posts", fetch],
    async () => {
      const { data } = await axios.get(
        `https://jsonplaceholder.typicode.com/posts`
      )
      return data
    },
    { enabled: fetch }
  )
}

export default usePostHook
src/components/Posts.js
import React, { useState } from "react"
import { usePostHook } from "./hooks/postHook"

const Posts = () => {
  const [fetch, setFetch] = useState(false)

  const {
    data: posts,
    isFetching,
    isInitialLoading,
    isError,
    error,
    refetch,
  } = usePostHook(fetch)

  const handleFetchPosts = () => {
    setFetch(true)
    refetch()
  }

  return (
    <>
      <button
        onClick={handleFetchPosts}
        className="bg-gray-200 hover:bg-gray-300 rounded m-6 px-1 py-0.5 "
      >
        Fetch Posts
      </button>

      <h1 className="mb-6 text-blue-400 text-center">
        {isFetching && "Fetching posts...."}
      </h1>

      {posts ? (
        posts.map(post => (
          <h1 className="w-1/2 m-auto" key={post.title}>
            {post.title}
          </h1>
        ))
      ) : isInitialLoading ? (
        <h1 className="text-blue-400 text-center">Loading...</h1>
      ) : isError ? (
        <h1>{error.message}</h1>
      ) : (
        ""
      )}
    </>
  )
}

export default Posts

We can go one step further & instead of managing a local state in the Posts component, we can manage the state in the usePostHook itself.

This would look something like this:

import { useState } from "react"
import { useQuery } from "@tanstack/react-query"
import axios from "axios"

export const usePostHook = () => {
  const [fetch, setFetch] = useState(false)
  const queryResult = useQuery(
    ["posts", fetch],
    async () => {
      const { data } = await axios.get(
        `https://jsonplaceholder.typicode.com/posts`
      )
      return data
    },
    { enabled: fetch }
  )

  return [() => setFetch(true), queryResult]
}

export default usePostHook
src/components/Posts.js
import React from "react"
import { usePostHook } from "./hooks/postHook"

const Posts = () => {
  const [
    setFetch,
    { data: posts, isFetching, isInitialLoading, isError, error, refetch },
  ] = usePostHook()

  const handleFetchPosts = () => {
    setFetch()
    refetch()
  }

  return (
    <>
      <button
        onClick={handleFetchPosts}
        className="bg-gray-200 hover:bg-gray-300 rounded m-6 px-1 py-0.5 "
      >
        Fetch Posts
      </button>

      <h1 className="mb-6 text-blue-400 text-center">
        {isFetching && "Fetching posts...."}
      </h1>

      {posts ? (
        posts.map(post => (
          <h1 className="w-1/2 m-auto" key={post.title}>
            {post.title}
          </h1>
        ))
      ) : isInitialLoading ? (
        <h1 className="text-blue-400 text-center">Loading...</h1>
      ) : isError ? (
        <h1>{error.message}</h1>
      ) : (
        ""
      )}
    </>
  )
}

export default Posts

And now, we are done.

If you want to get notified when I publish a new blog post or want to about know the projects I am working on and the tech I am exploring, consider subscribing to my weekly newsletter below.

Join the Newsletter