@tanstack/solid-query 5.0.0-beta.20 → 5.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,12 +8,16 @@ import {
8
8
  createComputed,
9
9
  createMemo,
10
10
  createResource,
11
+ createSignal,
12
+ mergeProps,
11
13
  on,
12
14
  onCleanup,
15
+ untrack,
13
16
  } from 'solid-js'
14
17
  import { createStore, reconcile, unwrap } from 'solid-js/store'
15
18
  import { useQueryClient } from './QueryClientProvider'
16
19
  import { shouldThrowError } from './utils'
20
+ import { useIsRestoring } from './isRestoring'
17
21
  import type { CreateBaseQueryOptions } from './types'
18
22
  import type { Accessor } from 'solid-js'
19
23
  import type { QueryClient } from './QueryClient'
@@ -104,18 +108,31 @@ export function createBaseQuery<
104
108
  | QueryObserverResult<TData, TError>
105
109
 
106
110
  const client = createMemo(() => useQueryClient(queryClient?.()))
111
+ const isRestoring = useIsRestoring()
107
112
 
108
- const defaultedOptions = client().defaultQueryOptions(options())
109
- defaultedOptions._optimisticResults = 'optimistic'
110
- defaultedOptions.structuralSharing = false
111
- if (isServer) {
112
- defaultedOptions.retry = false
113
- defaultedOptions.throwOnError = true
114
- }
115
- const observer = new Observer(client(), defaultedOptions)
113
+ const defaultedOptions = createMemo(() =>
114
+ mergeProps(client().defaultQueryOptions(options()), {
115
+ get _optimisticResults() {
116
+ return isRestoring() ? 'isRestoring' : 'optimistic'
117
+ },
118
+ structuralSharing: false,
119
+ ...(isServer && { retry: false, throwOnError: true }),
120
+ }),
121
+ )
122
+
123
+ const [observer, setObserver] = createSignal(
124
+ new Observer(client(), untrack(defaultedOptions)),
125
+ )
126
+ // we set the value in a computed because `createMemo`
127
+ // returns undefined during transitions
128
+ createComputed(
129
+ on(client, (c) => setObserver(new Observer(c, defaultedOptions())), {
130
+ defer: true,
131
+ }),
132
+ )
116
133
 
117
134
  const [state, setState] = createStore<QueryObserverResult<TData, TError>>(
118
- observer.getOptimisticResult(defaultedOptions),
135
+ observer().getOptimisticResult(defaultedOptions()),
119
136
  )
120
137
 
121
138
  const createServerSubscriber = (
@@ -124,9 +141,9 @@ export function createBaseQuery<
124
141
  ) => void,
125
142
  reject: (reason?: any) => void,
126
143
  ) => {
127
- return observer.subscribe((result) => {
144
+ return observer().subscribe((result) => {
128
145
  notifyManager.batchCalls(() => {
129
- const query = observer.getCurrentQuery()
146
+ const query = observer().getCurrentQuery()
130
147
  const unwrappedResult = hydrateableObserverResult(query, result)
131
148
 
132
149
  if (unwrappedResult.isError) {
@@ -139,32 +156,30 @@ export function createBaseQuery<
139
156
  }
140
157
 
141
158
  const createClientSubscriber = () => {
142
- return observer.subscribe((result) => {
159
+ const obs = observer()
160
+ return obs.subscribe((result) => {
143
161
  notifyManager.batchCalls(() => {
144
162
  // @ts-expect-error - This will error because the reconcile option does not
145
163
  // exist on the query-core QueryObserverResult type
146
- const reconcileOptions = observer.options.reconcile
164
+ const reconcileOptions = obs.options.reconcile
165
+
166
+ setState((store) => {
167
+ return reconcileFn(
168
+ store,
169
+ result,
170
+ reconcileOptions === undefined ? 'id' : reconcileOptions,
171
+ )
172
+ })
147
173
  // If the query has data we dont suspend but instead mutate the resource
148
174
  // This could happen when placeholderData/initialData is defined
149
- if (queryResource()?.data && result.data && !queryResource.loading) {
150
- setState((store) => {
151
- return reconcileFn(
152
- store,
153
- result,
154
- reconcileOptions === undefined ? 'id' : reconcileOptions,
155
- )
156
- })
175
+ if (
176
+ queryResource()?.data &&
177
+ result.data &&
178
+ !queryResource.loading &&
179
+ isRestoring()
180
+ )
157
181
  mutate(state)
158
- } else {
159
- setState((store) => {
160
- return reconcileFn(
161
- store,
162
- result,
163
- reconcileOptions === undefined ? 'id' : reconcileOptions,
164
- )
165
- })
166
- refetch()
167
- }
182
+ else refetch()
168
183
  })()
169
184
  })
170
185
  }
@@ -178,17 +193,16 @@ export function createBaseQuery<
178
193
  ResourceData | undefined
179
194
  >(
180
195
  () => {
196
+ const obs = observer()
181
197
  return new Promise((resolve, reject) => {
182
- if (isServer) {
183
- unsubscribe = createServerSubscriber(resolve, reject)
184
- } else {
185
- if (!unsubscribe) {
186
- unsubscribe = createClientSubscriber()
187
- }
188
- }
198
+ if (isServer) unsubscribe = createServerSubscriber(resolve, reject)
199
+ else if (!unsubscribe && !isRestoring())
200
+ unsubscribe = createClientSubscriber()
201
+
202
+ obs.updateResult()
189
203
 
190
- if (!state.isLoading) {
191
- const query = observer.getCurrentQuery()
204
+ if (!state.isLoading && !isRestoring()) {
205
+ const query = obs.getCurrentQuery()
192
206
  resolve(hydrateableObserverResult(query, state))
193
207
  }
194
208
  })
@@ -197,7 +211,9 @@ export function createBaseQuery<
197
211
  initialValue: state,
198
212
 
199
213
  // If initialData is provided, we resolve the resource immediately
200
- ssrLoadFrom: options().initialData ? 'initial' : 'server',
214
+ get ssrLoadFrom() {
215
+ return options().initialData ? 'initial' : 'server'
216
+ },
201
217
 
202
218
  get deferStream() {
203
219
  return options().deferStream
@@ -212,37 +228,50 @@ export function createBaseQuery<
212
228
  * Note that this is only invoked on the client, for queries that were originally run on the server.
213
229
  */
214
230
  onHydrated(_k, info) {
231
+ const defaultOptions = defaultedOptions()
215
232
  if (info.value) {
216
233
  hydrate(client(), {
217
234
  queries: [
218
235
  {
219
- queryKey: defaultedOptions.queryKey,
220
- queryHash: defaultedOptions.queryHash,
236
+ queryKey: defaultOptions.queryKey,
237
+ queryHash: defaultOptions.queryHash,
221
238
  state: info.value,
222
239
  },
223
240
  ],
224
241
  })
225
242
  }
226
243
 
227
- if (!unsubscribe) {
228
- /**
229
- * Do not refetch query on mount if query was fetched on server,
230
- * even if `staleTime` is not set.
231
- */
232
- const newOptions = { ...defaultedOptions }
233
- if (defaultedOptions.staleTime || !defaultedOptions.initialData) {
234
- newOptions.refetchOnMount = false
235
- }
236
- // Setting the options as an immutable object to prevent
237
- // wonky behavior with observer subscriptions
238
- observer.setOptions(newOptions)
239
- setState(observer.getOptimisticResult(newOptions))
240
- unsubscribe = createClientSubscriber()
244
+ if (unsubscribe) return
245
+ /**
246
+ * Do not refetch query on mount if query was fetched on server,
247
+ * even if `staleTime` is not set.
248
+ */
249
+ const newOptions = { ...defaultOptions }
250
+ if (defaultOptions.staleTime || !defaultOptions.initialData) {
251
+ newOptions.refetchOnMount = false
241
252
  }
253
+ // Setting the options as an immutable object to prevent
254
+ // wonky behavior with observer subscriptions
255
+ observer().setOptions(newOptions)
256
+ setState(observer().getOptimisticResult(newOptions))
257
+ unsubscribe = createClientSubscriber()
242
258
  },
243
259
  },
244
260
  )
245
261
 
262
+ createComputed(
263
+ on(
264
+ [isRestoring, observer],
265
+ ([restoring]) => {
266
+ const unsub = unsubscribe
267
+ queueMicrotask(() => unsub?.())
268
+ unsubscribe = null
269
+ if (!restoring) refetch()
270
+ },
271
+ { defer: true },
272
+ ),
273
+ )
274
+
246
275
  onCleanup(() => {
247
276
  if (unsubscribe) {
248
277
  unsubscribe()
@@ -252,8 +281,11 @@ export function createBaseQuery<
252
281
 
253
282
  createComputed(
254
283
  on(
255
- () => client().defaultQueryOptions(options()),
256
- () => observer.setOptions(client().defaultQueryOptions(options())),
284
+ [observer, defaultedOptions],
285
+ ([obs, opts]) => {
286
+ obs.setOptions(opts)
287
+ setState(obs.getOptimisticResult(opts))
288
+ },
257
289
  {
258
290
  // Defer because we don't need to trigger on first render
259
291
  // This only cares about changes to options after the observer is created
@@ -266,12 +298,14 @@ export function createBaseQuery<
266
298
  on(
267
299
  () => state.status,
268
300
  () => {
301
+ const obs = observer()
269
302
  if (
270
303
  state.isError &&
271
304
  !state.isFetching &&
272
- shouldThrowError(observer.options.throwOnError, [
305
+ !isRestoring() &&
306
+ shouldThrowError(obs.options.throwOnError, [
273
307
  state.error,
274
- observer.getCurrentQuery(),
308
+ obs.getCurrentQuery(),
275
309
  ])
276
310
  ) {
277
311
  throw state.error
@@ -1,5 +1,5 @@
1
1
  import { MutationObserver } from '@tanstack/query-core'
2
- import { createComputed, on, onCleanup } from 'solid-js'
2
+ import { createComputed, createMemo, on, onCleanup } from 'solid-js'
3
3
  import { createStore } from 'solid-js/store'
4
4
  import { useQueryClient } from './QueryClientProvider'
5
5
  import { shouldThrowError } from './utils'
@@ -22,10 +22,10 @@ export function createMutation<
22
22
  options: CreateMutationOptions<TData, TError, TVariables, TContext>,
23
23
  queryClient?: Accessor<QueryClient>,
24
24
  ): CreateMutationResult<TData, TError, TVariables, TContext> {
25
- const client = useQueryClient(queryClient?.())
25
+ const client = createMemo(() => useQueryClient(queryClient?.()))
26
26
 
27
27
  const observer = new MutationObserver<TData, TError, TVariables, TContext>(
28
- client,
28
+ client(),
29
29
  options(),
30
30
  )
31
31
 
@@ -1,7 +1,19 @@
1
- import { QueriesObserver, notifyManager } from '@tanstack/query-core'
2
- import { createComputed, onCleanup } from 'solid-js'
1
+ import { QueriesObserver } from '@tanstack/query-core'
3
2
  import { createStore, unwrap } from 'solid-js/store'
3
+ import {
4
+ batch,
5
+ createComputed,
6
+ createMemo,
7
+ createRenderEffect,
8
+ createResource,
9
+ mergeProps,
10
+ on,
11
+ onCleanup,
12
+ onMount,
13
+ } from 'solid-js'
4
14
  import { useQueryClient } from './QueryClientProvider'
15
+ import { useIsRestoring } from './isRestoring'
16
+ import type { CreateQueryResult, SolidQueryOptions } from './types'
5
17
  import type { Accessor } from 'solid-js'
6
18
  import type { QueryClient } from './QueryClient'
7
19
  import type {
@@ -10,8 +22,8 @@ import type {
10
22
  QueriesPlaceholderDataFunction,
11
23
  QueryFunction,
12
24
  QueryKey,
25
+ QueryObserverResult,
13
26
  } from '@tanstack/query-core'
14
- import type { CreateQueryResult, SolidQueryOptions } from './types'
15
27
 
16
28
  // This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
17
29
  // `placeholderData` function does not have a parameter
@@ -159,7 +171,7 @@ export type QueriesResults<
159
171
 
160
172
  export function createQueries<
161
173
  T extends Array<any>,
162
- TCombinedResult = QueriesResults<T>,
174
+ TCombinedResult extends QueriesResults<T> = QueriesResults<T>,
163
175
  >(
164
176
  queriesOptions: Accessor<{
165
177
  queries: readonly [...QueriesOptions<T>]
@@ -167,17 +179,22 @@ export function createQueries<
167
179
  }>,
168
180
  queryClient?: Accessor<QueryClient>,
169
181
  ): TCombinedResult {
170
- const client = useQueryClient(queryClient?.())
182
+ const client = createMemo(() => useQueryClient(queryClient?.()))
183
+ const isRestoring = useIsRestoring()
171
184
 
172
- const defaultedQueries = queriesOptions().queries.map((options) => {
173
- const defaultedOptions = client.defaultQueryOptions(options)
174
- defaultedOptions._optimisticResults = 'optimistic'
175
- return defaultedOptions
176
- })
185
+ const defaultedQueries = createMemo(() =>
186
+ queriesOptions().queries.map((options) =>
187
+ mergeProps(client().defaultQueryOptions(options), {
188
+ get _optimisticResults() {
189
+ return isRestoring() ? 'isRestoring' : 'optimistic'
190
+ },
191
+ }),
192
+ ),
193
+ )
177
194
 
178
195
  const observer = new QueriesObserver(
179
- client,
180
- defaultedQueries,
196
+ client(),
197
+ defaultedQueries(),
181
198
  queriesOptions().combine
182
199
  ? ({
183
200
  combine: queriesOptions().combine,
@@ -185,27 +202,89 @@ export function createQueries<
185
202
  : undefined,
186
203
  )
187
204
 
188
- // @ts-expect-error - Types issue with solid-js createStore
189
205
  const [state, setState] = createStore<TCombinedResult>(
190
- observer.getOptimisticResult(defaultedQueries)[1](),
206
+ observer.getOptimisticResult(defaultedQueries())[1](),
207
+ )
208
+
209
+ createRenderEffect(
210
+ on(
211
+ () => queriesOptions().queries.length,
212
+ () => setState(observer.getOptimisticResult(defaultedQueries())[1]()),
213
+ ),
214
+ )
215
+
216
+ const dataResources = createMemo(
217
+ on(
218
+ () => state.length,
219
+ () =>
220
+ state.map((queryRes) => {
221
+ const dataPromise = () =>
222
+ new Promise((resolve) => {
223
+ if (queryRes.isFetching && queryRes.isLoading) return
224
+ resolve(unwrap(queryRes.data))
225
+ })
226
+ return createResource(dataPromise)
227
+ }),
228
+ ),
191
229
  )
192
230
 
193
- const unsubscribe = observer.subscribe((result) => {
194
- notifyManager.batchCalls(() => {
195
- setState(unwrap(result) as unknown as TCombinedResult)
196
- })()
231
+ batch(() => {
232
+ const dataResources_ = dataResources()
233
+ for (let index = 0; index < dataResources_.length; index++) {
234
+ const dataResource = dataResources_[index]!
235
+ dataResource[1].mutate(() => unwrap(state[index]!.data))
236
+ dataResource[1].refetch()
237
+ }
197
238
  })
198
239
 
240
+ let taskQueue: Array<() => void> = []
241
+ const subscribeToObserver = () =>
242
+ observer.subscribe((result) => {
243
+ taskQueue.push(() => {
244
+ batch(() => {
245
+ const dataResources_ = dataResources()
246
+ for (let index = 0; index < dataResources_.length; index++) {
247
+ const dataResource = dataResources_[index]!
248
+ const unwrappedResult = { ...unwrap(result[index]!) }
249
+ // @ts-expect-error typescript pedantry regarding the possible range of index
250
+ setState(index, unwrap(unwrappedResult))
251
+ dataResource[1].mutate(() => unwrap(state[index]!.data))
252
+ dataResource[1].refetch()
253
+ }
254
+ })
255
+ })
256
+
257
+ queueMicrotask(() => {
258
+ const taskToRun = taskQueue.pop()
259
+ if (taskToRun) taskToRun()
260
+ taskQueue = []
261
+ })
262
+ })
263
+
264
+ let unsubscribe: () => void = () => undefined
265
+ createComputed<() => void>((cleanup) => {
266
+ cleanup?.()
267
+ unsubscribe = isRestoring() ? () => undefined : subscribeToObserver()
268
+ // cleanup needs to be scheduled after synchronous effects take place
269
+ return () => queueMicrotask(unsubscribe)
270
+ })
199
271
  onCleanup(unsubscribe)
200
272
 
273
+ onMount(() => {
274
+ observer.setQueries(
275
+ defaultedQueries(),
276
+ queriesOptions().combine
277
+ ? ({
278
+ combine: queriesOptions().combine,
279
+ } as QueriesObserverOptions<TCombinedResult>)
280
+ : undefined,
281
+ { listeners: false },
282
+ )
283
+ })
284
+
201
285
  createComputed(() => {
202
- const updatedQueries = queriesOptions().queries.map((options) => {
203
- const defaultedOptions = client.defaultQueryOptions(options)
204
- defaultedOptions._optimisticResults = 'optimistic'
205
- return defaultedOptions
206
- })
207
286
  observer.setQueries(
208
- updatedQueries,
287
+ defaultedQueries(),
209
288
  queriesOptions().combine
210
289
  ? ({
211
290
  combine: queriesOptions().combine,
@@ -215,5 +294,22 @@ export function createQueries<
215
294
  )
216
295
  })
217
296
 
218
- return state
297
+ const handler = (index: number) => ({
298
+ get(target: QueryObserverResult, prop: keyof QueryObserverResult): any {
299
+ if (prop === 'data') {
300
+ return dataResources()[index]![0]()
301
+ }
302
+ return Reflect.get(target, prop)
303
+ },
304
+ })
305
+
306
+ const getProxies = () =>
307
+ state.map((s, index) => {
308
+ return new Proxy(s, handler(index))
309
+ })
310
+
311
+ const [proxifiedState, setProxifiedState] = createStore(getProxies())
312
+ createRenderEffect(() => setProxifiedState(getProxies()))
313
+
314
+ return proxifiedState as TCombinedResult
219
315
  }
package/src/index.ts CHANGED
@@ -27,3 +27,4 @@ export { createInfiniteQuery } from './createInfiniteQuery'
27
27
  export { createMutation } from './createMutation'
28
28
  export { useIsMutating } from './useIsMutating'
29
29
  export { createQueries } from './createQueries'
30
+ export { useIsRestoring, IsRestoringProvider } from './isRestoring'
@@ -0,0 +1,6 @@
1
+ import { type Accessor, createContext, useContext } from 'solid-js'
2
+
3
+ const IsRestoringContext = createContext<Accessor<boolean>>(() => false)
4
+
5
+ export const useIsRestoring = () => useContext(IsRestoringContext)
6
+ export const IsRestoringProvider = IsRestoringContext.Provider