React-Query
React-query thường được sử dụng để quản lý, lưu trữ các dữ liệu được truy vấn từ server một cách hiệu quả. Một số tính năng chính của react-query như: gọi API ( fetching data ), tự động cache dữ liệu từ API trả về, tự động refetch khi dữ liệu stale ( bị lỗi thời ),...
Installation: npm i @tanstack/react-query pnpm add @tanstack/react-query yarn add @tanstack/react-query bun add @tanstack/react-query
Ví dụ cơ bản về react-query
Trong ví dụ này sẽ bao gồm 3 khái niệm chính của react-query là Queries, Mutation, Query Invalidation.
import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider,
} from '@tanstack/react-query' // Create a client
const queryClient = new QueryClient() function App() { return (
// Provide the client to your App <QueryClientProvider client={queryClient}> <Todos /> </QueryClientProvider> )
}
render(<App />, document.getElementById('root'))
import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider,
} from '@tanstack/react-query'
import { getTodos, postTodo } from '../my-api' function Todos() {
// Truy cập vào client const queryClient = useQueryClient() // Query: Tạo query dùng để call API getTodos với key là "todos" const query = useQuery({ queryKey: ['todos'], queryFn: getTodos }) // Mutations: Tạo mutation dùng để call API postTodo const mutation = useMutation({ mutationFn: postTodo, onSuccess: () => {
// Invalidate and refetch: Sau khi call API postTodo thành công sẽ tiến hành invalidate data để thực hiện gọi lại API getTodos để refetch dữ liệu mới.
// Lưu ý: React-query sử dụng queryKey để biết cần phải invalidate query nào queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) return ( <div> <ul> {query.data?.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> <button onClick={() => { mutation.mutate({ id: Date.now(), title: 'Do Laundry', }) }} > Add Todo </button> </div> )
}
Queries
Queries được sử dụng để fetch data từ server, ví dụ như method GET.
Ví dụ:
import { useQuery } from '@tanstack/react-query'
function App() { const { isFetching, isPending, isError, isRefetching, isSuccess, error, data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
}
Trong đó, cần truyền vào 2 giá trị đó là:
- queryKey: Định nghĩa một unique key cho query, có thể truyền một hoặc nhiều key. Key này được dùng để refetch data, caching, chia sẽ query cho toàn bộ ứng dụng
- queryFn: Là hàm gọi API.
Gía trị trả về của query bao gồm:
isPending
: Query chưa có dữ liệuisError
: Query nhận về lỗiisSuccess
: Query thành công và dữ liệu đã được sẵn sàngerror
- Chứa lỗi.data
- Chứa dữ liệu.isFetching
- Trạng thái đang gọi api.
Lưu ý*: Nếu bạn có thắc mắc khi nào nên sử dụng isPending
, isFetching
, isRefetching
, mình sẽ giải thích ở cuối bài viết.
Ví dụ về việc sử dụng các giá trị trên:
function Todos() { const { isPending, isError, data, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList, }) if (isPending || isFetching) { return <span>Loading...</span> } if (isError) { return <span>Error: {error.message}</span> }
// Có thể hiểu rằng tại thời điểm này isSuccess = true nên không cần kiểm tra điều kiện return ( <ul> {data.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> )
}
Query Key
Query Key là một chuỗi các giá trị dùng để định danh mỗi query dữ liệu. Nó giúp React Query tìm và quản lý các yêu cầu trong Query Cache.
Hãy đảm bảo việc truyền key hợp lý cho từng query để việc caching data hoạt động đúng. Đối với React-Query việc caching dựa vào queryKey, giả sử gọi useQuery có query key giống nhau ở 2 nơi khác nhau thì chỉ call API 1 lần, vì data đã được caching.
Lưu ý: Để useQuery thực thi lại hàm queryFn với params mới nhất thì cần truyền giá trị vào cả queryKey và queryFn.
Ví dụ:
const [page, setPage] = useState(0) const fetchProjects = (page = 0) => fetch('/api/projects?page=' + page).then((res) => res.json()) const { error, data, isFetching, } = useQuery({ // useQuery sẽ thực hiện gọi API mỗi lần page thay đổi giá trị queryKey: ['projects', page], queryFn: () => fetchProjects(page), })
Dependent Queries
Đặt điều kiện cho query, khi nào thỏa mãn điều kiện thì query mới được thực thi. Thường được sử dụng trong trường hợp phụ thuộc vào query trước đó phải hoàn thành trước rồi mới thực hiện.
Sử dụng tính năng này bằng cách thêm key enabled
Ví dụ:
// Tạo query để get user bằng email
const { data: user } = useQuery({ queryKey: ['user', email], queryFn: getUserByEmail,
}) const userId = user?.id
// Sau đó lấy tất cả projects của user đó bằng userId const { status, fetchStatus, data: projects,
} = useQuery({ queryKey: ['projects', userId], queryFn: getProjectsByUser,
// Query sẽ không thực hiện nếu không có userId enabled: !!userId,
})
Mutations
Không giống như useQuery, mutations thường được sử dụng cho các method POST/PUT/PATCH/DELETE.
Ví dụ:
function App() {
// Khởi tạo mutation bằng useMutation const mutation = useMutation({ mutationFn: (newTodo) => { return axios.post('/todos', newTodo) }, }) return ( <div> {mutation.isPending ? ( 'Adding todo...' ) : ( ****<> {mutation.isError ? ( <div>An error occurred: {mutation.error.message}</div> ) : null} {mutation.isSuccess ? <div>Todo added!</div> : null} <button onClick={() => { // Gọi mutationFn với các tham số như id, title mutation.mutate({ id: new Date(), title: 'Do Laundry' }) }} > Create Todo </button> </> )} </div> )
}
Một vài giá trị mà useMutation trả về:
isIdle
orstatus === 'idle'
- mutation đang rảnh rỗi không làm gìisPending
orstatus === 'pending'
- mutation đang chạyisError
orstatus === 'error'
- mutation chạy xong và gặp lỗiisSuccess
orstatus === 'success'
- mutation chạy xong và thành côngerror
- lấy ra lỗi khi mutation lỗidata
- lấy ra data khi mutation thành côngmutate
- là một hàm đại diện cho mutationFn, khi gọi hàm mutate thì hàm mutationFn sẽ được thực thireset
- là một hàm dùng đễ clear data hoặc error
Mutation Side Effect
Chúng ta có thể thực thi một đoạn code vào các giai đoạn vòng đời mutations để thực hiện các tác vụ khác nhau bằng các tham số truyền vào useMutation như: onMutate, onError, onSuccess, onSettled.
useMutation({ mutationFn: addTodo, onMutate: (variables) => { // Khi mutationFn bắt đầu được thực thi }, onError: (error, variables, context) => { // Khi thực thi mutationFn xảy ra lỗi }, onSuccess: (data, variables, context) => { // Khi thực thi mutationFn thành công }, onSettled: (data, error, variables, context) => { // Khi thực thi mutationFn thành công hoặc lỗi },
})
Cơ chế Caching trong React-Query
Dưới đây là ví dụ cơ bản về cơ chế caching:
Giả sử chúng ta đang sử dụng cacheTime mặc định là 5 phút và staleTime mặc định là 0.
Một phiên bản đầu tiên của useQuery({ queryKey: ['todos'], queryFn:fetchTodos })
được khởi tạo ở component A.
- Vì không có truy vấn nào khác được thực hiện bằng khóa truy vấn
['todos']
nên truy vấn này sẽ thực thi. - Khi call API xong, dữ liệu trả về sẽ được lưu vào bộ đệm có key là
['todos']
.
Phiên bản thứ hai của useQuery({ queryKey: ['todos'], queryFn:fetchTodos })
được khởi tạo ở component B.
- Vì bộ đệm đã có dữ liệu cho key
['todos']
từ truy vấn ở component A nên dữ liệu đó sẽ được trả về ngay lập tức từ bộ đệm mà không cần gọi lại API. - Lưu ý rằng trạng thái của cả hai truy vấn ở component A và B đều được cập nhật (bao gồm isFetching, isLoading và các giá trị liên quan khác) vì chúng có cùng key truy vấn.
- Khi có yêu cầu gọi API mới, dữ liệu của bộ đệm với khóa
['todos']
sẽ được cập nhật dữ liệu mới và cả hai phiên bản component A,B đều được cập nhật dữ liệu mới.
Một số trường hợp sử dụng react-query trong thực tế
isFetching Global
const isFetching = useIsFetching()
: Lấy trạng thái isFetching của bất kỳ query nào đang thực hiện fetch data trong ứng dụng, isFetching
trả về số lượng query đang fetching ( trả về số thay vì true/false )
Refetch data khi focus vào Window
// Global
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false,// default: true }, },
})
function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
} useQuery({ queryKey: ['todos'], queryFn: fetchTodos, refetchOnWindowFocus: false,
})
Can thiệp vào việc khi nào useQuery được phép thực thi bằng cách sử dụng enable
function Todos() { const [start, setStart] = useState(false) const { data } = useQuery({ queryKey: ['todos', filter], queryFn: () => fetchTodos(filter), // Sẽ không thực hiện query cho đến khi start = true enabled: start }) return ( <div> <button onClick={() => setStart(true)}>Start Query</button> {data && <TodosTable data={data}} /> </div> )
}
Retry query khi xảy ra lỗi
Ví dụ: Thực hiện lại 10 lần mỗi lần delay 1s nếu query bị lỗi, nếu sau 10 lần vẫn còn lỗi thì sẽ dừng và trả về kết quả lỗi
import { useQuery } from '@tanstack/react-query'
const result = useQuery({ queryKey: ['todos', 1], queryFn: fetchTodoListPage, retry: 10,// Query sẽ retry 10 lần, sau đó ở lần 11 vẫn bị lỗi thì query sẽ trả về kết quả lỗi retryDelay: 1000, // Thời gian delay mỗi lần retry
})
Một số cách Invalidation query
Sau khi thực hiện Invalidation thì những query này sẽ bị đánh dấu là cũ, khi component chứa query đó đc mount thì query sẽ thực hiện refetch để cập nhật data mới.
// Invalidate tất cả query có trong cache
queryClient.invalidateQueries() // Invalidate mỗi query có key bắt đầu bằng todos
queryClient.invalidateQueries({ queryKey: ['todos'] }) // Reset mỗi query có key bắt đầu bằng todos
queryClient.resetQueries({ queryKey: ['todos'] }) // Invalidate sau 5s
cacheTime: 5000
Làm mới dữ liệu sau khi tạo record mới
import { useMutation, useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
// Sau khi addTodo thành công sẽ tiến hành invalidate query có key là "todos" và "reminders" để làm mới dữ liệu
const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.invalidateQueries({ queryKey: ['reminders'] }) },
})
Get data và Set data của useQuery
Get dùng để lấy ra data dựa vào queryKey.
Set được sử dụng để cập nhập lại data query sau khi thực hiện chỉnh sửa record mà không cần phải thực thi lại useQuery.
// Set data vào query có key là todo
queryClient.setQueryData(['todo'], data) // Get data của query có key là todo
queryClient.getQueryData(['todo'])
staleTime
Được truyền vào useQuery để xác định thời gian khi nào data trở thành data cũ, nếu data được đánh dấu là cũ thì khi component chứa useQuery đc mount thì useQuery sẽ thực hiện refetch để cập nhật data mới.
cacheTime
Là thời gian React Query giữ lại dữ liệu trong cache sau khi không còn component nào dùng query đó nữa. Sau thời gian này, toàn bộ data của query đó sẽ bị xóa khỏi bộ nhớ.
Sử dụng các trạng thái loading như thế nào?
Đối với useQuery:
useQuery có 2 khái niệm về loading như sau:
- Loading lần đầu - query chưa có trong cache, thực hiện call lần đầu.
- Loading nền - query đã có trong cache, thực hiện revalidate data.
Phân biệt isLoading, isFetching, isRefetching:
isLoading:
Loading lần đầu
isFetching:
Loading lần đầu || Loading nền
isRefetching:
Loading nền
Đối với useMutation:
isLoading
⇒ chỉ có một trạng thái loading nên không cần phân biệt