1. Getting Started, Why You Need It
The Overly Simple fetch
When using the native fetch API to fetch data, people usually write like this:
useEffect(() => {
fetch('https://api.example.com/students')
.then((res) => res.json())
.then((data) => {
setStudents(data)
})
}, [])
It's concise, but this approach can only be used in demos. Because the success or failure of the request is completely uncontrolled, and there is no guarantee that the page will work correctly.
If the loading time is too long without any feedback, or if the request fails without a front-end response, it's a fatal blow to the user experience.
Let's optimize it! Adding loading and error handling logic, the code becomes:
// A simple component to fetch student list
const StudentList = () => {
// Define state to store student data and loading status
const [students, setStudents] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
// Fetch data when component mounts
useEffect(() => {
const fetchStudents = async () => {
try {
setIsLoading(true)
const response = await fetch('https://api.example.com/students')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
setStudents(data)
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error occurred'))
} finally {
setIsLoading(false)
}
}
fetchStudents()
}, [])
// Loading state display
if (isLoading) {
return <div>Loading...</div>
}
// Error state display
if (error) {
return <div>Error: {error.message}</div>
}
return (
<ul>
{students.map((student) => (
<li key={student.id}>
<p>Name: {student.name}</p>
<p>Age: {student.age}</p>
<p>Gender: {student.gender}</p>
</li>
))}
</ul>
)
}
We're just handling the derived logic of a network request, but the code is already getting complex and boilerplate.
These derived states like isLoading and error need to be managed manually, and writing like this for every request is obviously cumbersome.
Race Conditions
Another common scenario is race conditions: when sending multiple similar requests, since we don't know which request finishes first, we can't guarantee that the final rendered result is the result of the last request.
The description above is a bit abstract. Let's imagine two scenarios:
There is a table that supports pagination. If a user quickly flips from page 1 to page 10, they expect to see the data from page 10. However, we cannot guarantee that the request for page 10 will return before the requests for other pages, so the result of page 10 might be overwritten by results from other pages.
An autocomplete component: the user types some content, and the component immediately fetches suggestions from the remote server. But because user input changes rapidly, the final displayed suggestions might be overwritten by a delayed response.
This is the "race condition" we need to solve.
One simple way to handle race conditions is to use a closure's active flag to prevent old requests from executing subsequent logic when the component re-renders.
useEffect(() => {
let active = true
const fetchData = async () => {
const response = await fetch(`https://swapi.dev/api/people/${props.id}/`)
const newData = await response.json()
if (active) {
setFetchedId(props.id)
setData(newData)
}
}
fetchData()
return () => {
active = false
}
}, [props.id])
Each time a request is initiated, a new active is created. Subsequent operations of the network request are executed only when active is true.
When the component re-renders or the page state becomes stale, active becomes false. This false is cached in the callback function as a closure, preventing the stale operations from executing.
Something Else
React official docs also list some disadvantages and alternatives to fetching data directly in useEffect, and recommend React Query, which I'd like to introduce to you.
Introducing Query
@tanstack/query (formerly react-query, hereinafter referred to as query) is an asynchronous state management tool.
It supports major front-end frameworks like React/Vue/Angular. I have deeply used both React and Vue versions in company projects, and the experience has been quite good.
An asynchronous state management tool, simply put, manages states obtained from asynchronous operations. Typically, it manages data returned from network requests and their derived states. This will be detailed later.
First, let's look at the library's achievements:
GitHub 42.7k ⭐, NPM weekly downloads 4,422,006.
This data looks impressive, and indeed it is; query is a very mainstream technology choice abroad.
Query seems relatively less popular in China and lacks comprehensive Chinese translations. That's also why I want to share this technology — to introduce good tools to more domestic developers.
Basic Usage of Query
This section covers the basic usage of Query. Initiating a network request is very simple: just call useQuery, which requires two parameters: queryKey and queryFn.
useQuery({
queryKey: any[],
queryFn: () => Promise
})
About queryFn
queryFn always returns a Promise, because Query doesn't care whether there is a real network request inside queryFn; it only cares about the state of the Promise.
If you use the browser's fetch API to write network requests, queryFn looks like this:
queryFn: async () => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Error status: ${response.status}`)
}
return response.json()
}
You don't need to wrap it with try-catch here; directly throwing an Error lets Query know that an error has occurred.
Query is positioned as an asynchronous state management tool. It doesn't conflict at all with synchronous state management libraries like pinia/jotai or network request libraries like axios, because they serve different purposes.
Therefore, for real projects, it is recommended to use network requests made with axios as the queryFn.
About queryKey
queryKey is an array, usually with the first element as a brief description of the state. For example, if fetching a student list, it could be ['student']. If fetching student details, it could be ['student', id].
Request-related parameters should also be placed in the queryKey array. Whenever the queryKey changes, the query function is re-executed, and the result is cached.
// id/sort/page/pageSize/sort etc. should all be placed in the query-key
useQuery({ queryKey: ['todo', sort], ... })
If the queryKey matches a key in the cache, Query will directly return the previous cached result, skipping the loading that blocks user operations, thus improving user experience.
queryKey might remind you of the dependency list of useEffect, but they are not exactly the same.
Query calculates a hash value based on queryKey, so you can safely use objects and arrays in queryKey. Also, Query ignores the order of object key-value pairs.
// These query-keys are equivalent
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })
Simple Example
A basic example: view the details of a book based on the selected book title.
import { useState } from 'react'
import { fetchBookDetail } from '../api/book'
import { useQuery } from '@tanstack/react-query'
const books = [
{ id: 1, title: 'React from Beginner to Master' },
{ id: 2, title: 'Deep Understanding of Vue' },
{ id: 3, title: 'Angular You Don\'t Know' },
]
const Intro = () => {
const [bookId, setBookId] = useState<number>(1)
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setBookId(parseInt(e.target.value))
}
const {
data: book,
isLoading,
isError,
error,
} = useQuery({
queryKey: ['book', bookId],
queryFn: () => fetchBookDetail(bookId),
})
if (isLoading) {
return <p>Loading...</p>
}
if (isError) {
return <p>Error: {error.message}</p>
}
return (
<>
<select value={bookId} onChange={handleChange}>
{books.map((book) => (
<option key={book.id} value={book.id}>
{book.title}
</option>
))}
</select>
<>
<h1>{book?.title}</h1>
<p>{book?.author}</p>
<p>{book?.description}</p>
</>
</>
)
}
export default Intro
Note that operations on book require the optional chaining operator (?), because book starts as undefined and becomes available data only after the asynchronous operation completes.
English
【@tanstack/query】1 Getting Started, Why You Need It
The Overly Simple fetch
When using the native fetch API to fetch data, people usually write like this:
useEffect(() => {
fetch('https://api.example.com/students')
.then((res) => res.json())
.then((data) => {
setStudents(data)
})
}, [])
It's concise, but this approach can only be used in demos. Because the success or failure of the request is completely uncontrolled, and there is no guarantee that the page will work correctly.
If the loading time is too long without any feedback, or if the request fails without a front-end response, it's a fatal blow to the user experience.
Let's optimize it! Adding loading and error handling logic, the code becomes:
// A simple component to fetch student list
const StudentList = () => {
// Define state to store student data and loading status
const [students, setStudents] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState(null)
// Fetch data when component mounts
useEffect(() => {
const fetchStudents = async () => {
try {
setIsLoading(true)
const response = await fetch('https://api.example.com/students')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
setStudents(data)
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error occurred'))
} finally {
setIsLoading(false)
}
}
fetchStudents()
}, [])
// Loading state display
if (isLoading) {
return <div>Loading...</div>
}
// Error state display
if (error) {
return <div>Error: {error.message}</div>
}
return (
<ul>
{students.map((student) => (
<li key={student.id}>
<p>Name: {student.name}</p>
<p>Age: {student.age}</p>
<p>Gender: {student.gender}</p>
</li>
))}
</ul>
)
}
We're just handling the derived logic of a network request, but the code is already getting complex and boilerplate.
These derived states like isLoading and error need to be managed manually, and writing like this for every request is obviously cumbersome.
Race Conditions
Another common scenario is race conditions: when sending multiple similar requests, since we don't know which request finishes first, we can't guarantee that the final rendered result is the result of the last request.
The description above is a bit abstract. Let's imagine two scenarios:
There is a table that supports pagination. If a user quickly flips from page 1 to page 10, they expect to see the data from page 10. However, we cannot guarantee that the request for page 10 will return before the requests for other pages, so the result of page 10 might be overwritten by results from other pages.
An autocomplete component: the user types some content, and the component immediately fetches suggestions from the remote server. But because user input changes rapidly, the final displayed suggestions might be overwritten by a delayed response.
This is the "race condition" we need to solve.
One simple way to handle race conditions is to use a closure's active flag to prevent old requests from executing subsequent logic when the component re-renders.
useEffect(() => {
let active = true
const fetchData = async () => {
const response = await fetch(`https://swapi.dev/api/people/${props.id}/`)
const newData = await response.json()
if (active) {
setFetchedId(props.id)
setData(newData)
}
}
fetchData()
return () => {
active = false
}
}, [props.id])
Each time a request is initiated, a new active is created. Subsequent operations of the network request are executed only when active is true.
When the component re-renders or the page state becomes stale, active becomes false. This false is cached in the callback function as a closure, preventing the stale operations from executing.
Something Else
React official docs also list some disadvantages and alternatives to fetching data directly in useEffect, and recommend React Query, which I'd like to introduce to you.
Introducing Query
@tanstack/query (formerly react-query, hereinafter referred to as query) is an asynchronous state management tool.
It supports major front-end frameworks like React/Vue/Angular. I have deeply used both React and Vue versions in company projects, and the experience has been quite good.
An asynchronous state management tool, simply put, manages states obtained from asynchronous operations. Typically, it manages data returned from network requests and their derived states. This will be detailed later.
First, let's look at the library's achievements:
GitHub 42.7k ⭐, NPM weekly downloads 4,422,006.
This data looks impressive, and indeed it is; query is a very mainstream technology choice abroad.
Query seems relatively less popular in China and lacks comprehensive Chinese translations. That's also why I want to share this technology — to introduce good tools to more domestic developers.
Basic Usage of Query
This section covers the basic usage of Query. Initiating a network request is very simple: just call useQuery, which requires two parameters: queryKey and queryFn.
useQuery({
queryKey: any[],
queryFn: () => Promise
})
About queryFn
queryFn always returns a Promise, because Query doesn't care whether there is a real network request inside queryFn; it only cares about the state of the Promise.
If you use the browser's fetch API to write network requests, queryFn looks like this:
queryFn: async () => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Error status: ${response.status}`)
}
return response.json()
}
You don't need to wrap it with try-catch here; directly throwing an Error lets Query know that an error has occurred.
Query is positioned as an asynchronous state management tool. It doesn't conflict at all with synchronous state management libraries like pinia/jotai or network request libraries like axios, because they serve different purposes.
Therefore, for real projects, it is recommended to use network requests made with axios as the queryFn.
About queryKey
queryKey is an array, usually with the first element as a brief description of the state. For example, if fetching a student list, it could be ['student']. If fetching student details, it could be ['student', id].
Request-related parameters should also be placed in the queryKey array. Whenever the queryKey changes, the query function is re-executed, and the result is cached.
// id/sort/page/pageSize/sort etc. should all be placed in the query-key
useQuery({ queryKey: ['todo', sort], ... })
If the queryKey matches a key in the cache, Query will directly return the previous cached result, skipping the loading that blocks user operations, thus improving user experience.
queryKey might remind you of the dependency list of useEffect, but they are not exactly the same.
Query calculates a hash value based on queryKey, so you can safely use objects and arrays in queryKey. Also, Query ignores the order of object key-value pairs.
// These query-keys are equivalent
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })
Simple Example
A basic example: view the details of a book based on the selected book title.
import { useState } from 'react'
import { fetchBookDetail } from '../api/book'
import { useQuery } from '@tanstack/react-query'
const books = [
{ id: 1, title: 'React from Beginner to Master' },
{ id: 2, title: 'Deep Understanding of Vue' },
{ id: 3, title: 'Angular You Don\'t Know' },
]
const Intro = () => {
const [bookId, setBookId] = useState<number>(1)
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setBookId(parseInt(e.target.value))
}
const {
data: book,
isLoading,
isError,
error,
} = useQuery({
queryKey: ['book', bookId],
queryFn: () => fetchBookDetail(bookId),
})
if (isLoading) {
return <p>Loading...</p>
}
if (isError) {
return <p>Error: {error.message}</p>
}
return (
<>
<select value={bookId} onChange={handleChange}>
{books.map((book) => (
<option key={book.id} value={book.id}>
{book.title}
</option>
))}
</select>
<>
<h1>{book?.title}</h1>
<p>{book?.author}</p>
<p>{book?.description}</p>
</>
</>
)
}
export default Intro
Note that operations on book require the optional chaining operator (?), because book starts as undefined and becomes available data only after the asynchronous operation completes.