Skip to content

Commit

Permalink
feat(react-query): add queryOptions (#828)
Browse files Browse the repository at this point in the history
fix #767 with @wes5510

@wes5510 I guess that this issue is simmilar issue with
TanStack/query#6556
I want to solve this issue like
TanStack/query#6556 (comment)
(Is this solution same with your opinion?). In my opinion, I think it
would be a good idea to add queryOptions

![Apr-03-2024
01-54-47](https://github.com/suspensive/react/assets/61593290/e767b5ad-df9b-4f05-8678-a5b7f47bd6e4)

### select type when I hover on field name

![image](https://github.com/suspensive/react/assets/61593290/8091735f-40c6-43cd-8035-8682b5c4fb96)
### select type when I hover on select's arg
I expected TData but it's any!!??🥲

![image](https://github.com/suspensive/react/assets/61593290/a8be5353-0f18-4876-bdd9-e791e1d14d70)

I mention you because you suggested this interface(queryOptions) and
I've wanted to add it for a while too. I would like to request a review
to you with our maintainers.

# Solution: new `queryOptions`

This new queryOptions will solve select's type issue like
TanStack/query#6556 (comment)

## More advantages

Tkdodo, The maintainer of TanStack Query explains well why this
interface is needed in [video explaining queryOptions in TanStack Query
v5](https://youtu.be/bhE3wuB_TuA?feature=shared&t=1697).
You can also use queryOptions in TanStack Query v4.

1. QueryKey management becomes easier by processing queryKey and queryFn
together.
2. You can remove unnecessary custom query hooks. This is because they
can all be used directly in `useQuery`, `useQueries` of TanStack Query
v4, and `useSuspenseQuery`, `useSuspenseQueries`, and `SuspenseQuery` of
Suspensive React Query.
3. TanStack Query v5 already supports queryOptions. This Suspensive
React Query's `queryOptions` will make migration from TanStack Query v4
to TanStack Query v5 easier.

```jsx /queryOptions/
import { queryOptions, useSuspenseQuery, useSuspenseQueries, SuspenseQuery } from '@suspensive/react-query'
import { useQuery, useQueries, useQueryClient } from '@tanstack/react-query'

const postQueryOptions = (postId) =>
   queryOptions({
     queryKey: ['posts', postId] as const,
     queryFn: ({
       queryKey: [, postId], // You can use queryKey.
     }) => fetch(`https://example.com/posts/${postId}`),
   })

// No need to create custom query hooks.
// You can use queryOptions directly in useQuery, useQueries, useSuspenseQuery, useSuspenseQueries, SuspenseQuery.
const post1Query = useQuery(postQueryOptions(1))
const { data: post1 } = useSuspenseQuery({
   ...postQueryOptions(1);
   refetchInterval: 2000, // Extensibility is clearly expressed in usage.
})
const [post1Query, post2Query] = useQueries({
   queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const [{ data: post1 }, { data: post2 }] = useSuspenseQueries({
   queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const Example = () => <SuspenseQuery {...postQueryOptions(1)}>{({ data: post1 }) => <>{post1.text}</>}</SuspenseQuery>

// You can easily use queryKey and queryFn in queryClient's methods.
const queryClient = useQueryClient()
queryClient.refetchQueries(postQueryOptions(1))
queryClient.prefetchQuery(postQueryOptions(1))
queryClient.invalidateQueries(postQueryOptions(1))
queryClient.fetchQuery(postQueryOptions(1))
queryClient.resetQueries(postQueryOptions(1))
queryClient.cancelQueries(postQueryOptions(1))

```


## PR Checklist

- [x] I did below actions if need

1. I read the [Contributing
Guide](https://github.com/suspensive/react/blob/main/CONTRIBUTING.md)
2. I added documents and tests.

---------

Co-authored-by: Gihyeon Lee <wes5510@icloud.com>
Co-authored-by: Minsoo Kim <57122180+minsoo-web@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 4, 2024
1 parent 5ba8832 commit 7f19c76
Show file tree
Hide file tree
Showing 18 changed files with 532 additions and 83 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-windows-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@suspensive/react-query": minor
---

feat(react-query): add queryOptions
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Just as [`<SuspenseQuery/>`](./SuspenseQuery.ko.mdx) makes useSuspenseQuery easier to use in jsx, `<SuspenseInfiniteQuery/>` serves to make useSuspenseInfiniteQuery easier to use in jsx.

```jsx
```jsx /SuspenseInfiniteQuery/
import { SuspenseInfiniteQuery } from '@suspensive/react-query'
import { Suspense, ErrorBoundary } from '@suspensive/react'
import { PostItem } from '~/components'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[`<SuspenseQuery/>`](./SuspenseQuery.ko.mdx)가 useSuspenseQuery를 jsx에서 쉽게 사용하게 하는 역할과 마찬가지로 `<SuspenseInfiniteQuery/>`는 useSuspenseInfiniteQuery를 jsx에서 사용하기 쉽게 하기 위한 역할을 합니다.

```jsx
```jsx /SuspenseInfiniteQuery/
import { SuspenseInfiniteQuery } from '@suspensive/react-query'
import { Suspense, ErrorBoundary } from '@suspensive/react'
import { PostItem } from '~/components'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ We provide these components to clearly express what causes suspense at the same
2. Changing the range of Suspense and ErrorBoundary becomes simple. Parallel processing of queries is also easier.
3. Because it manages all data-fetching within the Page component, the internal components are presentational, so it is easy to separate the components.

```jsx
```jsx /SuspenseQuery/
import { SuspenseQuery } from '@suspensive/react-query'
import { Suspense, ErrorBoundary } from '@suspensive/react'
import { PostItem, AuthorProfile } from '~/components'
Expand All @@ -25,13 +25,11 @@ const PostsPage = ({ authorId }) => (
)
```

## Motivation
## Motivation: useSuspenseQuery is not obvious

Because the existing useSuspenseQuery is a hook, it creates components with names such as AuthorInfo and PostList to place Suspense and ErrorBoundary on the parent.
This makes it difficult to predict what suspense and errors will be thrown inside AuthorInfo and PostList.

#### AS-IS: useSuspenseQuery

```jsx
// posts/page.tsx
import { Suspense, ErrorBoundary } from '@suspensive/react'
Expand All @@ -48,7 +46,7 @@ const PostsPage = ({ userId }) => (
)
```

```jsx
```jsx /useSuspenseQuery/
// posts/components/AuthorInfo.tsx
import { useSuspenseQuery } from '@suspensive/react-query'
import { AuthorProfile } from '~/components'
Expand All @@ -62,7 +60,7 @@ const AuthorInfo = ({ userId }) => {
}
```

```jsx
```jsx /useSuspenseQuery/
// posts/components/PostList.tsx
import { useSuspenseQuery } from '@suspensive/react-query'
import { PostListItem } from '~/components'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
2. Suspense, ErrorBoundary의 범위 변경도 간단해집니다. query의 병렬처리도 더 쉽습니다.
3. Page 컴포넌트 내에서 data-fetching을 모두 관장하기 때문에 내부의 컴포넌트는 presentational하므로 컴포넌트를 분리하기 쉽습니다.

```jsx
```jsx /SuspenseQuery/
import { SuspenseQuery } from '@suspensive/react-query'
import { Suspense, ErrorBoundary } from '@suspensive/react'
import { PostItem, AuthorProfile } from '~/components'
Expand All @@ -25,14 +25,12 @@ const PostsPage = ({ authorId }) => (
)
```

## 동기
## 동기: useSuspenseQuery가 명확히 드러나지 않음

기존의 useSuspenseQuery는 훅이기 때문에 부모에 Suspense, ErrorBoundary를 배치하기 위해 AuthorInfo, PostList와 같은 이름을 가진 컴포넌트를 만들게 합니다.
이것은 AuthorInfo, PostList 내부에서 던져질 suspense와 error가 있을지 예측하기 어렵게 만듭니다.

#### AS-IS: useSuspenseQuery

```jsx
```jsx /useSuspenseQuery/
// posts/page.tsx
import { Suspense, ErrorBoundary } from '@suspensive/react'
import { AuthorInfo } from './components/AuthorInfo'
Expand All @@ -48,7 +46,7 @@ const PostsPage = ({ userId }) => (
)
```

```jsx
```jsx /useSuspenseQuery/
// posts/components/AuthorInfo.tsx
import { useSuspenseQuery } from '@suspensive/react-query'
import { AuthorProfile } from '~/components'
Expand All @@ -62,7 +60,7 @@ const AuthorInfo = ({ userId }) => {
}
```

```jsx
```jsx /useSuspenseQuery/
// posts/components/PostList.tsx
import { useSuspenseQuery } from '@suspensive/react-query'
import { PostListItem } from '~/components'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"useSuspenseQuery": { "title": "useSuspenseQuery" },
"useSuspenseQueries": { "title": "useSuspenseQueries" },
"useSuspenseInfiniteQuery": { "title": "useSuspenseInfiniteQuery" },
"queryOptions": { "title": "queryOptions" },
"SuspenseQuery": { "title": "<SuspenseQuery/>" },
"SuspenseInfiniteQuery": { "title": "<SuspenseInfiniteQuery/>" },
"QueryErrorBoundary": { "title": "<QueryErrorBoundary/>" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"useSuspenseQuery": { "title": "useSuspenseQuery" },
"useSuspenseQueries": { "title": "useSuspenseQueries" },
"useSuspenseInfiniteQuery": { "title": "useSuspenseInfiniteQuery" },
"queryOptions": { "title": "queryOptions" },
"SuspenseQuery": { "title": "<SuspenseQuery/>" },
"SuspenseInfiniteQuery": { "title": "<SuspenseInfiniteQuery/>" },
"QueryErrorBoundary": { "title": "<QueryErrorBoundary/>" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Therefore, we will maintain support for @tanstack/react-query v4.
Because the existing useSuspenseQuery is a hook, it creates components with names such as AuthorInfo and PostList to place Suspense and ErrorBoundary on the parent.
This makes it difficult to predict what suspense and errors will be thrown inside AuthorInfo and PostList.

#### AS-IS: useSuspenseQuery
#### Motivation: Want to expose that suspense occurs in child components.

```jsx
// posts/page.tsx
Expand Down Expand Up @@ -69,7 +69,7 @@ const PostList = ({ userId }) => {
}
```

#### TO-BE: `<SuspenseQuery/>`
#### Solution: Let's reveal that suspense occurs with `<SuspenseQuery/>`

Therefore, we provide these components to clearly express what causes suspension at the same depth.

Expand All @@ -96,6 +96,57 @@ const PostsPage = ({ authorId }) => (
)
```

### New `queryOptions` [#828](https://github.com/suspensive/react/pull/828)

Tkdodo, The maintainer of TanStack Query explains well why this interface is needed in [video explaining queryOptions in TanStack Query v5](https://youtu.be/bhE3wuB_TuA?feature=shared&t=1697).
You can also use queryOptions in TanStack Query v4.

1. QueryKey management becomes easier by processing queryKey and queryFn together.
2. You can remove unnecessary custom query hooks. This is because they can all be used directly in `useQuery`, `useQueries` of TanStack Query v4, and `useSuspenseQuery`, `useSuspenseQueries`, and `SuspenseQuery` of Suspensive React Query.
3. TanStack Query v5 already supports queryOptions. This Suspensive React Query's `queryOptions` will make migration from TanStack Query v4 to TanStack Query v5 easier.

```jsx /queryOptions/
import { queryOptions, useSuspenseQuery, useSuspenseQueries, SuspenseQuery } from '@suspensive/react-query'
import { useQuery, useQueries, useQueryClient } from '@tanstack/react-query'

const postQueryOptions = (postId) =>
queryOptions({
queryKey: ['posts', postId] as const,
queryFn: ({
queryKey: [, postId], // You can use queryKey.
}) => fetch(`https://example.com/posts/${postId}`),
})

// No need to create custom query hooks.
// You can use queryOptions directly in useQuery, useQueries, useSuspenseQuery, useSuspenseQueries, SuspenseQuery.
const post1Query = useQuery(postQueryOptions(1))
const { data: post1 } = useSuspenseQuery({
...postQueryOptions(1);
refetchInterval: 2000, // Extensibility is clearly expressed in usage.
})
const [post1Query, post2Query] = useQueries({
queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const [{ data: post1 }, { data: post2 }] = useSuspenseQueries({
queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const Example = () => (
<SuspenseQuery {...postQueryOptions(1)}>
{({ data: post1 }) => <>{post1.text}</>}
</SuspenseQuery>
);

// You can easily use queryKey and queryFn in queryClient's methods.
const queryClient = useQueryClient()
queryClient.refetchQueries(postQueryOptions(1))
queryClient.prefetchQuery(postQueryOptions(1))
queryClient.invalidateQueries(postQueryOptions(1))
queryClient.fetchQuery(postQueryOptions(1))
queryClient.resetQueries(postQueryOptions(1))
queryClient.cancelQueries(postQueryOptions(1))

```

## Handling BREAKING CHANGES

### Removed `enabled`, `placeholderData` option of `useSuspenseQuery`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
기존의 useSuspenseQuery는 훅이기 때문에 부모에 Suspense, ErrorBoundary를 배치하기 위해 AuthorInfo, PostList와 같은 이름을 가진 컴포넌트를 만들게 합니다.
이것은 AuthorInfo, PostList 내부에서 던져질 suspense와 error가 있을지 예측하기 어렵게 만듭니다.

#### AS-IS: useSuspenseQuery
#### 동기: 자식 컴포넌트에서 suspense가 발생됨을 명확히 드러나게 하고 싶음

```jsx
// posts/page.tsx
Expand Down Expand Up @@ -69,7 +69,7 @@ const PostList = ({ userId }) => {
}
```

#### TO-BE: `<SuspenseQuery/>`
#### 해결법: `<SuspenseQuery/>`로 suspense가 발생됨을 드러내자

따라서 같은 depth에서 Suspense를 발생시키는 것이 무엇인지 명확하게 표현하기 위해 이 컴포넌트들을 제공합니다.

Expand All @@ -96,6 +96,57 @@ const PostsPage = ({ authorId }) => (
)
```

### 새로운 `queryOptions` [#828](https://github.com/suspensive/react/pull/828)

TanStack Query의 메인테이너 [Tkdodo의 TanStack Query v5의 queryOptions 설명 영상](https://youtu.be/bhE3wuB_TuA?feature=shared&t=1697)에서 이 interface가 필요한 이유를 잘 설명되어 있습니다.
TanStack Query v4에서도 queryOptions를 활용할 수 있습니다.

1. queryKey와 queryFn을 묶어서 처리해 queryKey관리가 쉬워집니다.
2. 불필요한 커스텀 쿼리 훅을 제거할 수 있습니다. TanStack Query v4의 `useQuery`, `useQueries`, Suspensive React Query의 `useSuspenseQuery`, `useSuspenseQueries`, `SuspenseQuery`에 모두 직접 사용할 수 있기 때문입니다.
3. TanStack Query v5에는 이미 queryOptions가 제공되고 있기 때문에 TanStack Query v4에서 TanStack Query v5로의 마이그레이션이 쉬워집니다.

```jsx /queryOptions/
import { queryOptions, useSuspenseQuery, useSuspenseQueries, SuspenseQuery } from '@suspensive/react-query'
import { useQuery, useQueries, useQueryClient } from '@tanstack/react-query'

const postQueryOptions = (postId) =>
queryOptions({
queryKey: ['posts', postId] as const,
queryFn: ({
queryKey: [, postId], // queryKey를 활용할 수 있습니다.
}) => fetch(`https://example.com/posts/${postId}`),
})

// 커스텀 쿼리 훅을 만들 필요가 없습니다.
// useQuery, useQueries, useSuspenseQuery, useSuspenseQueries, SuspenseQuery에서 직접 queryOptions를 활용할 수 있습니다.
const post1Query = useQuery(postQueryOptions(1))
const { data: post1 } = useSuspenseQuery({
...postQueryOptions(1),
refetchInterval: 2000, // 사용처에서 확장성이 명확히 표현됩니다.
})
const [post1Query, post2Query] = useQueries({
queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const [{ data: post1 }, { data: post2 }] = useSuspenseQueries({
queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const Example = () => (
<SuspenseQuery {...postQueryOptions(1)}>
{({ data: post1 }) => <>{post1.text}</>}
</SuspenseQuery>
);

// queryClient의 메소드에서 queryKey와 queryFn을 쉽게 활용할 수 있습니다.
const queryClient = useQueryClient()
queryClient.refetchQueries(postQueryOptions(1))
queryClient.prefetchQuery(postQueryOptions(1))
queryClient.invalidateQueries(postQueryOptions(1))
queryClient.fetchQuery(postQueryOptions(1))
queryClient.resetQueries(postQueryOptions(1))
queryClient.cancelQueries(postQueryOptions(1))

```

## BREAKING CHANGES 처리하기

### 오직 하나의 queryOptions만을 받습니다.
Expand Down
46 changes: 46 additions & 0 deletions docs/suspensive.org/src/pages/docs/react-query/queryOptions.en.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# queryOptions

Tkdodo, The maintainer of TanStack Query explains well why this interface is needed in [video explaining queryOptions in TanStack Query v5](https://youtu.be/bhE3wuB_TuA?feature=shared&t=1697).
You can also use queryOptions in TanStack Query v4.

1. QueryKey management becomes easier by processing queryKey and queryFn together.
2. You can remove unnecessary custom query hooks. This is because they can all be used directly in `useQuery`, `useQueries` of TanStack Query v4, and `useSuspenseQuery`, `useSuspenseQueries`, and `SuspenseQuery` of Suspensive React Query.
3. TanStack Query v5 already supports queryOptions. This Suspensive React Query's `queryOptions` will make migration from TanStack Query v4 to TanStack Query v5 easier.

```jsx /queryOptions/
import { queryOptions, useSuspenseQuery, useSuspenseQueries, SuspenseQuery } from '@suspensive/react-query'
import { useQuery, useQueries, useQueryClient } from '@tanstack/react-query'

const postQueryOptions = (postId) =>
queryOptions({
queryKey: ['posts', postId] as const,
queryFn: ({
queryKey: [, postId], // You can use queryKey.
}) => fetch(`https://example.com/posts/${postId}`),
})

// No need to create custom query hooks.
// You can use queryOptions directly in useQuery, useQueries, useSuspenseQuery, useSuspenseQueries, SuspenseQuery.
const post1Query = useQuery(postQueryOptions(1))
const { data: post1 } = useSuspenseQuery({
...postQueryOptions(1);
refetchInterval: 2000, // Extensibility is clearly expressed in usage.
})
const [post1Query, post2Query] = useQueries({
queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const [{ data: post1 }, { data: post2 }] = useSuspenseQueries({
queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const Example = () => <SuspenseQuery {...postQueryOptions(1)}>{({ data: post1 }) => <>{post1.text}</>}</SuspenseQuery>

// You can easily use queryKey and queryFn in queryClient's methods.
const queryClient = useQueryClient()
queryClient.refetchQueries(postQueryOptions(1))
queryClient.prefetchQuery(postQueryOptions(1))
queryClient.invalidateQueries(postQueryOptions(1))
queryClient.fetchQuery(postQueryOptions(1))
queryClient.resetQueries(postQueryOptions(1))
queryClient.cancelQueries(postQueryOptions(1))

```
46 changes: 46 additions & 0 deletions docs/suspensive.org/src/pages/docs/react-query/queryOptions.ko.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# queryOptions

TanStack Query의 메인테이너 [Tkdodo의 TanStack Query v5의 queryOptions 설명 영상](https://youtu.be/bhE3wuB_TuA?feature=shared&t=1697)에서 이 interface가 필요한 이유를 잘 설명되어 있습니다.
TanStack Query v4에서도 queryOptions를 활용할 수 있습니다.

1. queryKey와 queryFn을 묶어서 처리해 queryKey관리가 쉬워집니다.
2. 불필요한 커스텀 쿼리 훅을 제거할 수 있습니다. TanStack Query v4의 `useQuery`, `useQueries`, Suspensive React Query의 `useSuspenseQuery`, `useSuspenseQueries`, `SuspenseQuery`에 모두 직접 사용할 수 있기 때문입니다.
3. TanStack Query v5에는 이미 queryOptions가 제공되고 있기 때문에 TanStack Query v4에서 TanStack Query v5로의 마이그레이션이 쉬워집니다.

```jsx /queryOptions/
import { queryOptions, useSuspenseQuery, useSuspenseQueries, SuspenseQuery } from '@suspensive/react-query'
import { useQuery, useQueries, useQueryClient } from '@tanstack/react-query'

const postQueryOptions = (postId) =>
queryOptions({
queryKey: ['posts', postId] as const,
queryFn: ({
queryKey: [, postId], // queryKey를 활용할 수 있습니다.
}) => fetch(`https://example.com/posts/${postId}`),
})

// 커스텀 쿼리 훅을 만들 필요가 없습니다.
// useQuery, useQueries, useSuspenseQuery, useSuspenseQueries, SuspenseQuery에서 직접 queryOptions를 활용할 수 있습니다.
const post1Query = useQuery(postQueryOptions(1))
const { data: post1 } = useSuspenseQuery({
...postQueryOptions(1),
refetchInterval: 2000, // 사용처에서 확장성을 명확히 표현됩니다.
})
const [post1Query, post2Query] = useQueries({
queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const [{ data: post1 }, { data: post2 }] = useSuspenseQueries({
queries: [postQueryOptions(1), { ...postQueryOptions(2), refetchInterval: 2000 }],
})
const Example = () => <SuspenseQuery {...postQueryOptions(1)}>{({ data: post1 }) => <>{post1.text}</>}</SuspenseQuery>

// queryClient의 메소드에서 queryKey와 queryFn을 쉽게 활용할 수 있습니다.
const queryClient = useQueryClient()
queryClient.refetchQueries(postQueryOptions(1))
queryClient.prefetchQuery(postQueryOptions(1))
queryClient.invalidateQueries(postQueryOptions(1))
queryClient.fetchQuery(postQueryOptions(1))
queryClient.resetQueries(postQueryOptions(1))
queryClient.cancelQueries(postQueryOptions(1))

```
1 change: 1 addition & 0 deletions packages/react-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { queryOptions } from './queryOptions'
export { SuspenseQuery } from './SuspenseQuery'
export { SuspenseInfiniteQuery } from './SuspenseInfiniteQuery'

Expand Down

0 comments on commit 7f19c76

Please sign in to comment.