JotaiJotai

상태
React를 위한 기본적이고 유연한 상태 관리

URQL

urql은 GraphQL 쿼리, 캐싱, 상태 관리를 위한 툴킷을 제공합니다.

개요 문서에 따르면:

urql은 GraphQL 초보자도 쉽게 사용할 수 있으면서도 확장 가능한 고도로 커스터마이징 가능한 GraphQL 클라이언트입니다. 앱이 성장함에 따라 정규화된 캐싱과 같은 기능을 추가할 수 있습니다. 동적 단일 앱 애플리케이션과 고도로 커스터마이징된 GraphQL 인프라를 지원하도록 설계되었습니다. 요약하면, urql은 사용성과 적응성을 우선시합니다.

jotai-urql은 URQL을 위한 Jotai 확장 라이브러리입니다. 이 라이브러리는 URQL의 모든 GraphQL 기능을 통합한 일관된 인터페이스를 제공하며, 기존 Jotai 상태와 함께 이러한 기능을 활용할 수 있게 해줍니다.

설치

이 확장 기능을 사용하려면 jotai-urql, @urql/core, 그리고 wonka를 설치해야 합니다.

npm install jotai-urql @urql/core wonka

내보낸 함수들

Basic usage

쿼리:

import { useAtom } from 'jotai'
const countQueryAtom = atomWithQuery<{ count: number }>({
query: 'query Count { count }',
getClient: () => client, // 이 옵션은 전역적으로 `useRehydrateAtom([[clientAtom, client]])`를 사용하는 경우 선택 사항입니다.
})
const Counter = () => {
// 첫 번째 작업 결과가 해결될 때까지 일시 중단됩니다. 오류, 부분 데이터 또는 데이터와 함께 해결됩니다.
const [operationResult, reexecute] = useAtom(countQueryAtom)
if (operationResult.error) {
// 이 오류는 상위 ErrorBoundary에서 처리되어야 합니다.
throw operationResult.error
}
// 여기서는 옵셔널 체이닝을 사용해야 합니다. 데이터가 이 시점에서 undefined일 수 있기 때문입니다(오류가 발생한 경우에만).
return <>{operationResult.data?.count}</>
}

Mutation:

import { useAtom } from 'jotai'
const incrementMutationAtom = atomWithMutation<{ increment: number }>({
query: 'mutation Increment { increment }',
})
const Counter = () => {
const [operationResult, executeMutation] = useAtom(incrementMutationAtom)
return (
<div>
<button
onClick={() =>
executeMutation().then((it) => console.log(it.data?.increment))
}
>
Increment
</button>
<div>{operationResult.data?.increment}</div>
</div>
)
}

함수에 전달되는 옵션의 간소화된 타입

type AtomWithQueryOptions<
Data = unknown,
Variables extends AnyVariables = AnyVariables,
> = {
// 문자열 쿼리, 타입이 지정된 문서 노드, 문서 노드 등을 지원합니다.
query: DocumentInput<Data, Variables>
// 제네릭/타입이 지정된 문서 노드 타입에 따라 동적으로 적용됩니다.
getVariables?: (get: Getter) => Variables
getContext?: (get: Getter) => Partial<OperationContext>
getPause?: (get: Getter) => boolean
getClient?: (get: Getter) => Client
}
type AtomWithMutationOptions<
Data = unknown,
Variables extends AnyVariables = AnyVariables,
> = {
query: DocumentInput<Data, Variables>
getClient?: (get: Getter) => Client
}
// 구독 타입은 AtomWithQueryOptions와 동일합니다.

Suspense 비활성화

더 안정적인 것으로 입증된 import { loadable } from "jotai/utils" 사용을 권장합니다. 하지만 여전히 Suspense를 사용하고 싶다면 다음과 같이 할 수 있습니다:

import { suspenseAtom } from 'jotai-urql'
export const App = () => {
// 앱 전체에 대해 Suspense를 비활성화
useHydrateAtoms([[suspenseAtom, false]])
return <Counter />
}

유용한 헬퍼 훅

다음은 특이한 케이스를 처리하고, @tanstack/react-query의 기본 동작과 유사하게 바인딩을 사용하는 헬퍼 훅입니다. 여기서는 Promise가 거부될 경우 에러를 에러로 처리하며, 주로 근처의 ErrorBoundaries에서 처리됩니다. 이 훅은 Suspense 버전에서만 유효합니다.

useQueryAtomData

data를 깔끔하게 반환하며, 모든 오류 발생/재실행 케이스/예외 상황을 처리합니다.
타입이 재정의되어 data는 절대 undefinednull이 되지 않습니다. (단, 쿼리 자체의 반환 타입이 그럴 경우는 예외)

import type { AnyVariables, OperationResult } from '@urql/core'
import { useAtom } from 'jotai'
import type { AtomWithQuery } from 'jotai-urql'
export const useQueryAtomData = <
Data = unknown,
Variables extends AnyVariables = AnyVariables,
>(
queryAtom: AtomWithQuery<Data, Variables>,
) => {
const [opResult, dispatch] = useAtom(queryAtom)
if (opResult.error && opResult.stale) {
use(
// 여기서 트리를 일시 중단합니다. 이는 `network-only`로 리페치를 사용할 때
// Error Boundary 재시도 로직에서만 트리가 일시 중단되지 않아 발생할 수 있는
// "에러 발생 - Boundary에서 재시도 - 에러 발생 - Boundary에서 재시도"의 무한 루프를 방지합니다.
// (Jotai URQL 바인딩에서만 해당).
// eslint-disable-next-line promise/avoid-new
new Promise((resolve) => {
setTimeout(resolve, 10000) // 이 타임아웃은 새로운 작업 결과가 도착할 때까지
// 이 컴포넌트를 일시 중단시킵니다. 10초 후에는 컴포넌트를 다시 렌더링하려고 시도하고,
// 다시 일시 중단되는 무한 루프에 빠질 수 있습니다.
}),
)
}
if (opResult.error) {
throw opResult.error
}
if (!opResult.data) {
throw Error(
'쿼리 데이터가 undefined입니다. 쿼리를 일시 중단했을 가능성이 있습니다. 그런 경우 `useQueryAtom`을 대신 사용하세요.',
)
}
return [opResult.data, dispatch, opResult] as [
Exclude<typeof opResult.data, undefined>,
typeof dispatch,
typeof opResult,
]
}
// Promise가 해결되는 동안 트리를 일시 중단합니다 (React의 다음 버전에서는 필요 없음).
function use(promise: Promise<any> | any) {
if (promise.status === 'fulfilled') {
return promise.value
}
if (promise.status === 'rejected') {
throw promise.reason
} else if (promise.status === 'pending') {
throw promise
} else {
promise.status = 'pending'
// eslint-disable-next-line promise/catch-or-return
;(promise as Promise<any>).then(
(result: any) => {
promise.status = 'fulfilled'
promise.value = result
},
(reason: any) => {
promise.status = 'rejected'
promise.reason = reason
},
)
throw promise
}
}

기본 데모

동일한 클라이언트 인스턴스를 아톰과 urql 프로바이더에 참조하기

동일한 urqlClient 객체를 참조하려면 프로젝트의 루트를 <Provider>로 감싸고, UrqlProvider에 제공한 것과 동일한 urqlClient 값으로 clientAtom을 초기화해야 합니다.

이 단계를 생략하면 atomWithQuery를 사용할 때마다 클라이언트를 지정해야 할 수 있습니다. 이제는 선택적 getClient 매개변수를 무시하고 컨텍스트에서 클라이언트를 사용할 수 있습니다.

import { Suspense } from 'react'
import { Provider } from 'jotai/react'
import { useHydrateAtoms } from 'jotai/react/utils'
import { clientAtom } from 'jotai-urql'
import {
createClient,
cacheExchange,
fetchExchange,
Provider as UrqlProvider,
} from 'urql'
const urqlClient = createClient({
url: 'https://countries.trevorblades.com/',
exchanges: [cacheExchange, fetchExchange],
fetchOptions: () => {
return { headers: {} }
},
})
const HydrateAtoms = ({ children }) => {
useHydrateAtoms([[clientAtom, urqlClient]])
return children
}
export default function MyApp({ Component, pageProps }) {
return (
<UrqlProvider value={urqlClient}>
<Provider>
<HydrateAtoms>
<Suspense fallback="Loading...">
<Component {...pageProps} />
</Suspense>
</HydrateAtoms>
</Provider>
</UrqlProvider>
)
}