Cookie Consent by Free Privacy Policy Generator ๐Ÿ“Œ React query - The juicy parts

๐Ÿ  Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeitrรคge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden รœberblick รผber die wichtigsten Aspekte der IT-Sicherheit in einer sich stรคndig verรคndernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch รผbersetzen, erst Englisch auswรคhlen dann wieder Deutsch!

Google Android Playstore Download Button fรผr Team IT Security



๐Ÿ“š React query - The juicy parts


๐Ÿ’ก Newskategorie: Programmierung
๐Ÿ”— Quelle: dev.to

So I came across react query when I was building my first startup and I checked it out. On the one of the pages it said if you handled caching, pagination etc, then you're good, so I was like "I've handled all these myself, I'm good" (I was wrong) and I ended up not using react-query, but then I kept seeing people praise react-query so I decided to use it for my second startup and ooh boy, it's so dang good that I'm glad I didn't use it at first because I now appreciate the problems it solves, the time it saves and the overall developer experience is excellent.
In this article, I'll talk about;

  • Problems react-query solves
  • Using react-query (focusing on the juicy parts)

Problems React-query solves

In a typical full-stack application there is an HTTP request and response cycle going on. There are different request methods these include; GET, POST, PUT, DELETE, PATCH etc. These methods can be grouped into two groups i.e fetching data from server and updating server data. GET falls under fetching data and the rest fall under updating data, because of this, the foundation of react-query is also fetching and updating data. Just fetching and updating data is easy you can do it with axios or the browser fetch api, but in most applications just fetching and updating data is not enough to give users a good experience and also not optimizing this process will result to excess load on the server and so you want to cache data when necessary, when a user makes a POST request you update already fetched data or invalidate cache if necessary, also if data is too large, you fetch the data in chunks (pages) and so you'll need to handle pagination
in summary, react-query solves these common problems;

  • querying (parallel queries, dependent queries etc)
  • caching
  • pagination
  • mutations (updating data)
  • invalidating and updating cached data (managing state)

React-query is a lot and so based on the above common problems I'll be focusing on the juicy parts.

Using react-query (focusing on the juicy parts)

Based on my experience, I found the juicy parts to be;

  • useQuery
  • useQueries
  • useMutation
  • useQueryClient
  • useInfiniteQuery

1. useQuery

useQuery is the hook used to make http GET requests.
A basic usage example is

   const { data, isLoading, isError } = useQuery(
        ['users'],
        () => axios.get('https://api.example.com').then((res) => res.data),
        {
            staleTime: 1000,
        }
    );

The useQuery hook takes three arguments, you can also use the object syntax which is basically the same thing.

  • query key
  • query function and
  • options

useQuery(queryKey, queryFn, options)

Query key

The query key is what react-query uses to know which cached data to return when you're making the same request.react-query checks if there is data associated with the cache key and if the data is valid, then it returns that data.
How does react query know if data is stale or invalid?
you tell react-query when cached data should not be used by using the staleTime option and invalidateQuery method of useQueryClient we'll get into those later.
They query key can be a string, an array of strings and an array with nested objects. When a single string is used as query Key, it's converted to an array with that string as the only item.
Examples

//Valid query keys
 queryKey = "followers" //Converted to ["followers"]
 queryKey = ["followers", {user_name:"john_doe"}]
 queryKey = ["followers", 5, {user_name:"jane_doe", verified:true}]

Query Keys are hashed deterministically, which simply means for query keys with nested objects, the order of the keys of those objects don't matter.
These query keys are all equal

 useQuery(['users', { verified, followers_count }], queryFn, options)
 useQuery(['users', { followers_count, verified }], queryFn, options)

However for array items the order is important

// These query keys are not equal
useQuery(["followers", user_name], queryFn, options)
useQuery([user_name, "followers"], queryFn, options)

Query function

The query function is the function that makes the API call and whatever data the function returns is what get assigned to {data} returned by the useQuery hook.
For example:

 const fetchUsers = () => axios.get("https://api.example.com")
 //Here "data" will be axios response
 const {data} = useQuery(["users"], fetchUsers) 
 console.log(data) /*
    {
  // `data` is the response that was provided by the server
  data: {users: ["user1", "user2", "user3", "user4"]},
  status: 200,
  statusText: 'OK',
  headers: {},
  config: {},
  request: {}
}
*/

With this value of data if we want to access the data returned by the server (users) will have to use data.data.users
But when making API mostly is the data returned by the server that we want, so it's good to make your query function return that instead
Example

 const fetchUsers = () => axios.get("https://api.example.com").then(res => res.data)

 //Here "data" will be what server returns
 const {data} = useQuery(["users"], fetchUsers) 
 console.log(data) // {users: ["user1", "user2", "user3", "user4"]

You don't need to catch errors, react-query will do that for you and return it as error in the useQuery return object, more on that later.

Options

Options is the optional third parameter of the userQuery hook.
There are lot of options you can add to the Options object but as usual, we are going to focus on the juicy ones, you can check the react-query documentation for the rest.
The ones we are focusing on include;

  • staleTime
  • enabled
  • retry
  • onSuccess
  • onError
  • onSettledd
staleTime

The time in milliseconds after which data is considered stale. If set to Infinity the cached data will never be stale. It defaults to 0.
When staleTime is 0, react-query will always re-fetch data whenever the component is mounted or when user focuses on window (when they focus after leaving the browser) but when staleTime is set, react-query by default will not re-fetch data until the time set has passed
Example:

  const { data, isLoading } = useQuery(
        ["users"],
        fetchUsers,
        {
            staleTime: 60 * 1000 // 1 minute
        }
    )

react-query by default will only re-fetch data if last fetch time was more than 1 minute ago.

Notice I keep using "by default"? this is because this behavior can be changed, you can tell react-query to re-fetch data on component mount or on window focus regardless of if data is stale or not.
Here is how

    const options = {
        refetchOnMount: "always",
        refetchOnWindowFocus: "always" 
    }
enabled

The enabled option is used to tell react-query whether to fetch the data or not, react-query fetches data only if enabled is true. For example if you have a search feature on your app and you only want to make the API call when the user enters up to three characters. You can do that by using the enabled option.

Example:

  const [query, setQuery] = useState("")

  const { data, isLoading } = useQuery(
        ["search"],
        searchUsers,
        {
            enabled: query.length > 2
        }
    )
retry

retry option simply tell react-query what to do when a query fails.
if set to true react-query will retry infinitely until the query is successful.
if set to false react-query will not retry
alternatively you can set it to a number
eg: if set to 3 ({retry: 3}) react-query will retry 3 times when the query first fails

onSuccess, onError and onSettled

onSuccess is a function that gets called when a query is successful
onError is a function that gets called when a query fails and onSettled is a function that gets called when query is settled (whether it's successful or it failed)

    const [query, setQuery] = useState("")

    const { data, isLoading } = useQuery(
        ["search"],
        searchUsers,
        {
            enabled: query.length > 2,
            onSuccess: (data) => { 
                // Called when data is successfully fetched

            },
            onError: (error) => {
                // Called when query fails

            },
            onSettled: (data, error) => { 
                // Always called regardless of the outcome

            }
        }
    )

useQuery return Object

Went through how to use useQuery, now let's talk about what it returns. the object returned by the useQuery hooks has a lot of properties and will focus on the ...
Here there are

  • data this is the data returned by the query function if query is successful
  • error this receives the error as a result of a failed query (network error or error return by server)
  • isLoading a boolean which is set to true if there's no cached data and no query attempt was finished yet and false otherwise
  • isError a boolean which is set to true if query fails
  • isFetching a boolean which is set to true whenever the queryFn is executing, which includes initial loading as well as background refetches and false otherwise.
  • isSuccess is true if a query is successful

The difference between isLoading and isFetching is that isLoading is only set to true on first fetch (i.e no cache data, or query has not be executed before) but isFetching is set to true on first fetch and when fetching is happening in the background (refetching on mount, refetching on window focus etc)

2. useQueries

useQueries is simply making two or more useQuery at the same time, and it returns and array of useQuery return object for each query
Usage:

const results = useQueries({
  queries: [
    { queryKey: ['user', user_name], queryFn: fetchUser, staleTime: Infinity},
    { queryKey: ['posts', user_name], queryFn: fetchUserPosts, staleTime: Infinity}
  ]
})

In the above example results is an array of useQuery return value for each query inside the queries array

3. useMutation

This hooks is used to make update HTTP requests. these requests make changes to the data on the server as a result it could make already cached data with useQuery in correct, we'll learn how to use useMutation in combination with useQueryClient to make changes to data on the server and invalidate cached data that may be affected by those changes.

Basic Usage:

 const changeName = useMutation(
        (newName) => {
            return axios.put(`/user/name`, {name: newName})
        },
        {
            onSuccess: (data) => {
                const { message, type } = data.data 
            },
            onError: (err) => {
               console.log(err)
            }
        }
    )

That is how you initialize a react-query mutation, But this doesn't make any request yet. calling useMutation returns and object with the following properties

  • mutate
  • isLoading
  • isError
  • isSuccess
  • data
  • error etc.

We'll be focusing mutate because the rest work similar to that of useQuery

mutate is function that when called makes the actual update http request.
with regards to the above example, to make api request to change a user's name will do something like this

function ChangeName() {

    const changeName = useMutation(
        (newName) => {
            return axios.put(`/user/name`, { name: newName })
        },
        {
            onSuccess: (data) => {
              // Success same as that of useQuery
            },
            onError: (err) => {
                // Error same as that of useQuery
            },
        }
    )
    return (
        <form
            onSubmit={(event) => {
                event.preventDefault()
                // Call mutate to invoke the useMutation function
                changeName.mutate(new FormData(event.currentTarget).get('new_name'))
            }}
        >
            <input name="new_name" />
            <button type="submit">Change</button>
        </form>
    )
}

That's mostly all you need to know about useMutation.
In the above code the use just change their name, assuming we've fetched user data with useQuery and used ["user"] as our query key with staleTime of Inifinity this will mean that old name will continue to be showed even though the name has changed. We need a way to tell react-query that the cached data is not longer correct and it should be refetched. we'll discuss that in the next chapter

4. useQueryClient

The useQueryClient hooks returns the QueryClient instance which is passed as a prop to QueryClientProvider which means it gives us access to the heart of react-query and we can do cools things with it. We'll focus on using it to invalidate queries and also update cached data

Invalidating queries

So in the above useMutation example we needed a way to tell react-query that the user data is no longer accurate and so it should refetch. This is how we do that using useQueryClient.

function ChangeName() {
    const queryclient = useQueryClient()
    const changeName = useMutation(
        (newName) => {
            return axios.put(`/user/name`, { name: newName })
        },
        {
            onSuccess: (data) => {
                // This will invalidate the cached user data
                // and the user data will be refetched
                queryclient.invalidateQueries(["user"])
            },
            onError: (err) => {
                // Error same as that of useQuery
            },
        }
    )

    return (
        <form
            onSubmit={(event) => {
                event.preventDefault()
                // Call mutate to invoke the useMutation function
                changeName.mutate(new FormData(event.currentTarget).get('new_name'))
            }}
        >
            <input name="new_name" />
            <button type="submit">Change</button>
        </form>
    )
}

What we just did above is good, but should be we refetch the user data just because the name has changed? NO. Since the change happened on the users browser and we know the new name we could just update the user's cached data with the new name by use setQueryData method of the queryClient you can use that to modify cached data.
Here's how:

function ChangeName() {
    const queryclient = useQueryClient()
    const [newName, setNewName] = useState("")
    const changeName = useMutation(
        (newName) => {
            return axios.put(`/user/name`, { name: newName })
        },
        {
            onSuccess: (data) => {
                // This doesn't cause refetch and 
                // we also have correct data 
                queryclient.setQueryData(["user"], (oldData) => {
                    return { ...oldData, name: new_name }
                })
            },
            onError: (err) => {
                // Error same as that of useQuery
            },
        }
    )

    return (
        <form
            onSubmit={(event) => {
                event.preventDefault()
                // Call mutate to invoke the useMutation function
                changeName.mutate(newName)
            }}
        >
            <input name="new_name" value={newName} onChange={(e) => setNewName(e.target.value)} />
            <button type="submit">Change</button>
        </form>
    )
}

Another important aspect of invalidating queries is using the right query keys.

 const queryclient = useQueryClient()

    // The code below also invalidates queries with the following keys
    // ["user", "followers"]
    // ["user", {user_name}]
    queryclient.invalidateQueries(["user"])

Alternative you can use the exact property to make sure only queries that exactly match the query key get invalidated

  const queryclient = useQueryClient()

    // Does not invalidate queries with the following keys
    // ["user", "followers"]
    // ["user", {user_name}]
    queryclient.invalidateQueries(["user"], { exact: true })

5. useInfiniteQuery

Assuming you're building an application that has comment section and a post has 2000 comments, when a user clicks to see the comments you wouldn't want to send all 2000 comments to the user reason being the user can not read all 2000 comments, you'll be using too much bandwidth and it'll take a long time to load, instead you'll want to fetch the comments in chunks say 1-10 and when user clicks load more or has finished reading those (when the last comment is in view) you then fetch 11-20, then 21-30 and so on. You can do this with axios and useState but react-query made incredibly easy.
Like the useQuery hook, useInfiniteQuery takes queryKey, queryFn and options you can also box queryKey and queryFn in the options object and it's also what the react-query team recommends.
useInfiniteQuery also takes another important option which is getNextPageParam and it also returns an important function called fetchNextPage. These two functions are very important when using useInfiteQuery

Usage:

const fetchComments = ({ pageParam = 0 }) => axios.get(`${BASEURL}/comments/${postID}/${pageParam}`).then(res => res.data)

 const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
  } = useInfiniteQuery(["comments", { postID }], fetchComments,
    {
      // Function that tells react-query the page to fetch next
      getNextPageParam: (lastPage, pages) => {
        if (lastPage.pagination.isEnd) {
          return undefined
        }
        return lastPage.pagination.next
      },
      staleTime: Infinity
    })

Let's break the above code down starting with pageParam
pageParam is cursor or the page we're fetching, for the initial fetch, it's 0 or 1 depending on you handle pages on the backend. So what about the subsequent queries how does react-query now the page it should fetch? that's what the getNextPageParam does, It is a callback function that react-query passes lastPage and pages to and then calls the function whatever the function returns becomes the pageParam for the next query, if it returnsundefined it means we've reached the last page and there are no more pages to fetch, If the getNextPageParam returns undefined, the hasNextPage property of the returned value of useInfiniteQuery is set to false otherwise it's set to true. You can use the hasNextPage value to know whether to show load more button or not.
the lastPage parameter is the data returned by the server after the last fetch and pages is data of all fetches (pages)
do determine what page to fetch next, the server normally returns something like {comments: [], pagination:{next:11, isEnd: false}}
So the get the next pageParam we use lastPage.pagination.next and if the isEnd is true we return undefined to tell react-query that we've reached the end of the comments. Once you have your getNextPageParam function set up all you need to do to fetch next page is to call the fetchNextPage property of the useInfiniteQuery return object. So when user clicks on load more you call fetchNextPage

Example putting it all together

function Comments() {
    const fetchComments = ({ pageParam = 0 }) => axios.get("/post/comments/${postID}/${pageParam}").then(res => res.data)
    const {
        data,
        fetchNextPage,
        hasNextPage,
        isFetching,
    } = useInfiniteQuery(["comments", { postID }], fetchComments,
        {
            getNextPageParam: (lastPage, pages) => {
                if (lastPage.pagination.end) {
                    return undefined
                }
                return lastPage.pagination.next
            },
            staleTime: Infinity
        })
    // Since data returns an array of pages
    // you'll need to combine all the pages into one array
    // so you can map all comments and render on user's screen
    let comments: any[] = []
    if (data?.pages) {
        for (let page of data?.pages) {
            comments = comments.concat(page.comments)
        }
    }

    return (
        <div>
            {
                comments.map((comment) => {
                    return (
                        <Comment
                            key={comment.id}
                            user_name={comment.user_name}
                            comment={comment.comment}
                            date={new Date(comment.created_at)}
                            likes_count={comment.likes_count}
                        />
                    )
                })
            }
            {
                hasNextPage && (
                    <div style={{height:"10px"}}>
                        {!isFetching ?
                            <button onClick={() => fetchNextPage()}>Load more</button>
                            :
                            <Loading />
                        }

                    </div>
                )
            }
        </div>
    )
}

Alternatively, you can use intersectionObserver to automatically load comments so user doesn't have to click on load more making it an infinite scroll.
Example:

function Comments() {
    const fetchComments = ({ pageParam = 0 }) => axios.get("/post/comments/${postID}/${pageParam}").then(res => res.data)
    const {
        data,
        fetchNextPage,
        hasNextPage,
        isFetching,
    } = useInfiniteQuery(["comments", { postID }], fetchComments,
        {
            getNextPageParam: (lastPage, pages) => {
                if (lastPage.pagination.end) {
                    return undefined
                }
                return lastPage.pagination.next
            },
            staleTime: Infinity
        })
    // Since data returns an array of pages
    // you'll need to combine all the pages into one array
    // so you can map all comments and render on user's screen
    let comments: any[] = []
    if (data?.pages) {
        for (let page of data?.pages) {
            comments = comments.concat(page.comments)
        }
    }

    // Load comments automatically use intersectionObserver api
    const loaderRef = useRef(null)
    useEffect(() => {
        const observer = new IntersectionObserver(
            ([entry]) => {
                if (entry.isIntersecting) {
                    fetchNextPage()
                }
            },
            { threshold: 0.5 }
        );

        if (loaderRef.current) {
            observer.observe(loaderRef.current);
        }

        return () => observer.disconnect();
    }, []);

    return (
        <div>
            {
                comments.map((comment) => {
                    return (
                        <Comment
                            key={comment.id}
                            user_name={comment.user_name}
                            comment={comment.comment}
                            date={new Date(comment.created_at)}
                            likes_count={comment.likes_count}
                        />
                    )
                })
            }
            {
                hasNextPage && (
                    <div ref={loaderRef} style={{ height: "10px" }}>
                        {isFetching && <Loading />}
                    </div>
                )
            }
        </div>
    )
}

If you are not familiar with intersectionObserver you can learn more about it here

Conclusion

The topics we've discussed in this blog are the ones I found most useful, If you find yourself trying to do something not covered in this article, there is a high chance react-query allows you to do that, check the react-query documentation and you'll find it, the react-query team thought of everything.

...



๐Ÿ“Œ React query - The juicy parts


๐Ÿ“ˆ 59.81 Punkte

๐Ÿ“Œ This Week In React #127: Nextra, React-Query, React Documentary, Storybook, Remix, Tamagui, Solito, TC39, Rome...


๐Ÿ“ˆ 39.4 Punkte

๐Ÿ“Œ This Week In React #131: useReducer, Controlled Inputs, Async React, DevTools, React-Query, Storybook, Remix, RN , Expo...


๐Ÿ“ˆ 39.4 Punkte

๐Ÿ“Œ This Week In React #142: React-Query, Million, OpenNext, Ariakit, Expo-Image, React-Three-Fiber, TS 5.1, Node.js 20, WebGPU...


๐Ÿ“ˆ 39.4 Punkte

๐Ÿ“Œ This Week In React #146: Concurrency, Server Components, Next.js, React-Query, Remix, Expo Router, Skia, React-Native...


๐Ÿ“ˆ 39.4 Punkte

๐Ÿ“Œ Building a CRUD App with Next.js, React Query, React Hook Form, and Yup


๐Ÿ“ˆ 30.07 Punkte

๐Ÿ“Œ This Week In React #139: React.dev, Remix, Server Components, Error Boundary, Wakuwork, React-Native, Bottom Sheet...


๐Ÿ“ˆ 27.98 Punkte

๐Ÿ“Œ A Serverless Query Engine from Spare Parts


๐Ÿ“ˆ 25.98 Punkte

๐Ÿ“Œ Open Databases a Juicy Extortion Target


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Hotspot Shield Vulnerability Could Reveal โ€˜Juicyโ€™ Info About Users, Researcher Claims


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ ID Thieves Turn to Snail Mail as Juicy Target for Financial Crimes


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Mit Juicy Fields zum legalen Cannabis-Bauer werden!


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ SourceWolf - Amazingly Fast Response Crawler To Find Juicy Stuff In The Source Code!


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Gamer Credentials Now a Booming, Juicy Target for Hackers


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Sensitive Juicy Dorks, Data Exposure


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Juicy Marbles: Das vegane Filet Mignon aus Europa im Test


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Juicy Fields: mutmaรŸlicher Cannabis-Betrug zog Razzia nach sich


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ 8 Juicy Chrome Extensions for Web Developers


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ The Last of Us Part I got a juicy patch with Steam Deck improvements


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ OilRigโ€™s Outer Space and Juicy Mix: Same olโ€™ rig, new drill pipes


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Sifchain: Found a url on source code which was disclosing different juicy informations like ip addresses and available endponts


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Bugsnax bound for Steam in 2022; developers tease "juicy surprises"


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ Juicy Regular Expression(REGEX) Patterns For Everyone


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ OilRig's Outer Space und Juicy Mix: Neue Werkzeuge fรผr dieselbe alte Bohrinsel


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ DireSearch -- Easy Scanner for Juicy Files


๐Ÿ“ˆ 24.5 Punkte

๐Ÿ“Œ How to Learn the Hard Parts of React โ€“ and Tips to Conquer Them


๐Ÿ“ˆ 23.88 Punkte

๐Ÿ“Œ Trend Micro ScanMail for Exchange 12.0 Log Query/Quarantine Query cross site scripting


๐Ÿ“ˆ 22.84 Punkte

๐Ÿ“Œ Nelson Open Source ERP 6.3.1 db/utils/query/data.xml query sql injection


๐Ÿ“ˆ 22.84 Punkte

๐Ÿ“Œ Trend Micro ScanMail for Exchange 12.0 Log Query/Quarantine Query Cross Site Scripting


๐Ÿ“ˆ 22.84 Punkte

๐Ÿ“Œ More DNS over HTTPS: Become One With the Packet. Be the Query. See the Query, (Thu, Dec 19th)


๐Ÿ“ˆ 22.84 Punkte

๐Ÿ“Œ GitHub Security Lab: ihsinme: CPP add query for: CPP Add query for CWE-20 Improper Input Validation


๐Ÿ“ˆ 22.84 Punkte

๐Ÿ“Œ CVE-2016-15020 | liftkit database up to 2.13.1 src/Query/Query.php processOrderBy sql injection


๐Ÿ“ˆ 22.84 Punkte

๐Ÿ“Œ TANStack Query: How It Changes the Way You Query APIs


๐Ÿ“ˆ 22.84 Punkte

๐Ÿ“Œ ISC BIND up to 9.9.4 DNS Query bin/named/query.c query_findclosestnsec3 NSEC3-Signed Zones memory corruption


๐Ÿ“ˆ 22.84 Punkte











matomo