Data Fetching

Data fetching #

Fetching data and updating data via HTTP API are not that hard. Here is a basic example with axios:

// pages/index.tsx

import axios from 'axios'
import React from 'react'

async function getPosts() {
  const result = await axios.get('https://jsonplaceholder.typicode.com/posts?_limit=5')
  return result.data
}

async function addPost(title, body) {
  await axios.post('https://jsonplaceholder.typicode.com/posts', {
    title, body
  })
}

function IndexPage() {

  const [posts, setPosts] = React.useState([])

  async function initial() {
    const result = await getPosts()
    setPosts(result)
  }

  React.useEffect(() => {
    initial()
  }, [])

  async function onClickAddPost() {
    await addPost('foo', 'bar')
  }

  return (
    <>
      <div>
        {posts.map(post => {
          return (
            <div key={post.id}>
              {post.title}
            </div>
          )
        })}
      </div>
      <button onClick={onClickAddPost}>add post</button>
    </>
  )
}

export default IndexPage

image-20210612132751487

Every HTTP request has their own state:

  • loading status: is loading or is not.
  • if request success, the response data
  • if request failed, the response error data

Managing these state for every HTTP request will cause us to write duplicated code. This is why we need react-query.

There are two major concepts in react-query:

Setting up react-query in Next.js:

$ yarn add react-query

In pages/_app.tsx, add a QueryClientProvider at the top of the root component:

// pages/_app.tsx

import React from 'react'
import { QueryClient, QueryClientProvider } from 'react-query'

export const queryClient = new QueryClient()

export default function MyApp({ Component, pageProps }) {

	return (
   <QueryClientProvider client={queryClient}>
     <Component {...pageProps} />
   </QueryClientProvider>
 )
}

Query #

Fetching data from server is query.

// pages/index.tsx

import axios from 'axios'
import React from 'react'
import { useQuery } from 'react-query'

async function getPosts() {
  const result = await axios.get('https://jsonplaceholder.typicode.com/posts?_limit=5')
  return result.data
}

function IndexPage() {

  const getPostsQuery = useQuery('getPosts', getPosts)

  return (
    <>
      <div>
    		{getPostsQuery.isLoading && <div>Loading...</div>}
	      {getPostsquery.error && <div>Something error</div>}
        {getPostsQuery.data?.map(post => {
          return (
            <div key={post.id}>
              {post.title}
            </div>
          )
        })}
      </div>
    </>
  )
}

export default IndexPage

We use useQuery() to define a query. Every query has a key for being identify. react-query use this key to cache query result data. It means that if user has already queried getPosts, the other query with this key in other components will first get the cached data before fetching data again, and then refresh the cache and re-render the UI.

Imagine two pages that would display the posts list. They both have useQuery('getPosts'). When the user comes to the first page, he will see a loading spinner. But when he navigates to the second page, he will instantly see the posts list without waiting to load because the data was cached.

As you can see in the above code example, we can use isLoading to know if the HTTP request is loading. And we can use data to access to the response data.

Mutation #

Updating data (such as POST, PUT, DELETE request) is called mutation. To create a mutation, use useMutation(). This method accepts a mutation function. Then we can call .mutate() to muate a mutation with some variables. Let’s rewrite the above example:

// pages/index.tsx

import axios from 'axios'
import React from 'react'
import { useMutation, useQuery } from 'react-query'

async function getPosts() {
  const result = await axios.get('https://jsonplaceholder.typicode.com/posts?_limit=5')
  return result.data
}

async function addPost({ title, body }) {
  await axios.post('https://jsonplaceholder.typicode.com/posts', {
    title, body
  })
}

function IndexPage() {

  const getPostsQuery = useQuery('getPosts', getPosts)

  const addPostMutation = useMutation(addPost)

  function onClickAddPost() {
    addPostMutation.mutate({ title: 'foo', body: 'bar' })
  }

  return (
    <>
      <div>
        {getPostsQuery.isLoading && <div>Loading...</div>}
        {getPostsQuery.data?.map(post => {
          return (
            <div key={post.id}>
              {post.title}
            </div>
          )
        })}

	      {addPostMutation.isLoading && <div>Adding post...</div>}
        <button onClick={onClickAddPost}>Add post</button>
      </div>
    </>
  )
}

export default IndexPage

We can even do something when the mutation success or fail:

function onClickAddPost() {
  addPostMutation.mutate({ title: 'foo', body: 'bar' }, {
    onSuccess(data) {
      // ...
    },
    onError(err) {
      // ...
    }
  })
}

In general, when user add new post, the getPosts() result is supposed to be outdated. To make a better user experience, we should refetch all the query that associated with fetching posts list.

Every query has a refetch() method, we can call it when the mutation is success:

function onClickAddPost() {
  addPostMutation.mutate({ title: 'foo', body: 'bar' }, {
    onSuccess(data) {
+			getPostsQuery.refetch()
    },
    onError(err) {
      // ...
    }
  })
}

But it’s not a good idea. Because the queries asscociated with fetching posts list may appear in any where around the entire project. We should use an important feature in react-query —— Query Invalidation.

Query Invalidation #

You can call invalidateQueries() from queryClient to mark a query as stale, since every query has a unique key. In our example, we should mark getPosts query as stale:

+ import { queryClient } from './_app'
function onClickAddPost() {
  addPostMutation.mutate({ title: 'foo', body: 'bar' }, {
    onSuccess(data) {
-			getPostsQuery.refetch()
+			queryClient.invalidateQueries('getPosts')
    },
    onError(err) {
      // ...
    }
  })
}

When the query is marked as stale, react-query will refetch all the queries with the getPosts key. The UI will be automatically updated with the up-to-date data.