Skip to content

Commit

Permalink
Merge branch 'feature/isSubscribed' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
TkDodo authored Jan 6, 2025
2 parents bf7971f + 80490f1 commit 7af572f
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 10 deletions.
104 changes: 104 additions & 0 deletions packages/react-query/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5964,6 +5964,110 @@ describe('useQuery', () => {
})
})

describe('subscribed', () => {
it('should be able to toggle subscribed', async () => {
const key = queryKey()
const queryFn = vi.fn(async () => 'data')
function Page() {
const [subscribed, setSubscribed] = React.useState(true)
const { data } = useQuery({
queryKey: key,
queryFn,
subscribed,
})
return (
<div>
<span>data: {data}</span>
<button onClick={() => setSubscribed(!subscribed)}>toggle</button>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('data: data'))

expect(
queryClient.getQueryCache().find({ queryKey: key })!.observers.length,
).toBe(1)

fireEvent.click(rendered.getByRole('button', { name: 'toggle' }))

expect(
queryClient.getQueryCache().find({ queryKey: key })!.observers.length,
).toBe(0)

expect(queryFn).toHaveBeenCalledTimes(1)

fireEvent.click(rendered.getByRole('button', { name: 'toggle' }))

// background refetch when we re-subscribe
await waitFor(() => expect(queryFn).toHaveBeenCalledTimes(2))
expect(
queryClient.getQueryCache().find({ queryKey: key })!.observers.length,
).toBe(1)
})

it('should not be attached to the query when subscribed is false', async () => {
const key = queryKey()
const queryFn = vi.fn(async () => 'data')
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn,
subscribed: false,
})
return (
<div>
<span>data: {data}</span>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('data:'))

expect(
queryClient.getQueryCache().find({ queryKey: key })!.observers.length,
).toBe(0)

expect(queryFn).toHaveBeenCalledTimes(0)
})

it('should not re-render when data is added to the cache when subscribed is false', async () => {
const key = queryKey()
let renders = 0
function Page() {
const { data } = useQuery({
queryKey: key,
queryFn: async () => 'data',
subscribed: false,
})
renders++
return (
<div>
<span>{data ? 'has data' + data : 'no data'}</span>
<button
onClick={() => queryClient.setQueryData<string>(key, 'new data')}
>
set data
</button>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('no data'))

fireEvent.click(rendered.getByRole('button', { name: 'set data' }))

await sleep(10)

await waitFor(() => rendered.getByText('no data'))

expect(renders).toBe(1)
})
})

it('should have status=error on mount when a query has failed', async () => {
const key = queryKey()
const states: Array<UseQueryResult<unknown>> = []
Expand Down
8 changes: 7 additions & 1 deletion packages/react-query/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ export interface UseBaseQueryOptions<
TData,
TQueryData,
TQueryKey
> {}
> {
/**
* Set this to `false` to unsubscribe this observer from updates to the query cache.
* Defaults to `true`.
*/
subscribed?: boolean
}

export type AnyUseQueryOptions = UseQueryOptions<any, any, any, any>
export interface UseQueryOptions<
Expand Down
10 changes: 6 additions & 4 deletions packages/react-query/src/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,24 @@ export function useBaseQuery<
),
)

// note: this must be called before useSyncExternalStore
const result = observer.getOptimisticResult(defaultedOptions)

const shouldSubscribe = !isRestoring && options.subscribed !== false
React.useSyncExternalStore(
React.useCallback(
(onStoreChange) => {
const unsubscribe = isRestoring
? noop
: observer.subscribe(notifyManager.batchCalls(onStoreChange))
const unsubscribe = shouldSubscribe
? observer.subscribe(notifyManager.batchCalls(onStoreChange))
: noop

// Update result to make sure we did not miss any query updates
// between creating the observer and subscribing to it.
observer.updateResult()

return unsubscribe
},
[observer, isRestoring],
[observer, shouldSubscribe],
),
() => observer.getCurrentResult(),
() => observer.getCurrentResult(),
Expand Down
13 changes: 8 additions & 5 deletions packages/react-query/src/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type UseQueryOptionsForUseQueries<
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'placeholderData'
'placeholderData' | 'subscribed'
> & {
placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData>
}
Expand Down Expand Up @@ -231,6 +231,7 @@ export function useQueries<
}: {
queries: readonly [...QueriesOptions<T>]
combine?: (result: QueriesResults<T>) => TCombinedResult
subscribed?: boolean
},
queryClient?: QueryClient,
): TCombinedResult {
Expand Down Expand Up @@ -271,19 +272,21 @@ export function useQueries<
),
)

// note: this must be called before useSyncExternalStore
const [optimisticResult, getCombinedResult, trackResult] =
observer.getOptimisticResult(
defaultedQueries,
(options as QueriesObserverOptions<TCombinedResult>).combine,
)

const shouldSubscribe = !isRestoring && options.subscribed !== false
React.useSyncExternalStore(
React.useCallback(
(onStoreChange) =>
isRestoring
? noop
: observer.subscribe(notifyManager.batchCalls(onStoreChange)),
[observer, isRestoring],
shouldSubscribe
? observer.subscribe(notifyManager.batchCalls(onStoreChange))
: noop,
[observer, shouldSubscribe],
),
() => observer.getCurrentResult(),
() => observer.getCurrentResult(),
Expand Down

0 comments on commit 7af572f

Please sign in to comment.