DEV Community

Nicholas Coffey
Nicholas Coffey

Posted on β€’ Edited on

6

Creating a Basic 'useQuery' React Hook

This post can also be found on my personal blog.


Often times when creating a React application that fetches data, I find myself repeating the same fetch logic in multiple components. In order to make my code more DRY (Don't Repeat Yourself), I decided to extract this logic into one reusable custom hook.

An example component before the custom hook

import { Fragment, useEffect, useState } from 'react'
import axios from 'axios'

// type data from https://jsonplaceholder.typicode.com/posts
type Post = {
  userId: number
  id: number
  title: string
  body: string
}

export default function Posts() {
  const [posts, setPosts] = useState<Post[]>()
  const [error, setError] = useState<string>()
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    axios
      .get<Post[]>('https://jsonplaceholder.typicode.com/posts')
      .then(res => {
        setPosts(res.data)
        setLoading(false)
      })
      .catch(err => {
        setError(err)
        setLoading(false)
      })
  }, [])

  if (error) {
    return <p>Error: {error}</p>
  } else if (loading) {
    return <p>Loading...</p>
  }

  return (
    <>
      {posts.map(({ title, body }, index) => (
        <Fragment key={index}>
          <h1>{title}</h1>
          <p>{body}</p>
        </Fragment>
      ))}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

The logic in the Posts component above allows for a reactive component that shows posts they are loaded, when posts are still loading, and when there is an error fetching the posts. However, if another component need the same logic, like a list of users, that component would need to copy this logic.

What are hooks?

Hooks are simply functions that have access to other React hooks like useState and useEffect. Unlike components, hooks can return whatever they want. This functionality is exactly what allows us to pull our data fetching logic into one reusable place.

Making a 'useQuery' Hook

import { useState, useEffect, useCallback } from 'react'
import axios, { AxiosResponse } from 'axios'

export default function useQuery<T>(url: string) {
  const [data, setData] = useState<T>()
  const [error, setError] = useState<string>()
  const [loading, setLoading] = useState(false)

  const handleError = (error: any) => {
    setError(error.response?.data.err)
    setLoading(false)
  }

  // this function is calling useCallback to stop an infinite loop since it is in the dependency array of useEffect
  const runQuery = useCallback(() => {
    const handleSuccess = (res: AxiosResponse<T>) => {
      setData(res.data)
      setLoading(false)
    }

    setLoading(true)
    axios.get<T>(url).then(handleSuccess).catch(handleError)
  }, [url])

  useEffect(() => {
    runQuery()
  }, [runQuery])

  return { data, loading, error, refetch: runQuery }
}
Enter fullscreen mode Exit fullscreen mode

This new hook allows us to fetch data from an API, while checking for errors and whether or not it's still loading just like in the Posts component above! To briefly explain how it works, when the hook is first mounted it will call runQuery enabled by the useEffect hook. The runQuery function uses axios to call the url passed into the hook and sets the data, loading, and error states depending on the API's response like in the Posts component's useEffect call. Then, the hook returns an object containing the data, loading, and error states as well the runQuery function renamed to refetch in case a component needs to get the data again.

Using 'useQuery' in the Posts component

import { Fragment } from 'react'
import useQuery from './useQuery'

// type data from https://jsonplaceholder.typicode.com/posts
type Post = {
  userId: number
  id: number
  title: string
  body: string
}

export default function Posts() {
  const { data: posts, loading, error } = useQuery<Post[]>('https://jsonplaceholder.typicode.com/posts')

  if (error) {
    return <p>Error: {error}</p>
  } else if (loading) {
    return <p>Loading...</p>
  }

  return (
    <>
      {posts.map(({ title, body }, index) => (
        <Fragment key={index}>
          <h1>{title}</h1>
          <p>{body}</p>
        </Fragment>
      ))}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

As seen above, the Posts component is now much cleaner. Instead of calling useState three times, only one call to the new useQuery hook is needed. All that's required is a url string to be passed in as well as an optional generic type to make the returned data typed. Then after destructuring the returned object into separate posts, error, and loading constants, all the logic below should remain the same. This is great, but what is more valuable is the fact that this hook can now be used in all of our components! If a user list component was needed, as mentioned above, useQuery could be used again just like in the Posts component giving that Users component access to it's own data, loading, and error states.

Conclusion

I was pleasantly surprised how easy it was to make my own custom React hook. It took a little more work and time upfront but now have a reusable hook that I can use in any React application I build in the future!

Playwright CLI Flags Tutorial

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • 0:56 --last-failed: Zero in on just the tests that failed in your previous run
  • 2:34 --only-changed: Test only the spec files you've modified in git
  • 4:27 --repeat-each: Run tests multiple times to catch flaky behavior before it reaches production
  • 5:15 --forbid-only: Prevent accidental test.only commits from breaking your CI pipeline
  • 5:51 --ui --headed --workers 1: Debug visually with browser windows and sequential test execution

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Click on any timestamp above to jump directly to that section in the tutorial!

Watch Full Video πŸ“ΉοΈ

Top comments (1)

Collapse
 
rlaffers profile image
Richard Laffers β€’

I think there is a flaw. In your success handler you forgot to check if the React component is still mounted. Trying to set local state on an unmounted component is an error. I recommend reading robinwieruch.de/react-fetching-data

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

πŸ‘‹ Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay