PostDetail.jsx
const { data, isLoading, isError, error } = useQuery('comments', () => fetchComments(post.id) );
👉🏽 (post.id
에 따라) 각각 다른 데이터(query)를 받아오지만 같은 query key(comments
)를 사용하고 있기 때문
👉🏽 이미 알려진(만들어진?)(known keys) 키의 쿼리 데이터는 특정한 트리거가 있어야 refetch된다.
트리거의 예
- component remount
- window refocus
- running refetch function
- automated refetch (지정된 간격으로 refetch 자동 실행)
- query invalidation after a mutation
PostDetail.jsx
const { data, isLoading, isError, error } = useQuery(['comments', post.id], () => fetchComments(post.id) );
post.id
가 업데이트되면) 새로운 쿼리를 만든다.👉🏽 쿼리 함수에 있는 값(데이터를 구별할 때 쓰이는 값. 여기서는 post.id)이 쿼리 키(배열)에 포함돼야 한다!
inactive
상태가 된다.Post.jsx
... const maxPostPage = 10; // 최대 10페이지로 임의로 설정해 줌 (json placeholder가 제공하는 api에 limit가 10으로 설정되어 있음..) // 페이지에 따라 받아오는 데이터가 다른 쿼리 함수 async function fetchPosts(pageNum) { // 매개변수로 pageNum const response = await fetch( `https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${pageNum}` // pageNum에 따라 다른 api 요청 ); return response.json(); } export function Posts() { const [currentPage, setCurrentPage] = useState(1); // 현재 페이지 state 만들기. 첫 페이지를 1페이지로 설정 const [selectedPost, setSelectedPost] = useState(null); const { data, isError, error, isLoading } = useQuery( ['posts', currentPage], // currentPage를 넣어서 쿼리키를 의존성 배열처럼 만든다 () => fetchPosts(currentPage), // 인자를 가지는 쿼리함수 { staleTime: 2000, } );
Next Page
, Previous Page
버튼 클릭currentPage
state를 업데이트Posts.jsx
... <button disabled={currentPage <= 1} // 현재 페이지가 1 이하면 비활성화 onClick={() => { setCurrentPage((previousValue) => previousValue - 1); // 클릭하면 현재페이지 -1 }}>Previous page</button> <span>Page {currentPage}</span> <button disabled={currentPage >= maxPostPage} onClick={() => { setCurrentPage((previousValue) => previousValue + 1); // 클릭하면 현재페이지 +1 }}>Next page</button>
- 다음 페이지가 캐시에 없기 때문에
Next Page
버튼을 누를 때마다 로딩 인디케이터가 보이는 현상...
👉🏽 Pre-fetching으로 (데이터를 미리 가져와서 캐시에 넣어서) 이러한 현상을 없애줄 수 있다.
pre-fetch의 목적
- 일단 캐시된 데이터를 표시해 주면서
- 백그라운드에서 데이터의 업데이트 여부를 조용히 서버에서 확인하는 것
- 만약 데이터가 업데이트 됐을 경우 해당 데이터를 페이지에 보여줌.
QQQ) 그럼 두 번 통신해서 비효율적인 것 아닌지.
stale
상태가 됨 (설정할 수 있지만 stale
이 기본값) (inactive..)stale
상태의 데이터를 보여줌 (캐시가 만료되지 않았다는 가정 하에! 만약 사용자가 cacheTime보다 오래 페이지에 머물렀다면 캐시가 없기 때문에 다시 로딩 인디케이터가 나타남)Post.jsx
Next Page
를 prefetchingimport { useQuery, useQueryClient } from 'react-query'; // useQueryClient 불러오기 ... export function Posts() { ... const queryClient = useQueryClient(); // queryClient 사용 // currentPage가 바뀔 때마다 pre-fetching 하기 (useEffect와 의존성 배열 사용) useEffect(() => { if (currentPage < maxPostPage) { // Next Page 클릭에 대한 prefetching을 만들 것이므로 const nextPage = currentPage + 1; queryClient.prefetchQuery(['posts', nextPage], () => fetchPosts(nextPage) // 해당 포스트(다음 페이지)를 fetch ); } }, [currentPage, queryClient]);
참고 :
Previous Page
의 데이터를 캐시에 유지하기
keepPreviousData: true
: 쿼리 키가 변경되어서 새로운 데이터를 fetching 하는 동안에도 마지막으로 fetch 되었던 데이터 값을 유지한다. (이전 페이지로 이동했을 때 해당 데이터가 캐시에 있도록)
Post.jsx
export function Posts() { const { data, isError, error, isLoading } = useQuery( ['posts', currentPage], () => fetchPosts(currentPage), { staleTime: 2000, keepPreviousData: true, // Previous Page 데이터 유지하기 } );
isFetching
: 데이터 가져오는 중 (쿼리 함수 완료 전) (데이터 존재 여부 상관 X)isLoading
: isFetching + 캐시된 쿼리 데이터 없음 (데이터 새로 가져오는 중)isLoading
사용 (데이터 없어서 새로 가져올 때만 보여줄 용도로 사용되므로)🐥 (어떤 상태이든) 캐시가 있다는 것...
=> fetching 중일 때 보여줄 수 있는 데이터가 있다는 것 (fetching은 다시 해야 함)
참고 : 강의에서 사용하는 jsonplaceholder는 실제 서버 데이터를 변경할 수는 없음. mutation 요청 보내는 건 가능하지만...
- mutate 함수를 리턴 (..?) (객체의 속성 함수로...)
- query key가 필요 없음 (데이터를 저장하지 않으므로)
isLoading
은 있지만isFetching
은 없음 (캐시되는 항목이 없으므로)- retry(재시도) 기본값 없음. (자동 재시도를 적용하고 싶다면 설정은 가능)
useMutation과 useQuery의 차이점
- useQuery의 queryFn은 매개변수 가질 수 없지만
const { data, isLoading, isError, error } = useQuery( ['comments', post.id], () => fetchComments(post.id) // 쿼리함수 (매개변수 x) );
- useMutation의 mutationFn은 매개변수를 가질 수 있다.
const deleteMutation = useMutation((postId) => deletePost(postId)); // 변이함수 (매개변수 O)
post.id
)로 넣으면 <button onClick={() => deleteMutation.mutate(post.id)}> Delete </button>
postId
)로 전달된다const deleteMutation = useMutation((postId) => deletePost(postId));
PostDetail.jsx
- Delete 버튼 누르면 삭제
import { useQuery, useMutation } from 'react-query'; // 삭제 mutationFn async function deletePost(postId) { const response = await fetch( `https://jsonplaceholder.typicode.com/postId/${postId}`, { method: 'DELETE' } ); return response.json(); } const deleteMutation = useMutation((postId) => deletePost(postId)); return ( <> ... <button onClick={() => deleteMutation.mutate(post.id)}>Delete</button> // mutate의 인자로 삭제할 post id를 전달 {deleteMutation.isError && ( <p style={{ color: 'red' }}>Error deleting the post</p> )} {deleteMutation.isLoading && ( <p style={{ color: 'purple' }}>Deleting the post...</p> )} {deleteMutation.isSuccess && ( <p style={{ color: 'green' }}>Post has been deleted</p> )} ...
PostDetail.jsx
return ( <> <h3 style={{ color: 'blue' }}>{post.title}</h3> <button onClick={() => deleteMutation.mutate(post.id)}>Delete</button> {deleteMutation.isError && ( // 에러 발생 시 <p style={{ color: 'red' }}>Error deleting the post</p> )} {deleteMutation.isLoading && ( // 로딩 시 <p style={{ color: 'purple' }}>Deleting the post...</p> )} {deleteMutation.isSuccess && ( // 요청 성공 했을 시 <p style={{ color: 'green' }}>Post has been deleted</p> )} <button>Update title</button> <p>{post.body}</p> <h4>Comments</h4> {data.map((comment) => ( <li key={comment.id}> {comment.email}: {comment.body} </li> ))} </> );
useQuery: 서버에서 데이터를 가져오고 최신 상태인지 확인하는 훅
staleTime : 데이터가 사용 가능한 상태로 유지되는 시간 (특정 트리거에 의해 re-fetch 되어 시작됨)
cacheTime : 데이터가 비활성화 된 후 남아있는 시간
쿼리 키가 변경되면 useQuery hook은 쿼리를 다시 실행함 (re-fetch)
( => 데이터 함수(쿼리 함수?)가 바뀌면 쿼리 키도 바뀜(바뀌어야 함). 데이터가 바뀌면 다시 실행될 수 있도록)