+11

react-query là gì? Tại sao nên sử dụng react-query?

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 useEffectuseState 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à onSuccessonError 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.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí