const 반환 = useQuery<데이터타입>(옵션)
enabled: boolean | (query: Query) => boolean
Set this to false to disable this query from automatically running.
Dependent Query로 사용 될 수 있다. => userId값이 있으면 쿼리를 가져오고, 아니면 쿼리가 실행되지 않는다.
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
쿼리에서 가져온 데이터를 가공해서 사용 가능하다.
queryFn에서도 가공할 수 있지만, api 타입이 맞지 않는경우 존재 가능 => 3번째 제너릭으로 select의 type을 선언 가능하다.
select에서 사용해야 memoization이 된다.
(즉 데이터가 갱신되는 상황에서 데이터가 동일한 상황일때, queryFn 에서는 가공로직을 돌지만, select로 처리하게 되면 가공로직을 돌지 않는다.)
const { data } = useQuery<Users, Error, string[]>({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('https://api.heropy.dev/v0/users')
const { users } = await res.json()
return users
},
staleTime: 1000 * 10,
select: data => data.map(user => user.name)
})
데이터 자동 갱신(다시 가져오기)의 시간 간격(ms), queryInstance가 다시 mount된 상태가 아니라도 서버로 데이터를 호출해 가져온다.
stale여부와 상관없이 시간에 따라 가져온다.
함수형태로 작성해 특정조건에서 interval을 지정가능하다.
background에서 다시 페이지로 돌아왔을때 => 데이터를 갱신함.
stale 여부와 상관없이 시간에 따라 가져온다.
useQuery 연결 시 데이터 갱신 여부.
true: 연결 시 데이터가 상한 경우만 갱신.
always: 연결 시 데이터 항상 갱신.
네트워크 재 연결시 데이터 갱신 여부 => 서버로부터 다시 가져온다.
true 이면 만약 쿼리가 stale상태라면, 네트워크가 재연결되었을 때 다시 가져온다.
브라우저 화면 포커스시 데이터 갱신 여부
true 이면 만약 쿼리가 stale상태라면, window가 refetch 되었을때 다시 가져온다.
데이터를 가져오는 중을 나타냄 (쿼리가 처음에 가져오든, 수동으로 가져오든 데이터를 가져오는 중간에 바뀌는 값)
isLoading은 isFetching && isPending과 동일, 첫번째 가져오기가 진행중임을 나타내는 값임.
refetch : 강제로 쿼리를 가져오게함. => 동일한 queryKey로 stale여부와 상관없이 데이터를 서버로부터 갱신한다.
QueryClient.getQueryData : QueryCache에서 무조건 cache된 데이터를 가져온다.
queryClient.fetchQuery() : 두 함수의 절충안, 데이터가 상했다면 서버에서 재호출하고, 상하지 않았다면 queryClient의 queryCache로부터 데이터를 가져온다.
getQueryData와 다르게 queryOptions을 같이 넘겨줘야한다.(만약 stale인 상태라면 => 서버에서 가져와 다시 캐싱을 하기 때문)
그렇기 때문에, queryKey가 동일한 useQuery와 fetchQuery의 Option을 동기하화는게 필요하다.(staleTime을 동일하게!)
const options = queryOptions<ResponseValue>({
queryKey: ['delay'],
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
staleTime: 1000 * 10
}) // option을 재사용하자!
const queryData = useCallback(async () => {
const data = await queryClient.fetchQuery(options)
console.log(data) // 캐시된 데이터 or 새로 가져온 데이터
}, [queryClient])
queryKey는 배열이여야 한다, 쿼리를 식별하는 고유한 값.
다중아이템 쿼리키를 사용할 때는, 아이템의 순서가 중요하다.
기본적으로 queryFn에서 사용하는 변수는 쿼리키에 포함되어야 한다 => 쿼리키가 변하면 서버에서 데이터를 갱신 할 수 있기 때문
내부에서는 queryKey가 직렬화되어 관리되기 때문에 => 아래 서로같은 쿼리에서 undefined부분은 제거되어 같게 취급된다.
하지만, queryKey는 array이기 때문에 queryKey의 순서가 중요하다.
// 단일 아이템 쿼리 키
useQuery({ queryKey: ['hello'] })
// 다중 아이템 쿼리 키
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
// 서로 같은 쿼리
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
useQuery({ queryKey: ['hello', 'world', 123, { b: 2, c: undefined, a: 1 }] })
// 서로 다른 쿼리
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2, c: 3 }] })
useQuery({ queryKey: ['hello', 'world'] })
useQuery({ queryKey: [123, 'world', { a: 1, b: 2, c: 3 }], 'hello' })
useQuery를 통해 데이터를 가져오면 => queryKey값을 기반으로 QueryClient에 저장
staleTime은 리엑트 쿼리애개 캐시된 데이터를 얼마나 자주 최신화 시켜줘야 하는지 알려줌.
cacheTime이 지났다면 GC에 의해 Query Manager에서 사라진다.
useQuery, useInfiniteQuery로 만들어져 사용된 인스턴스들은 다른 컴포넌트에서 사용될 때를 대비해 캐시되며, 5분 후에 Garbage Collection (이하 GC)에 의해 사라진다. 기본값은 5분, 5버전부터는 gcTime으로 변경됨.
refetching이 일어나는 특정조건은 다음과 같다. => query가 stale상태로 변했다고 바로 서버에서 데이터를 갱신하는게 아니다!!
새로운 Query Instance가 마운트 될 때 (페이지 이동 후 등의 상황)
브라우저 화면을 다시 focus 할 때
인터넷이 재연결되었을 때
refetchInterval이 설정되어있을 때
{
['todos', 'list', { filters: 'all' }],
['todos', 'list', { filters: 'done' }],
['todos', 'detail', 1],
['todos', 'detail', 2],
}
아래 코드를 실행하면 => ['todo', 'list'] 에 해당하는 모든 하위 쿼리들이 무효화된다.
removeQueries도 마찬가지. 만약 정확하게 동일한 key를 가진 query만 무효화 하고 싶다면 exact 옵션을 사용 할 수 있다.
queryClient.invalidateQueries(['todos', 'list'])
queryClient.removeQueries({ queryKey, exact: true })
https://tanstack.com/query/latest/docs/reference/QueryClient/#queryclientinvalidatequeries
await queryClient.invalidateQueries(
{
queryKey: ['posts'],
exact,
refetchType: 'active',
},
{ throwOnError, cancelRefetch },
)
invalidateQueries은 3번째 옵션으로 refetchType이 존재한다. 기본값은 active인데, queryKey와 matching 되는 active한 쿼리를 무효화하고 다시 가져온다.
queryKey와 일치해도 상태가 active가 아니라면, 무효화시키지 않는다. 이때는 refetchType을 따로 설정해줘야한다.
const 반환 = useInfiniteQuery<페이지타입>(옵션)
const {
data, // 가져온 데이터
isLoading, // 첫 페이지 가져오는 중
isFetching, // 다음 페이지 가져오는 중
isFetched, // 첫 페이지 가져오기 완료
hasNextPage, // 다음 페이지가 있는지 여부
fetchPreviousPage, // 이전 페이지 가져오기 함수
fetchNextPage // 다음 페이지 가져오기 함수
} = useInfiniteQuery<Page>({
queryKey: ['movies', queryText], // 검색어로 쿼리 키 생성!
queryFn: async ({ pageParam }) => {
const res = await fetch(
`https://omdbapi.com/?apikey=7035c60c&s=${queryText}&page=${pageParam}`
)
return res.json()
},
initialPageParam: 1, // 첫 페이지 번호 초기화!
//처음 pageParams로 들어간다.
getNextPageParam: (lastPage, pages) => {
// 한 페이지당 최대 10개까지의 영화 정보를 가져옴!
// 마지막 페이지 번호 계산!
const maxPage = Math.ceil(Number.parseInt(pages[0].totalResults, 10) / 10)
// maxPage를 계산함
// 다음 페이지가 있으면, 다음 페이지 번호 반환! => 계속 호출가능
// hasNextPage값이 true
if (lastPage.Response === 'True' && pages.length < maxPage) {
return pages.length + 1
}
// 다음 페이지가 없으면 undefined | null 반환! => 호출 종료
// hasNextPage값이 false
return undefined
},
//lastPage => 페이지의 데이터
enabled: false, // 검색어 입력 전까지 대기!
staleTime: 1000 * 60 * 5 // 5분
})
useEffect(() => {
if (queryText) fetchPreviousPage()
}, [queryText, fetchPreviousPage])
// 검색어가 변경될 때마다, 캐시된 데이터가 있어서
//그 데이터의 다음 페이지를 가져오지 않도록 이미 캐시된 이전 페이지를 가져옴!
/// 만약 query가 fresh라면, 캐시된 데이터가 있는 경우 => 이전에 가져왔던 데이터(이전페이지)를 가져온다.
...
{isFetched && hasNextPage && (
<button
disabled={isFetching}
onClick={() => fetchNextPage()}>
{isFetching ? '로딩 중..' : '더 보기!'}
</button>
)}
//아래에서 버튼을 누르면 => 다음 페이지를 가져온다.
새로운 다음 페이지를 가져오면, 다음 페이지의 정보로 호출되는 함수.(필수 옵션!)
다음 페이지 번호를 반환해야 함, 다음 페이지가 없으면, undefined 또는 null을 반환해야 함!
(lastPage: TPage, allPages: TPage[], lastPageParam: number, allPageParams: number[]) => TPageParam | undefined | null
새로운 이전 페이지를 가져오면, 이전 페이지의 정보로 호출되는 함수.
이전 페이지 번호를 반환해야 함! 이전 페이지가 없으면, undefined 또는 null을 반환해야 함!
(firstPage: TPage, allPages: TPage[], firstPageParam: number, allPageParams: number[]) => TPageParam | undefined | null
(options?: FetchNextPageOptions) => Promise<UseInfiniteQueryResult>
(options?: FetchPreviousPageOptions) => Promise<UseInfiniteQueryResult>
hasNextPage : 다음페이지가 있는지의 여부
hasPreviousPage : 이전페이지가 있는지의 여부
isFetchingNextPage : 다음 페이지를 가져오는 중인지의 여부.
isFetchingPreviousPage : 이전 페이지를 가져오는 중인지의 여부.
웹 페이지에서 DOM 요소의 가시성 및 위치를 감시하고, 요소가 화면에 들어오거나 나갈 때 이벤트를 트리거하는 JavaScript API
Scroll event는 동기적으로 실행되기 때문에 메인스레드에 영향을 줌. 또한 여러 scroll event가 등록되어있을경우 이벤트가 중첩되어 호출되는 현상이 발생가능
Intersection Observer api는 비동기적으로 실행되기 때문에 메인스레드에 영향을 주지 않으면서 변경사항 관찰 가능함.
무한스크롤이나, 페이지 스크롤시 이미지를 Lazy Loading 하는데 사용가능함.
Intersection Observer 객체 생성
IntersectionObserverCallback: IntersectionObserverCallback은 관찰된 요소의 상태가 변경될 때 호출되는 콜백 함수입니다. 이 콜백 함수는 두 개의 매개변수를 받습니다
entries 및 observer.
entries는 IntersectionObserverEntry 객체의 리스트임.
observer는 생성한 인스턴스를 참조함.
const observer = new IntersectionObserver(callback, options);
function callback(entries, observer) {
entries.forEach((entry) => {
// entry.intersectionRatio를 통해 가시성 정보 확인
if (entry.isIntersecting) {
// 요소가 화면에 들어옴
} else {
// 요소가 화면을 벗어남
}
});
}
root : 교차영역의 기준이 될 root 엘리먼트. observe의 대상으로 등록할 엘리먼트는 반드시 root의 하위요소여야만함. (default : null, 브라우저 viewport)
rootMargin : default : '0px 0px 0px 0px' => root element마진값.
threshold : 0 ~ 1 사이의 숫자혹은 이 숫자들의 배열. 타깃 엘리먼트에 대한 교차비율을 의미함. 0이라면, 타깃 엘리먼트가 교차영역에 진입했을때 observer를 실행, 1이라면 타깃 전체가 들어왔을 때 observer를 실행
observe : 타겟에 대한 관찰을 시작할 때 사용함
unobserve : 타깃에 대한 관찰을 멈출 때 사용함.
disconnect : 전체 다수 엘리먼트에 대한 관찰을 멈출때 사용
takeRecords : IntersectionObserverEntry 배열반환.
콜백함수에서 isIntersecting을 감지 후, fetchNextPage 함수를 통해 직접 api를 호출하는 방식으로 무한스크롤을 구현 가능하다.
스크롤이 하단까지 안닿는 경우 => 버튼을 넣는것도 해결방법
export default function MovieList() {
...
const observerEl = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const currentObserverEl = observerEl.current
const io = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage()
}
})
if (currentObserverEl) {
io.observe(currentObserverEl)
}
return () => {
if (currentObserverEl) {
io.disconnect()
}
}
}, [hasNextPage, fetchNextPage])
return (
...
{isFetching ? <div>로딩 중..</div> : null}
<div
ref={observerEl}
style={{ height: '20px' }}
/>
)
import { useInView } from 'react-intersection-observer'
export default function MovieList() {
...
const { ref, inView } = useInView()
...
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage()
}
}, [inView, hasNextPage, fetchNextPage])
return (
...
<div
ref={ref}
style={{ height: '20px' }}
/>
)
post,put,delete시 사용
Optimistic Update 사용가능 (mutation 시 먼저 UI를 update하는 기능)
만약 잘못된 응답이 왔다면 => 원래 UI로 돌려야함.
const 반환 = useMutation(옵션)
onSuccess : 성공시 실행되는 함수
onError : 실패시 실행되는 함수
retry : 재시도 횟수, 기본값 0
retryDelay : 재시도 시작 간격(2배씩 증가해 시도한다. 1초 =>2초 => 4초...)
queryClient.setMutationDefaults의 기본값 상속을 위한 키
캐시할때 사용함, 캐시한 데이터를 일치시킬 때 (근데 잘 안씀)
mutate는 void반환 => scope를 통해 여러 컴포넌트에서 병렬처리를 보장 할 수 있다.
mutateAsync는 promise반환 => 하나의 컴포넌트 안에서 await으로 처리 순서를 보장 할 수 있다.
onMutate => 변이함수가 실행되기 전에 호출되는 함수이다. 여기서 먼저 front local data를 가지고 낙관적 업데이트를 진행한다.
onSuccess => mutation이 성공했다면? => 쿼리무효화를 통해 서버로부터 데이터 갱신
onError => 실패했다면 다시 캐시데이터를 원상복구, context로 데이터를 전달한다!
import React, { useState } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Users, User } from './Users'
export default function AddUser() {
const [name, setName] = useState('')
const [age, setAge] = useState(0)
const queryClient = useQueryClient()
const { mutate, error, isPending, isError } = useMutation({
mutationFn: async (newUser: User) => {
//
const res = await fetch('https://api.heropy.dev/v0/users', {
method: 'POST',
body: JSON.stringify(newUser)
})
if (!res.ok) throw new Error('변이 중 에러 발생!') // 변이 실패!
return res.json() // 변이 성공!
},
onMutate: async newUser => {
// 낙관적 업데이트 전에 사용자 목록 쿼리를 취소해 잠재적인 충돌 방지!
await queryClient.cancelQueries({ queryKey: ['users'] })
// 캐시된 데이터(사용자 목록) 가져오기!
const cachedUsers = queryClient.getQueryData<Users>(['users'])
// 낙관적 업데이트
if (cachedUsers) {
queryClient.setQueryData<Users>(['users'], [...cachedUsers, newUser])
} //현재 캐시데이터 업데이트
// 각 콜백의 context로 전달할 데이터 반환!
return { previousUsers : cachedUsers }
}, // 사전에 동작
onSuccess: (data, newUser, context) => {
console.log('onSuccess', data, newUser, context)
// 변이 성공 시 캐시 무효화로 사용자 목록 데이터 갱신!
queryClient.invalidateQueries({ queryKey: ['users'] })
},
onError: (error, newUser, context) => {
console.log('onError', error, newUser, context)
// 변이 실패 시, 낙관적 업데이트 결과를 이전 사용자 목록으로 되돌리기!
if (context) {
queryClient.setQueryData(['users'], context.previousUsers)
}
},
onSettled: (data, error, newUser, context) => {
console.log('onSettled', data, error, newUser, context)
}, //항상 호출되는 콜백
retry: 3, // 변이 실패 시 3번 재시도
retryDelay: 500 // 0.5초 간격으로 재시도
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
mutate({ name, age }) // 변이!
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="사용자 이름"
/>
<input
type="number"
value={age || ''}
onChange={e => setAge(Number.parseInt(e.target.value, 10))}
placeholder="사용자 나이"
/>
<button
type="submit"
disabled={isPending}>
{isPending ? '사용자 추가 중..' : '사용자 추가하기!'}
</button>
{isError && <p>에러 발생: {error.message}</p>}
</form>
)
}
useSuspenseQuery를 사용하면, 서버 측 렌더링 단계에서 가져오기를 시도합니다.
원래라면 => query로 가져온 후 => client측에서 렌더링을 업데이트 했으나 요청을 날림과 동시에 서버에서 렌더링해서 보내주는 형태임.
'use client'
import { useSuspenseQuery } from '@tanstack/react-query'
type ResponseValue = {
message: string
time: string
}
export default function DelayedData() {
const { data } = useSuspenseQuery<ResponseValue>({
queryKey: ['delay'],
queryFn: async () => {
const res = await fetch('https://api.heropy.dev/v0/delay?t=1000', {
cache: 'no-store'
})
return res.json()
},
staleTime: 1000 * 10
})
return <div>{data.time}</div>
}
import { Suspense } from 'react'
import DelayedData from '@/components/DelayedData'
export default function Page() {
return (
<Suspense fallback={<div>loading..</div>}>
<DelayedData />
</Suspense>
)
}
ref) https://www.heropy.dev/p/HZaKIE
https://tanstack.com/query/latest
Intersection Observer api : https://blog.hyeyoonjung.com/2019/01/09/intersectionobserver-tutorial/
mdn : https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API