Skip to content

Commit 2b1939f

Browse files
feat(query): placeholder
1 parent 2c50181 commit 2b1939f

File tree

4 files changed

+87
-33
lines changed

4 files changed

+87
-33
lines changed

src/query-options.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { type InjectionKey, type MaybeRefOrGetter, inject } from 'vue'
21
import type { EntryKey } from './entry-options'
3-
import type { ErrorDefault } from './types-extension'
42
import type { PiniaColadaOptions } from './pinia-colada'
3+
import type { ErrorDefault } from './types-extension'
4+
import { inject, type InjectionKey, type MaybeRefOrGetter } from 'vue'
55

66
/**
77
* `true` refetch if data is stale (refresh()), `false` never refetch, 'always' always refetch.
@@ -79,6 +79,8 @@ export interface UseQueryOptions<TResult = unknown, TError = ErrorDefault> {
7979
// TODO: this might be just sugar syntax to do `setQueryData()` on creation
8080
initialData?: () => TResult
8181

82+
placeholderData?: TResult | ((previousData: TResult) => TResult)
83+
8284
/**
8385
* Function to type and ensure the `error` property is always an instance of `TError`.
8486
*

src/query-store.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1+
import type {
2+
AsyncStatus,
3+
DataState,
4+
DataState_Success,
5+
DataStateStatus,
6+
} from './data-state'
7+
import type { defineQuery } from './define-query'
8+
import type { EntryKey } from './entry-options'
9+
import type { UseQueryOptionsWithDefaults } from './query-options'
10+
import type { ErrorDefault } from './types-extension'
111
import { defineStore } from 'pinia'
212
import {
313
type ComponentInternalInstance,
414
type EffectScope,
5-
type ShallowRef,
615
getCurrentInstance,
716
getCurrentScope,
817
markRaw,
918
shallowReactive,
19+
type ShallowRef,
1020
shallowRef,
1121
toValue,
1222
} from 'vue'
13-
import { stringifyFlatObject, toValueWithArgs } from './utils'
1423
import { type EntryNodeKey, TreeMapNode } from './tree-map'
15-
import type { EntryKey } from './entry-options'
16-
import type { UseQueryOptionsWithDefaults } from './query-options'
17-
import type { ErrorDefault } from './types-extension'
18-
import type { defineQuery } from './define-query'
19-
import type {
20-
AsyncStatus,
21-
DataState,
22-
DataStateStatus,
23-
DataState_Success,
24-
} from './data-state'
24+
import { stringifyFlatObject, toValueWithArgs } from './utils'
2525

2626
/**
2727
* NOTE: Entries could be classes but the point of having all functions within the store is to allow plugins to hook
@@ -501,9 +501,10 @@ export const useQueryCache = defineStore(QUERY_STORE_ID, ({ action }) => {
501501
<TResult, TError>(
502502
entry: UseQueryEntry<TResult, TError>,
503503
state: DataState<TResult, TError>,
504+
resetWhen?: boolean,
504505
) => {
505506
entry.state.value = state
506-
entry.when = Date.now()
507+
if (resetWhen) entry.when = Date.now()
507508
},
508509
)
509510

src/use-query.spec.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import type { MockInstance } from 'vitest'
2-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
import type { GlobalMountOptions } from '../test/utils'
3+
import type { UseQueryOptions } from './query-options'
4+
import type { UseQueryEntry } from './query-store'
35
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
46
import { createPinia } from 'pinia'
7+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
58
import { defineComponent, nextTick, ref, shallowReactive } from 'vue'
69
import { mockWarn } from '../test/mock-warn'
7-
import type { GlobalMountOptions } from '../test/utils'
810
import { isSpy } from '../test/utils'
9-
import { useQuery } from './use-query'
10-
import type { UseQueryEntry } from './query-store'
11-
import { QUERY_STORE_ID, createQueryEntry, useQueryCache } from './query-store'
12-
import { TreeMapNode, entryNodeSize } from './tree-map'
13-
import type { UseQueryOptions } from './query-options'
1411
import { PiniaColada } from './pinia-colada'
12+
import { createQueryEntry, QUERY_STORE_ID, useQueryCache } from './query-store'
13+
import { entryNodeSize, TreeMapNode } from './tree-map'
14+
import { useQuery } from './use-query'
1515

1616
describe('useQuery', () => {
1717
beforeEach(() => {
@@ -456,6 +456,48 @@ describe('useQuery', () => {
456456
expect(query).toHaveBeenCalledTimes(1)
457457
})
458458

459+
it('uses the placeholder data if configured while refetching', async () => {
460+
const { wrapper, query } = mountDynamicKey({
461+
query: async () => {
462+
await new Promise((res) => setTimeout(res, 100))
463+
return { when: Date.now() }
464+
},
465+
staleTime: 1000,
466+
placeholderData: (data) => data,
467+
})
468+
469+
vi.advanceTimersByTime(100)
470+
await flushPromises()
471+
expect(query).toHaveBeenCalledTimes(1)
472+
const dataId0 = wrapper.vm.data
473+
474+
await wrapper.vm.setId(1)
475+
// Placeholder data is used
476+
expect(wrapper.vm.data).toBe(dataId0)
477+
478+
vi.advanceTimersByTime(100)
479+
await flushPromises()
480+
const dataId1 = wrapper.vm.data
481+
// Refetch data is used
482+
expect(dataId1).not.toBe(dataId0)
483+
484+
await wrapper.vm.setId(0)
485+
// Data is not stale: placeholder data is not used, catched data is used
486+
expect(wrapper.vm.data).toBe(dataId0)
487+
488+
vi.advanceTimersByTime(1001)
489+
490+
await wrapper.vm.setId(1)
491+
// Data is stale: placeholder data is used
492+
expect(wrapper.vm.data).toBe(dataId0)
493+
494+
vi.advanceTimersByTime(100)
495+
await flushPromises()
496+
// Refetch data is used
497+
expect(dataId1).not.toBe(dataId0)
498+
expect(query).toHaveBeenCalledTimes(3)
499+
})
500+
459501
it('refreshes the data if mounted and the key changes', async () => {
460502
const { wrapper, query } = mountDynamicKey({
461503
initialId: 0,

src/use-query.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import type { ComputedRef, MaybeRefOrGetter, ShallowRef } from 'vue'
2+
import type { AsyncStatus, DataState, DataStateStatus } from './data-state'
3+
import type { EntryKey } from './entry-options'
4+
import type {
5+
UseQueryOptions,
6+
UseQueryOptionsWithDefaults,
7+
} from './query-options'
8+
import type { UseQueryEntry } from './query-store'
9+
import type { ErrorDefault } from './types-extension'
210
import {
311
computed,
412
getCurrentInstance,
@@ -10,22 +18,14 @@ import {
1018
toValue,
1119
watch,
1220
} from 'vue'
13-
import { IS_CLIENT, useEventListener } from './utils'
14-
import type { UseQueryEntry } from './query-store'
21+
import { getCurrentDefineQueryEffect } from './define-query'
22+
import { useQueryOptions } from './query-options'
1523
import {
1624
queryEntry_addDep,
1725
queryEntry_removeDep,
1826
useQueryCache,
1927
} from './query-store'
20-
import { useQueryOptions } from './query-options'
21-
import type { EntryKey } from './entry-options'
22-
import type {
23-
UseQueryOptions,
24-
UseQueryOptionsWithDefaults,
25-
} from './query-options'
26-
import type { ErrorDefault } from './types-extension'
27-
import { getCurrentDefineQueryEffect } from './define-query'
28-
import type { AsyncStatus, DataState, DataStateStatus } from './data-state'
28+
import { IS_CLIENT, useEventListener } from './utils'
2929

3030
/**
3131
* Return type of `useQuery()`.
@@ -200,6 +200,15 @@ export function useQuery<TResult, TError = ErrorDefault>(
200200
entry,
201201
(entry, previousEntry) => {
202202
if (!isActive) return
203+
if (entry.stale && entry.options?.placeholderData && previousEntry?.state.value.status === 'success') {
204+
cacheEntries.setEntryState(entry, {
205+
// TODO: pending?
206+
status: 'success',
207+
// @ts-expect-error: TODO: fix this
208+
data: (typeof entry.options.placeholderData === 'function') ? entry.options.placeholderData(previousEntry.state.value.data) : previousEntry.state.value.data,
209+
error: null,
210+
}, false)
211+
}
203212
if (previousEntry) {
204213
queryEntry_removeDep(previousEntry, hasCurrentInstance, cacheEntries)
205214
queryEntry_removeDep(previousEntry, currentEffect, cacheEntries)

0 commit comments

Comments
 (0)