Skip to content

Commit

Permalink
feat(solid-query): Add reconcile option (#5287)
Browse files Browse the repository at this point in the history
* feat(solid-query): Add `reconcile` option

* feat(solid-query): Add `reconcile` callback test
  • Loading branch information
ardeora authored Apr 21, 2023
1 parent 0b0b706 commit 831a811
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 41 deletions.
84 changes: 84 additions & 0 deletions packages/solid-query/src/QueryClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type {
QueryClientConfig as QueryCoreClientConfig,
DefaultOptions as CoreDefaultOptions,
QueryObserverOptions as QueryCoreObserverOptions,
InfiniteQueryObserverOptions as QueryCoreInfiniteQueryObserverOptions,
DefaultError,
QueryKey,
} from '@tanstack/query-core'
import { QueryClient as QueryCoreClient } from '@tanstack/query-core'

export interface QueryObserverOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = never,
> extends Omit<
QueryCoreObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>,
'structuralSharing'
> {
/**
* Set this to a reconciliation key to enable reconciliation between query results.
* Set this to `false` to disable reconciliation between query results.
* Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom reconciliation logic.
* Defaults reconciliation key to `id`.
*/
reconcile?:
| string
| false
| ((oldData: TData | undefined, newData: TData) => TData)
}

export interface InfiniteQueryObserverOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> extends Omit<
QueryCoreInfiniteQueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey,
TPageParam
>,
'structuralSharing'
> {
/**
* Set this to a reconciliation key to enable reconciliation between query results.
* Set this to `false` to disable reconciliation between query results.
* Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom reconciliation logic.
* Defaults reconciliation key to `id`.
*/
reconcile?:
| string
| false
| ((oldData: TData | undefined, newData: TData) => TData)
}

export interface DefaultOptions<TError = DefaultError>
extends CoreDefaultOptions<TError> {
queries?: QueryObserverOptions<unknown, TError>
}

export interface QueryClientConfig extends QueryCoreClientConfig {
defaultOptions?: DefaultOptions
}

export class QueryClient extends QueryCoreClient {
constructor(config: QueryClientConfig = {}) {
super(config)
}
}
2 changes: 1 addition & 1 deletion packages/solid-query/src/QueryClientProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { QueryClient } from '@tanstack/query-core'
import type { QueryClient } from './QueryClient'
import type { JSX } from 'solid-js'
import { createContext, useContext, onMount, onCleanup } from 'solid-js'

Expand Down
91 changes: 75 additions & 16 deletions packages/solid-query/src/__tests__/createInfiniteQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Index,
Match,
Switch,
on,
} from 'solid-js'
import type {
CreateInfiniteQueryResult,
Expand Down Expand Up @@ -193,7 +194,8 @@ describe('useInfiniteQuery', () => {

it('should keep the previous data when placeholderData is set', async () => {
const key = queryKey()
const states: CreateInfiniteQueryResult<InfiniteData<string>>[] = []
const states: Partial<CreateInfiniteQueryResult<InfiniteData<string>>>[] =
[]

function Page() {
const [order, setOrder] = createSignal('desc')
Expand All @@ -212,7 +214,16 @@ describe('useInfiniteQuery', () => {
}))

createRenderEffect(() => {
states.push({ ...state })
states.push({
data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined,
hasNextPage: state.hasNextPage,
hasPreviousPage: state.hasPreviousPage,
isFetching: state.isFetching,
isFetchingNextPage: state.isFetchingNextPage,
isFetchingPreviousPage: state.isFetchingPreviousPage,
isSuccess: state.isSuccess,
isPlaceholderData: state.isPlaceholderData,
})
})

return (
Expand Down Expand Up @@ -375,7 +386,8 @@ describe('useInfiniteQuery', () => {

it('should be able to reverse the data', async () => {
const key = queryKey()
const states: CreateInfiniteQueryResult<InfiniteData<number>>[] = []
const states: Partial<CreateInfiniteQueryResult<InfiniteData<number>>>[] =
[]

function Page() {
const state = createInfiniteQuery(() => ({
Expand All @@ -394,9 +406,19 @@ describe('useInfiniteQuery', () => {
defaultPageParam: 0,
}))

createRenderEffect(() => {
states.push({ ...state })
})
createRenderEffect(
on(
() => ({ ...state }),
() => {
states.push({
data: state.data
? JSON.parse(JSON.stringify(state.data))
: undefined,
isSuccess: state.isSuccess,
})
},
),
)

return (
<div>
Expand Down Expand Up @@ -439,7 +461,8 @@ describe('useInfiniteQuery', () => {

it('should be able to fetch a previous page', async () => {
const key = queryKey()
const states: CreateInfiniteQueryResult<InfiniteData<number>>[] = []
const states: Partial<CreateInfiniteQueryResult<InfiniteData<number>>>[] =
[]

function Page() {
const start = 10
Expand All @@ -456,7 +479,15 @@ describe('useInfiniteQuery', () => {
}))

createRenderEffect(() => {
states.push({ ...state })
states.push({
data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined,
hasNextPage: state.hasNextPage,
hasPreviousPage: state.hasPreviousPage,
isFetching: state.isFetching,
isFetchingNextPage: state.isFetchingNextPage,
isFetchingPreviousPage: state.isFetchingPreviousPage,
isSuccess: state.isSuccess,
})
})

createEffect(() => {
Expand Down Expand Up @@ -518,7 +549,8 @@ describe('useInfiniteQuery', () => {

it('should be able to refetch when providing page params automatically', async () => {
const key = queryKey()
const states: CreateInfiniteQueryResult<InfiniteData<number>>[] = []
const states: Partial<CreateInfiniteQueryResult<InfiniteData<number>>>[] =
[]

function Page() {
const state = createInfiniteQuery(() => ({
Expand All @@ -535,7 +567,13 @@ describe('useInfiniteQuery', () => {
}))

createRenderEffect(() => {
states.push({ ...state })
states.push({
data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined,
isFetching: state.isFetching,
isFetchingNextPage: state.isFetchingNextPage,
isRefetching: state.isRefetching,
isFetchingPreviousPage: state.isFetchingPreviousPage,
})
})

return (
Expand Down Expand Up @@ -632,7 +670,8 @@ describe('useInfiniteQuery', () => {

it('should silently cancel any ongoing fetch when fetching more', async () => {
const key = queryKey()
const states: CreateInfiniteQueryResult<InfiniteData<number>>[] = []
const states: Partial<CreateInfiniteQueryResult<InfiniteData<number>>>[] =
[]

function Page() {
const start = 10
Expand All @@ -649,7 +688,13 @@ describe('useInfiniteQuery', () => {
}))

createRenderEffect(() => {
states.push({ ...state })
states.push({
hasNextPage: state.hasNextPage,
data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined,
isFetching: state.isFetching,
isFetchingNextPage: state.isFetchingNextPage,
isSuccess: state.isSuccess,
})
})

createEffect(() => {
Expand Down Expand Up @@ -978,7 +1023,8 @@ describe('useInfiniteQuery', () => {

it('should be able to set new pages with the query client', async () => {
const key = queryKey()
const states: CreateInfiniteQueryResult<InfiniteData<number>>[] = []
const states: Partial<CreateInfiniteQueryResult<InfiniteData<number>>>[] =
[]

function Page() {
const [firstPage, setFirstPage] = createSignal(0)
Expand All @@ -996,7 +1042,13 @@ describe('useInfiniteQuery', () => {
}))

createRenderEffect(() => {
states.push({ ...state })
states.push({
hasNextPage: state.hasNextPage,
data: state.data ? JSON.parse(JSON.stringify(state.data)) : undefined,
isFetching: state.isFetching,
isFetchingNextPage: state.isFetchingNextPage,
isSuccess: state.isSuccess,
})
})

createEffect(() => {
Expand Down Expand Up @@ -1066,7 +1118,8 @@ describe('useInfiniteQuery', () => {

it('should only refetch the first page when initialData is provided', async () => {
const key = queryKey()
const states: CreateInfiniteQueryResult<InfiniteData<number>>[] = []
const states: Partial<CreateInfiniteQueryResult<InfiniteData<number>>>[] =
[]

function Page() {
const state = createInfiniteQuery(() => ({
Expand All @@ -1083,7 +1136,13 @@ describe('useInfiniteQuery', () => {
}))

createRenderEffect(() => {
states.push({ ...state })
states.push({
data: JSON.parse(JSON.stringify(state.data)),
hasNextPage: state.hasNextPage,
isFetching: state.isFetching,
isFetchingNextPage: state.isFetchingNextPage,
isSuccess: state.isSuccess,
})
})

createEffect(() => {
Expand Down
63 changes: 59 additions & 4 deletions packages/solid-query/src/__tests__/createQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from './utils'
import { vi } from 'vitest'
import type { Mock } from 'vitest'
import { reconcile } from 'solid-js/store'

describe('createQuery', () => {
const queryCache = new QueryCache()
Expand Down Expand Up @@ -1282,7 +1283,6 @@ describe('createQuery', () => {
count++
return count === 1 ? result1 : result2
},
notifyOnChangeProps: 'all',
}))

createRenderEffect(() => {
Expand Down Expand Up @@ -1322,9 +1322,8 @@ describe('createQuery', () => {

expect(todos).toEqual(result1)
expect(newTodos).toEqual(result2)
expect(newTodos).not.toBe(todos)
expect(newTodo1).toBe(todo1)
expect(newTodo2).not.toBe(todo2)
expect(newTodo2).toBe(todo2)

return null
})
Expand Down Expand Up @@ -3257,7 +3256,7 @@ describe('createQuery', () => {

it('should keep initial data when the query key changes', async () => {
const key = queryKey()
const states: DefinedCreateQueryResult<{ count: number }>[] = []
const states: Partial<DefinedCreateQueryResult<{ count: number }>>[] = []

function Page() {
const [count, setCount] = createSignal(0)
Expand All @@ -3266,6 +3265,7 @@ describe('createQuery', () => {
queryFn: () => ({ count: 10 }),
staleTime: Infinity,
initialData: () => ({ count: count() }),
reconcile: false,
}))
createRenderEffect(() => {
states.push({ ...state })
Expand Down Expand Up @@ -4610,6 +4610,61 @@ describe('createQuery', () => {
expect(states).toHaveLength(1)
})

it('The reconcile fn callback should correctly maintain referential equality', async () => {
const key1 = queryKey()
const states: Array<Array<number>> = []

function Page() {
const [forceValue, setForceValue] = createSignal(1)

const state = createQuery(() => ({
queryKey: key1,
queryFn: async () => {
await sleep(10)
return [1, 2]
},
select: (res) => res.map((x) => x + 1),
reconcile(oldData, newData) {
return reconcile(newData)(oldData)
},
}))

createEffect(() => {
if (state.data) {
states.push(state.data)
}
})

const forceUpdate = () => {
setForceValue((prev) => prev + 1)
}

return (
<div>
<h2>Data: {JSON.stringify(state.data)}</h2>
<h2>forceValue: {forceValue}</h2>
<button onClick={forceUpdate}>forceUpdate</button>
</div>
)
}

render(() => (
<QueryClientProvider client={queryClient}>
<Page />
</QueryClientProvider>
))
await waitFor(() => screen.getByText('Data: [2,3]'))
expect(states).toHaveLength(1)

fireEvent.click(screen.getByRole('button', { name: /forceUpdate/i }))

await waitFor(() => screen.getByText('forceValue: 2'))
await waitFor(() => screen.getByText('Data: [2,3]'))

// effect should not be triggered again due to structural sharing
expect(states).toHaveLength(1)
})

it('should cancel the query function when there are no more subscriptions', async () => {
const key = queryKey()
let cancelFn: Mock = vi.fn()
Expand Down
2 changes: 1 addition & 1 deletion packages/solid-query/src/__tests__/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { QueryClientConfig } from '@tanstack/query-core'
import { QueryClient } from '@tanstack/query-core'
import { QueryClient } from '../QueryClient'
import type { ParentProps } from 'solid-js'
import { createEffect, createSignal, onCleanup, Show } from 'solid-js'
import { vi } from 'vitest'
Expand Down
Loading

0 comments on commit 831a811

Please sign in to comment.