Có thể các bạn đã nghe hay thấy người khác nhắc đến thư viện react-query nhiều rồi. Tại sao phải dùng react-query khi ta có thể sử dụng useEffect để fetch data từ api? Bài viết này sẽ giải đáp thắc mắc đó.
react-query là gì?
Theo README của react-query
Hooks for fetching, caching and updating asynchronous data in React
Đơn giản thì react-query là thư viện để fetching, caching và updating dữ liệu bất đồng bộ của React.
Tại sao phải dùng react-query?
Trước đây, để fetch dữ liệu từ server thì có nhiều cách. Một số cách phổ biến là sử dụng useEffect
và useState
hay sử dụng thư viện quản lí state (Redux).
const Component = () => { const [data, setData] = useState(null); useEffect(() => { const getData = async () => { const response = await fetch("https://example.com/api").then(response => response.json); setData(response) } getData() }) return ( // Some JSX here )
}
Cách này hoạt động ổn, tuy nhiên lại khá là rườm rà. Phải viết 1 đống code chỉ để lấy dữ liệu từ server và hiện ra cho người dùng.
Sử dụng react-query
react-query cung cấp nhiều hooks với API cực kì đơn giản để fetch cũng như update dữ liệu từ server. Hỗ trợ sẵn caching, refetching và nhiều thứ khác.
Đầu tiên ta sẽ tìm hiểu cách để fetch dữ liệu sử dụnguseQuery
useQuery
Để sử dụng hook useQuery
, ta phải truyền ít nhất 2 tham số:
- Tham số đầu tiên là queryKey
- Tham số thứ 2 là hàm trả về 1 promise:
- Resolve data, hoặc
- Throw error
- Tham số thứ 3 là các options (ta sẽ tìm hiểu ở bên dưới).
queryKey được sử dụng để refetching, caching và chia sẻ dữ liệu giữa các component với nhau. (Ta sẽ tìm hiểu thêm với 1 số ví dụ ở bên dưới)
Sau đây là ví dụ sử dụng useQuery
cơ bản.
const Component = () => { const { data, isLoading, isError } = await useQuery('todo', () => fetch('https://example/api/todo').then(response => response.json())) if (isLoading) { return ( // Loading JSX ) } if (isError) { return ( // Error JSX ) } return ( // Some JSX here )
}
useQuery
trả về những thông tin cần thiết như data
, isLoading
hay isError
. (Bạn có thể xem thêm một số thông tin khác ở đây)
useMutation
useMutation
khác với useQuery
, được sử dụng để tạo/thay đổi/xóa dữ liệu ở. (ví dụ như đăng ký, đăng nhập).
Để sử dụng hook useMutation
, ta phải truyền ít nhất 1 tham số:
- Tham số thứ nhất là hàm trả về 1 promise:
- Resolve data, hoặc
- Throw error
- Tham số thứ 2 là các options (ta sẽ tìm hiểu sau).
function App() { const mutation = useMutation(newTodo => { return axios.post('/todos', newTodo) }) return ( <div> {mutation.isLoading ? ( 'Adding todo...' ) : ( <> {mutation.isError ? ( <div>An error occurred: {mutation.error.message}</div> ) : null} {mutation.isSuccess ? <div>Todo added!</div> : null} <button onClick={() => { mutation.mutate({ id: new Date(), title: 'Do Laundry' }) }} > Create Todo </button> </> )} </div> ) }
Cũng như useQuery
, useMutation
trả về những thông tin cần thiết như isLoading
hay isError
. (Bạn có thể xem thêm một số thông tin khác ở đây).
Tuy nhiên, useMutation
lại trả về mutate
thay vì data
(như ví dụ trên). Khi gọi hàm mutate
thì hàm số đầu tiên của useMutation
sẽ được gọi.
Một số ví dụ thực tế
Prefetch dữ liệu.
Mình gặp trường hợp này ở một dự án web phim, mình muốn khi user fetch dữ liệu của tập này, thì sẽ đồng thời fetch dữ liệu của tập tiếp theo, để khi nếu user bấm sang tập tiếp theo thì không phải đợi server trả về kết quả, từ đó tăng trải nghiệm người dùng lên đáng kể.
Đây là cách mình làm điều đó với react-query
.
Đầu tiên, mình có một custom hook là useVideoSources
, hook này sẽ trả về stream của video dựa vào ID của tập phim, tham số nhận vào sẽ là currentEpisode
(tập hiện tại) và nextEpisode
(tập tiếp theo).
const useVideoSources = (currentEpisode, nextEpisode) => { const queryClient = useQueryClient(); const getQueryKey = (episode) => `episode-${episode.id}`; return useQuery( getQueryKey(currentEpisode), () => fetchVideoSources(currentEpisode), { onSuccess: () => { queryClient.prefetchQuery(getQueryKey(nextEpisode), () => fetchSource(nextEpisode) ); }, onError: (error) => { toast.error(error.message); }, } );
};
Ở đây bạn có thể thấy mình có truyền một object có option là onSuccess
và onError
vào tham số thứ 3 (các options khác)
- Option
onSuccess
sẽ chạy khi query thực thi thành công, mình sẽ dùng method queryClient.prefetchQuery (useQueryClient) để prefetch trước dữ liệu của tập tiếp theo. Nếu người dùng chuyển sang tập tiếp theo thì sẽ hiện dữ liệu ngay lập tức mà không cần đợi, vì đã được fetch từ trước. - Option
onError
sẽ chạy nếu query xảy ra lỗi, mình sẽ hiện toast cho người dùng biết.
Optimistic UI updates
Một trường hợp khác cũng là của web phim ở phần bình luận, khi người dùng bấm like vào một bình luận nào đó, thay vì mình đợi kết quả rồi mới hiện đã like, thì mình sẽ hiện đã like trước khi kết quả từ server được trả về.
Tìm hiểu thêm về optimistic UI updates
const useCreateReaction = () => { const queryClient = useQueryClient(); return useMutation( async ({ commentId }) => { if (!user) throw new Error("Please login to react"); const { error, data } = await likeComment(commentId) if (error) throw error; return data; }, { onMutate: ({ commentId }) => { // Set dữ liệu của comment. queryClient.setQueryData( ["comment", commentId], (comment) => { // Gán isLiked của comment thành true comment.isLiked = true; return comment; } ); }, onSettled: (_, __, params) => { queryClient.invalidateQueries(["comment", params.commentId]); }, onError: (error) => { toast.error(error.message); }, } );
};
Ở hook trên, ta sẽ sử dụng hook useMutation
để xử lí cho trường hợp này.
- Option
useMutate
sẽ được chạy trước khi mutation success hoặc error. Lúc này, ta sẽ sử dụng method queryClient.setQueryData để thay đổi dữ liệu của bình luận, đổi property isLiked của bình luận thành true. Những component đang sử dụng dữ liệu của bình luận này (useQuery) sẽ được re-render vì dữ liệu của query này đã thay đổi. - Option
useSettled
sẽ được chạy sau khi mutation thực thi xong (kể cả success hay error). Lúc này ta sẽ thực hiện invalidate lại dữ liệu của bình luận, vì khi ta thực hiện mutation sẽ có khả năng gặp lỗi nên việc invalidate sẽ nhằm chắc chắn dữ liệu đang được hiển thị là chính xác. - Option
useError
sẽ chạy khi mutation gặp lỗi. Ta sẽ hiện 1 toast để cho người dùng biết.
Kết
react-query là một thư viện rất đáng để thử, còn rất nhiều tính năng hay của react-query mà mình chưa đề cập tới, bạn có thể xem docs của react-query ở đây:
Qua đó là 1 số chia sẻ về react-query của mình, vì mình chỉ tự học thôi nên có thể bài viết này có thể có nhiều chỗ chưa chính xác, mong mọi người góp ý ở dưới phần bình luận.