@pinia/colada
Advanced tools
Comparing version 0.9.1 to 0.10.0
import * as vue from 'vue'; | ||
import { ComputedRef, ShallowRef, EffectScope, ComponentInternalInstance, App, MaybeRefOrGetter, Ref } from 'vue'; | ||
import { ComputedRef, ShallowRef, App, MaybeRefOrGetter, EffectScope, ComponentInternalInstance, Ref } from 'vue'; | ||
import * as pinia from 'pinia'; | ||
@@ -104,2 +104,22 @@ import { Pinia } from 'pinia'; | ||
/** | ||
* Context passed to a Pinia Colada plugin. | ||
*/ | ||
interface PiniaColadaPluginContext { | ||
/** | ||
* The query cache used by the application. | ||
*/ | ||
cache: ReturnType<typeof useQueryCache>; | ||
/** | ||
* The Pinia instance used by the application. | ||
*/ | ||
pinia: Pinia; | ||
} | ||
/** | ||
* A Pinia Colada plugin. | ||
*/ | ||
interface PiniaColadaPlugin { | ||
(context: PiniaColadaPluginContext): void; | ||
} | ||
/** | ||
* The status of data. | ||
@@ -149,193 +169,207 @@ * - `pending`: initial state | ||
type _MutationKey<TVars> = EntryKey | ((vars: TVars) => EntryKey); | ||
/** | ||
* The keys to invalidate when a mutation succeeds. | ||
* @internal | ||
* Return type of `useQuery()`. | ||
*/ | ||
type _MutationKeys<TVars, TResult> = EntryKey[] | ((data: TResult, vars: TVars) => EntryKey[]); | ||
/** | ||
* Removes the nullish types from the context type to make `A & TContext` work instead of yield `never`. | ||
* @internal | ||
*/ | ||
type _ReduceContext<TContext> = TContext extends void | null | undefined ? _EmptyObject : TContext; | ||
/** | ||
* Context object returned by a global `onMutate` function that is merged with the context returned by a local | ||
* `onMutate`. | ||
* @example | ||
* ```ts | ||
* declare module '@pinia/colada' { | ||
* export interface UseMutationGlobalContext { | ||
* router: Router // from vue-router | ||
* } | ||
* } | ||
* | ||
* // add the `router` to the context | ||
* app.use(MutationPlugin, { | ||
* onMutate() { | ||
* return { router } | ||
* }, | ||
* }) | ||
* ``` | ||
*/ | ||
interface UseMutationGlobalContext { | ||
} | ||
interface UseMutationOptions<TResult = unknown, TVars = void, TError = ErrorDefault, TContext extends Record<any, any> | void | null = void> { | ||
interface UseQueryReturn<TResult = unknown, TError = ErrorDefault> { | ||
/** | ||
* The key of the mutation. If the mutation is successful, it will invalidate the query with the same key and refetch it | ||
* The state of the query. Contains its data, error, and status. | ||
*/ | ||
mutation: (vars: TVars, context: NoInfer<TContext>) => Promise<TResult>; | ||
key?: _MutationKey<TVars>; | ||
state: ComputedRef<DataState<TResult, TError>>; | ||
/** | ||
* Keys to invalidate if the mutation succeeds so that `useQuery()` refetch if used. | ||
* Status of the query. Becomes `'loading'` while the query is being fetched, is `'idle'` otherwise. | ||
*/ | ||
keys?: _MutationKeys<TVars, TResult>; | ||
asyncStatus: ComputedRef<AsyncStatus>; | ||
/** | ||
* Runs before the mutation is executed. **It should be placed before `mutation()` for `context` to be inferred**. It | ||
* can return a value that will be passed to `mutation`, `onSuccess`, `onError` and `onSettled`. If it returns a | ||
* promise, it will be awaited before running `mutation`. | ||
* | ||
* @example | ||
* ```ts | ||
* useMutation({ | ||
* // must appear before `mutation` for `{ foo: string }` to be inferred | ||
* // within `mutation` | ||
* onMutate() { | ||
* return { foo: 'bar' } | ||
* }, | ||
* mutation: (id: number, { foo }) => { | ||
* console.log(foo) // bar | ||
* return fetch(`/api/todos/${id}`) | ||
* }, | ||
* onSuccess(context) { | ||
* console.log(context.foo) // bar | ||
* }, | ||
* }) | ||
* ``` | ||
* The last successful data resolved by the query. | ||
*/ | ||
onMutate?: (vars: TVars) => _Awaitable<TContext>; | ||
data: ShallowRef<TResult | undefined>; | ||
/** | ||
* Runs if the mutation encounters an error. | ||
* The error rejected by the query. | ||
*/ | ||
onError?: (context: { | ||
error: TError; | ||
vars: TVars; | ||
} & UseMutationGlobalContext & _ReduceContext<TContext>) => unknown; | ||
error: ShallowRef<TError | null>; | ||
/** | ||
* Runs if the mutation is successful. | ||
* The status of the query. | ||
* @see {@link DataStateStatus} | ||
*/ | ||
onSuccess?: (context: { | ||
data: TResult; | ||
vars: TVars; | ||
} & UseMutationGlobalContext & _ReduceContext<TContext>) => unknown; | ||
status: ShallowRef<DataStateStatus>; | ||
/** | ||
* Runs after the mutation is settled, regardless of the result. | ||
* Returns whether the request is still pending its first call. Alias for `status.value === 'pending'` | ||
*/ | ||
onSettled?: (context: { | ||
data: TResult | undefined; | ||
error: TError | undefined; | ||
vars: TVars; | ||
} & UseMutationGlobalContext & _ReduceContext<TContext>) => unknown; | ||
} | ||
interface UseMutationReturn<TResult, TVars, TError> { | ||
isPending: ComputedRef<boolean>; | ||
/** | ||
* The combined state of the mutation. Contains its data, error, and status. It enables type narrowing based on the {@link UseMutationReturn.status}. | ||
* Returns whether the `data` is the `placeholderData`. | ||
*/ | ||
state: ComputedRef<DataState<TResult, TError>>; | ||
isPlaceholderData: ComputedRef<boolean>; | ||
/** | ||
* The status of the mutation. | ||
* @see {@link DataStateStatus} | ||
* Returns whether the request is currently fetching data. | ||
*/ | ||
status: ShallowRef<DataStateStatus>; | ||
isLoading: ShallowRef<boolean>; | ||
/** | ||
* Status of the mutation. Becomes `'loading'` while the query is being fetched, is `'idle'` otherwise. | ||
* Ensures the current data is fresh. If the data is stale, refetch, if not return as is. | ||
* @returns a promise that resolves when the refresh is done | ||
*/ | ||
asyncStatus: ShallowRef<AsyncStatus>; | ||
refresh: () => Promise<DataState<TResult, TError>>; | ||
/** | ||
* The result of the mutation. `undefined` if the mutation has not been called yet. | ||
* Ignores fresh data and triggers a new fetch | ||
* @returns a promise that resolves when the fetch is done | ||
*/ | ||
data: ShallowRef<TResult | undefined>; | ||
refetch: () => Promise<DataState<TResult, TError>>; | ||
} | ||
/** | ||
* Ensures and return a shared query state based on the `key` option. | ||
* | ||
* @param _options - The options of the query | ||
*/ | ||
declare function useQuery<TResult, TError = ErrorDefault>(_options: UseQueryOptions<TResult, TError>): UseQueryReturn<TResult, TError>; | ||
/** | ||
* Options for the Pinia Colada plugin. | ||
*/ | ||
interface PiniaColadaOptions extends Omit<UseQueryOptions, 'key' | 'query' | 'initialData' | 'transformError' | 'placeholderData'> { | ||
/** | ||
* The error of the mutation. `null` if the mutation has not been called yet or if it was successful. | ||
* Pinia instance to use. This is only needed if installing before the Pinia plugin. | ||
*/ | ||
error: ShallowRef<TError | null>; | ||
pinia?: Pinia; | ||
/** | ||
* Whether the mutation is currently executing. | ||
* Pinia Colada plugins to install. | ||
*/ | ||
isLoading: ComputedRef<boolean>; | ||
plugins?: PiniaColadaPlugin[]; | ||
/** | ||
* The variables passed to the mutation. | ||
*/ | ||
variables: ShallowRef<TVars | undefined>; | ||
/** | ||
* Calls the mutation and returns a promise with the result. | ||
* Function to ensure the `error` property is always an instance of the default global type error. Defaults to the | ||
* identity function. | ||
* | ||
* @param vars - parameters to pass to the mutation | ||
* @param error - error thrown | ||
*/ | ||
mutateAsync: unknown | void extends TVars ? () => Promise<TResult> : (vars: TVars) => Promise<TResult>; | ||
transformError?: (error: unknown) => ErrorDefault; | ||
/** | ||
* Calls the mutation without returning a promise to avoid unhandled promise rejections. | ||
* Executes setup code inside `useQuery()` to add custom behavior to all queries. **Must be synchronous**. | ||
* @experimental still going through testing to see what is needed | ||
* | ||
* @param args - parameters to pass to the mutation | ||
* @param context - properties of the `useQuery` return value and the options | ||
*/ | ||
mutate: (...args: unknown | void extends TVars ? [] : [vars: TVars]) => void; | ||
/** | ||
* Resets the state of the mutation to its initial state. | ||
*/ | ||
reset: () => void; | ||
setup?: <TResult = unknown, TError = unknown>(useQueryReturn: UseQueryReturn<TResult, TError>, options: UseQueryOptionsWithDefaults<TResult, TError>) => UseQueryReturn<TResult, TError>; | ||
} | ||
/** | ||
* Setups a mutation. | ||
* Plugin that installs the Query and Mutation plugins alongside some extra plugins. | ||
* | ||
* @param options - Options to create the mutation | ||
* @example | ||
* ```ts | ||
* const { mutate, status, error } = useMutation({ | ||
* mutation: (id: number) => fetch(`/api/todos/${id}`), | ||
* onSuccess({ queryClient }) { | ||
* queryClient.invalidateQueries('todos') | ||
* }, | ||
* }) | ||
* ``` | ||
* @see {@link PiniaColada} to only install the Query plugin. | ||
* @see {@link MutationPlugin} to only install the Query plugin. | ||
* | ||
* @param app - Vue App | ||
* @param options - Pinia Colada options | ||
*/ | ||
declare function useMutation<TResult, TVars = void, TError = ErrorDefault, TContext extends Record<any, any> | void | null = void>(options: UseMutationOptions<TResult, TVars, TError, TContext>): UseMutationReturn<TResult, TVars, TError>; | ||
declare function PiniaColada(app: App, options?: PiniaColadaOptions): void; | ||
/** | ||
* Define a mutation with the given options. Similar to `useMutation(options)` but allows you to reuse the mutation in | ||
* multiple places. | ||
* | ||
* @param options - the options to define the mutation | ||
* @example | ||
* ```ts | ||
* const useCreateTodo = defineMutation({ | ||
* mutation: (todoText: string) => | ||
* fetch('/api/todos', { | ||
* method: 'POST', | ||
* body: JSON.stringify({ text: todoText }), | ||
* }), | ||
* }) | ||
* ``` | ||
* `true` refetch if data is stale (refresh()), `false` never refetch, 'always' always refetch. | ||
*/ | ||
declare function defineMutation<TResult, TVars = void, TError = ErrorDefault, TContext extends Record<any, any> | void | null = void>(options: UseMutationOptions<TResult, TVars, TError, TContext>): () => UseMutationReturn<TResult, TVars, TError>; | ||
type _RefetchOnControl = boolean | 'always'; | ||
/** | ||
* Define a mutation with a function setup. Allows to return arbitrary values from the mutation function, create | ||
* contextual refs, rename the returned values, etc. | ||
* Context object passed to the `query` function of `useQuery()`. | ||
* @see {@link UseQueryOptions} | ||
*/ | ||
interface UseQueryFnContext { | ||
/** | ||
* `AbortSignal` instance attached to the query call. If the call becomes outdated (e.g. due to a new call with the | ||
* same key), the signal will be aborted. | ||
*/ | ||
signal: AbortSignal; | ||
} | ||
/** | ||
* Options for `useQuery()`. Can be extended by plugins. | ||
* | ||
* @param setup - a function to setup the mutation | ||
* @example | ||
* ```ts | ||
* const useCreateTodo = defineMutation(() => { | ||
* const todoText = ref('') | ||
* const { data, mutate, ...rest } = useMutation({ | ||
* mutation: () => | ||
* fetch('/api/todos', { | ||
* method: 'POST', | ||
* body: JSON.stringify({ text: todoText.value }), | ||
* }), | ||
* }) | ||
* // expose the todoText ref and rename other methods for convenience | ||
* return { ...rest, createTodo: mutate, todo: data, todoText } | ||
* }) | ||
* // use-query-plugin.d.ts | ||
* export {} // needed | ||
* declare module '@pinia/colada' { | ||
* interface UseQueryOptions { | ||
* // Whether to refresh the data when the component is mounted. | ||
* refreshOnMount?: boolean | ||
* } | ||
* } | ||
* ``` | ||
*/ | ||
declare function defineMutation<T>(setup: () => T): () => T; | ||
interface UseQueryOptions<TResult = unknown, TError = ErrorDefault> { | ||
/** | ||
* The key used to identify the query. Array of primitives **without** reactive values or a reactive array or getter. | ||
* It should be treaded as an array of dependencies of your queries, e.g. if you use the `route.params.id` property, | ||
* it should also be part of the key: | ||
* | ||
* ```ts | ||
* import { useRoute } from 'vue-router' | ||
* import { useQuery } from '@pinia/colada' | ||
* | ||
* const route = useRoute() | ||
* const { data } = useQuery({ | ||
* // pass a getter function (or computed, ref, etc.) to ensure reactivity | ||
* key: () => ['user', route.params.id], | ||
* query: () => fetchUser(route.params.id), | ||
* }) | ||
* ``` | ||
*/ | ||
key: MaybeRefOrGetter<EntryKey>; | ||
/** | ||
* The function that will be called to fetch the data. It **must** be async. | ||
*/ | ||
query: (context: UseQueryFnContext) => Promise<TResult>; | ||
/** | ||
* Whether the query should be enabled or not. If `false`, the query will not be executed until `refetch()` or | ||
* `refresh()` is called. If it becomes `true`, the query will be refreshed. | ||
*/ | ||
enabled?: MaybeRefOrGetter<boolean>; | ||
/** | ||
* Time in ms after which the data is considered stale and will be refreshed on next read. | ||
* @default 5000 (5 seconds) | ||
*/ | ||
staleTime?: number; | ||
/** | ||
* Time in ms after which, once the data is no longer being used, it will be garbage collected to free resources. | ||
* @default 300000 (5 minutes) | ||
*/ | ||
gcTime?: number; | ||
/** | ||
* The data which is initially set to the query while the query is loading for the first time. | ||
* Note: unlike with `placeholderData`, setting the initial data changes the state of the query (it will be set to `success`). | ||
*/ | ||
initialData?: () => NoInfer<TResult>; | ||
/** | ||
* A placeholder data that is initially shown while the query is loading for the first time. This will also show the | ||
* `status` as `success` until the query finishes loading (no matter the outcome of the query). Note: unlike with | ||
* `initialData`, the placeholder does not change the cache state. | ||
*/ | ||
placeholderData?: NoInfer<TResult> | (<T extends TResult>(previousData: T | undefined) => NoInfer<TResult> | null | undefined | void); | ||
/** | ||
* Function to type and ensure the `error` property is always an instance of `TError`. | ||
* | ||
* @param error - error thrown | ||
* @example | ||
* ```ts | ||
* useQuery({ | ||
* key: ['user', id], | ||
* query: () => fetchUser(id), | ||
* transformError: (error): MyCustomError | UnexpectedError => | ||
* // this assumes both `MyCustomError` and a `UnexpectedError` are valid error classes | ||
* error instanceof MyCustomError ? error : new UnexpectedError(error), | ||
* }) | ||
* ``` | ||
*/ | ||
transformError?: (error: unknown) => TError; | ||
refetchOnMount?: _RefetchOnControl; | ||
refetchOnWindowFocus?: _RefetchOnControl; | ||
refetchOnReconnect?: _RefetchOnControl; | ||
} | ||
/** | ||
* Default options for `useQuery()`. Modifying this object will affect all the queries that don't override these | ||
*/ | ||
declare const USE_QUERY_DEFAULTS: { | ||
staleTime: number; | ||
gcTime: number; | ||
refetchOnWindowFocus: _RefetchOnControl; | ||
refetchOnReconnect: _RefetchOnControl; | ||
refetchOnMount: _RefetchOnControl; | ||
enabled: MaybeRefOrGetter<boolean>; | ||
transformError: (error: unknown) => any; | ||
}; | ||
type UseQueryOptionsWithDefaults<TResult = unknown, TError = ErrorDefault> = UseQueryOptions<TResult, TError> & typeof USE_QUERY_DEFAULTS; | ||
@@ -438,3 +472,3 @@ /** | ||
type DefineQueryEntry = [entries: UseQueryEntry[], returnValue: unknown]; | ||
declare const useQueryCache: pinia.StoreDefinition<"_pc_query", pinia._UnwrapAll<Pick<{ | ||
declare const useQueryCache: pinia.StoreDefinition<"_pc_query", Pick<{ | ||
caches: vue.ShallowReactive<TreeMapNode<UseQueryEntry<unknown, unknown>>>; | ||
@@ -445,3 +479,3 @@ ensureDefinedQuery: <T>(fn: () => T) => DefineQueryEntry; | ||
cancelQuery: (entry: UseQueryEntry, reason?: unknown) => void; | ||
invalidateQueries: (filters?: UseQueryEntryFilter) => Promise<DataState<unknown, unknown>[]>; | ||
invalidateQueries: (filters?: UseQueryEntryFilter) => Promise<unknown>; | ||
invalidate: (entry: UseQueryEntry) => void; | ||
@@ -454,3 +488,3 @@ fetch: <TResult, TError>(entry: UseQueryEntry<TResult, TError>) => Promise<DataState<TResult, TError>>; | ||
getEntries: (filters?: UseQueryEntryFilter) => UseQueryEntry[]; | ||
}, "caches">>, Pick<{ | ||
}, "caches">, Pick<{ | ||
caches: vue.ShallowReactive<TreeMapNode<UseQueryEntry<unknown, unknown>>>; | ||
@@ -461,3 +495,3 @@ ensureDefinedQuery: <T>(fn: () => T) => DefineQueryEntry; | ||
cancelQuery: (entry: UseQueryEntry, reason?: unknown) => void; | ||
invalidateQueries: (filters?: UseQueryEntryFilter) => Promise<DataState<unknown, unknown>[]>; | ||
invalidateQueries: (filters?: UseQueryEntryFilter) => Promise<unknown>; | ||
invalidate: (entry: UseQueryEntry) => void; | ||
@@ -476,3 +510,3 @@ fetch: <TResult, TError>(entry: UseQueryEntry<TResult, TError>) => Promise<DataState<TResult, TError>>; | ||
cancelQuery: (entry: UseQueryEntry, reason?: unknown) => void; | ||
invalidateQueries: (filters?: UseQueryEntryFilter) => Promise<DataState<unknown, unknown>[]>; | ||
invalidateQueries: (filters?: UseQueryEntryFilter) => Promise<unknown>; | ||
invalidate: (entry: UseQueryEntry) => void; | ||
@@ -487,2 +521,6 @@ fetch: <TResult, TError>(entry: UseQueryEntry<TResult, TError>) => Promise<DataState<TResult, TError>>; | ||
/** | ||
* The cache of the queries. It's the store returned by {@link useQueryCache}. | ||
*/ | ||
type QueryCache = ReturnType<typeof useQueryCache>; | ||
/** | ||
* Raw data of a query entry. Can be serialized from the server and used to hydrate the store. | ||
@@ -532,226 +570,242 @@ * @internal | ||
/** | ||
* Context passed to a Pinia Colada plugin. | ||
* Valid keys for a mutation. Similar to query keys. | ||
* @see {@link EntryKey} | ||
* @internal | ||
*/ | ||
interface PiniaColadaPluginContext { | ||
/** | ||
* The query cache used by the application. | ||
*/ | ||
cache: ReturnType<typeof useQueryCache>; | ||
/** | ||
* The Pinia instance used by the application. | ||
*/ | ||
pinia: Pinia; | ||
type _MutationKey<TVars> = EntryKey | ((vars: TVars) => EntryKey); | ||
/** | ||
* Removes the nullish types from the context type to make `A & TContext` work instead of yield `never`. | ||
* @internal | ||
*/ | ||
type _ReduceContext<TContext> = TContext extends void | null | undefined ? _EmptyObject : Record<any, any> extends TContext ? _EmptyObject : TContext; | ||
/** | ||
* Context object returned by a global `onMutate` function that is merged with the context returned by a local | ||
* `onMutate`. | ||
* @example | ||
* ```ts | ||
* declare module '@pinia/colada' { | ||
* export interface UseMutationGlobalContext { | ||
* router: Router // from vue-router | ||
* } | ||
* } | ||
* | ||
* // add the `router` to the context | ||
* app.use(MutationPlugin, { | ||
* onMutate() { | ||
* return { router } | ||
* }, | ||
* }) | ||
* ``` | ||
*/ | ||
interface UseMutationGlobalContext { | ||
} | ||
/** | ||
* A Pinia Colada plugin. | ||
* Context object passed to the different hooks of a mutation like `onMutate`, `onSuccess`, `onError` and `onSettled`. | ||
*/ | ||
interface PiniaColadaPlugin { | ||
(context: PiniaColadaPluginContext): void; | ||
interface UseMutationHooksContext { | ||
/** | ||
* The global query cache store. | ||
*/ | ||
caches: QueryCache; | ||
} | ||
/** | ||
* Options for the Pinia Colada plugin. | ||
* Options to create a mutation. | ||
*/ | ||
interface PiniaColadaOptions extends Omit<UseQueryOptions, 'key' | 'query' | 'initialData' | 'transformError' | 'placeholderData'> { | ||
interface UseMutationOptions<TResult = unknown, TVars = void, TError = ErrorDefault, TContext extends Record<any, any> = _EmptyObject> { | ||
/** | ||
* Pinia instance to use. This is only needed if installing before the Pinia plugin. | ||
* The key of the mutation. If the mutation is successful, it will invalidate the query with the same key and refetch it | ||
*/ | ||
pinia?: Pinia; | ||
mutation: (vars: TVars, context: _ReduceContext<NoInfer<TContext>>) => Promise<TResult>; | ||
/** | ||
* Pinia Colada plugins to install. | ||
* Optional key to identify the mutation globally and access it through other helpers like `useMutationState()`. | ||
*/ | ||
plugins?: PiniaColadaPlugin[]; | ||
key?: _MutationKey<NoInfer<TVars>>; | ||
/** | ||
* Function to ensure the `error` property is always an instance of the default global type error. Defaults to the | ||
* identity function. | ||
* Runs before the mutation is executed. **It should be placed before `mutation()` for `context` to be inferred**. It | ||
* can return a value that will be passed to `mutation`, `onSuccess`, `onError` and `onSettled`. If it returns a | ||
* promise, it will be awaited before running `mutation`. | ||
* | ||
* @param error - error thrown | ||
* @example | ||
* ```ts | ||
* useMutation({ | ||
* // must appear before `mutation` for `{ foo: string }` to be inferred | ||
* // within `mutation` | ||
* onMutate() { | ||
* return { foo: 'bar' } | ||
* }, | ||
* mutation: (id: number, { foo }) => { | ||
* console.log(foo) // bar | ||
* return fetch(`/api/todos/${id}`) | ||
* }, | ||
* onSuccess(context) { | ||
* console.log(context.foo) // bar | ||
* }, | ||
* }) | ||
* ``` | ||
*/ | ||
transformError?: (error: unknown) => ErrorDefault; | ||
onMutate?: ( | ||
/** | ||
* Executes setup code inside `useQuery()` to add custom behavior to all queries. **Must be synchronous**. | ||
* @experimental still going through testing to see what is needed | ||
* | ||
* @param context - properties of the `useQuery` return value and the options | ||
* The variables passed to the mutation. | ||
*/ | ||
setup?: <TResult = unknown, TError = unknown>(useQueryReturn: UseQueryReturn<TResult, TError>, options: UseQueryOptionsWithDefaults<TResult, TError>) => UseQueryReturn<TResult, TError>; | ||
} | ||
/** | ||
* Plugin that installs the Query and Mutation plugins alongside some extra plugins. | ||
* | ||
* @see {@link PiniaColada} to only install the Query plugin. | ||
* @see {@link MutationPlugin} to only install the Query plugin. | ||
* | ||
* @param app - Vue App | ||
* @param options - Pinia Colada options | ||
*/ | ||
declare function PiniaColada(app: App, options?: PiniaColadaOptions): void; | ||
/** | ||
* `true` refetch if data is stale (refresh()), `false` never refetch, 'always' always refetch. | ||
*/ | ||
type _RefetchOnControl = boolean | 'always'; | ||
/** | ||
* Context object passed to the `query` function of `useQuery()`. | ||
* @see {@link UseQueryOptions} | ||
*/ | ||
interface UseQueryFnContext { | ||
vars: NoInfer<TVars>, context: // always defined | ||
UseMutationHooksContext & UseMutationGlobalContext) => _Awaitable<TContext | undefined | void | null>; | ||
/** | ||
* `AbortSignal` instance attached to the query call. If the call becomes outdated (e.g. due to a new call with the | ||
* same key), the signal will be aborted. | ||
* Runs if the mutation is successful. | ||
*/ | ||
signal: AbortSignal; | ||
} | ||
/** | ||
* Options for `useQuery()`. Can be extended by plugins. | ||
* | ||
* @example | ||
* ```ts | ||
* // use-query-plugin.d.ts | ||
* export {} // needed | ||
* declare module '@pinia/colada' { | ||
* interface UseQueryOptions { | ||
* // Whether to refresh the data when the component is mounted. | ||
* refreshOnMount?: boolean | ||
* } | ||
* } | ||
* ``` | ||
*/ | ||
interface UseQueryOptions<TResult = unknown, TError = ErrorDefault> { | ||
onSuccess?: ( | ||
/** | ||
* The key used to identify the query. Array of primitives **without** reactive values or a reactive array or getter. | ||
* It should be treaded as an array of dependencies of your queries, e.g. if you use the `route.params.id` property, | ||
* it should also be part of the key: | ||
* | ||
* ```ts | ||
* import { useRoute } from 'vue-router' | ||
* import { useQuery } from '@pinia/colada' | ||
* | ||
* const route = useRoute() | ||
* const { data } = useQuery({ | ||
* // pass a getter function (or computed, ref, etc.) to ensure reactivity | ||
* key: () => ['user', route.params.id], | ||
* query: () => fetchUser(route.params.id), | ||
* }) | ||
* ``` | ||
* The result of the mutation. | ||
*/ | ||
key: MaybeRefOrGetter<EntryKey>; | ||
data: NoInfer<TResult>, | ||
/** | ||
* The function that will be called to fetch the data. It **must** be async. | ||
* The variables passed to the mutation. | ||
*/ | ||
query: (context: UseQueryFnContext) => Promise<TResult>; | ||
vars: NoInfer<TVars>, | ||
/** | ||
* Whether the query should be enabled or not. If `false`, the query will not be executed until `refetch()` or | ||
* `refresh()` is called. If it becomes `true`, the query will be refreshed. | ||
* The merged context from `onMutate` and the global context. | ||
*/ | ||
enabled?: MaybeRefOrGetter<boolean>; | ||
context: UseMutationGlobalContext & UseMutationHooksContext & _ReduceContext<NoInfer<TContext>>) => unknown; | ||
/** | ||
* Time in ms after which the data is considered stale and will be refreshed on next read. | ||
* Runs if the mutation encounters an error. | ||
*/ | ||
staleTime?: number; | ||
onError?: ( | ||
/** | ||
* Time in ms after which, once the data is no longer being used, it will be garbage collected to free resources. | ||
* The error thrown by the mutation. | ||
*/ | ||
gcTime?: number; | ||
error: NoInfer<TError>, | ||
/** | ||
* The data which is initially set to the query while the query is loading for the first time. | ||
* Note: unlike with `placeholderData`, setting the initial data changes the state of the query (it will be set to `success`). | ||
* The variables passed to the mutation. | ||
*/ | ||
initialData?: () => NoInfer<TResult>; | ||
vars: NoInfer<TVars>, | ||
/** | ||
* A placeholder data that is initially shown while the query is loading for the first time. This will also show the | ||
* `status` as `success` until the query finishes loading (no matter the outcome of the query). Note: unlike with | ||
* `initialData`, the placeholder does not change the cache state. | ||
* The merged context from `onMutate` and the global context. Properties returned by `onMutate` can be `undefined` | ||
* if `onMutate` throws. | ||
*/ | ||
placeholderData?: NoInfer<TResult> | (<T extends TResult>(previousData: T | undefined) => NoInfer<TResult> | null | undefined | void); | ||
context: (UseMutationHooksContext & Partial<Record<keyof UseMutationGlobalContext, never>> & Partial<Record<keyof _ReduceContext<NoInfer<TContext>>, never>>) | (UseMutationHooksContext & UseMutationGlobalContext & _ReduceContext<NoInfer<TContext>>)) => unknown; | ||
/** | ||
* Function to type and ensure the `error` property is always an instance of `TError`. | ||
* | ||
* @param error - error thrown | ||
* @example | ||
* ```ts | ||
* useQuery({ | ||
* key: ['user', id], | ||
* query: () => fetchUser(id), | ||
* transformError: (error): MyCustomError | UnexpectedError => | ||
* // this assumes both `MyCustomError` and a `UnexpectedError` are valid error classes | ||
* error instanceof MyCustomError ? error : new UnexpectedError(error), | ||
* }) | ||
* ``` | ||
* Runs after the mutation is settled, regardless of the result. | ||
*/ | ||
transformError?: (error: unknown) => TError; | ||
refetchOnMount?: _RefetchOnControl; | ||
refetchOnWindowFocus?: _RefetchOnControl; | ||
refetchOnReconnect?: _RefetchOnControl; | ||
onSettled?: ( | ||
/** | ||
* The result of the mutation. `undefined` if the mutation failed. | ||
*/ | ||
data: NoInfer<TResult> | undefined, | ||
/** | ||
* The error thrown by the mutation. `undefined` if the mutation was successful. | ||
*/ | ||
error: NoInfer<TError> | undefined, | ||
/** | ||
* The variables passed to the mutation. | ||
*/ | ||
vars: NoInfer<TVars>, | ||
/** | ||
* The merged context from `onMutate` and the global context. Properties returned by `onMutate` can be `undefined` | ||
* if `onMutate` throws. | ||
*/ | ||
context: (UseMutationHooksContext & Partial<Record<keyof UseMutationGlobalContext, never>> & Partial<Record<keyof _ReduceContext<NoInfer<TContext>>, never>>) | (UseMutationHooksContext & UseMutationGlobalContext & _ReduceContext<NoInfer<TContext>>)) => unknown; | ||
} | ||
/** | ||
* Default options for `useQuery()`. Modifying this object will affect all the queries that don't override these | ||
*/ | ||
declare const USE_QUERY_DEFAULTS: { | ||
staleTime: number; | ||
gcTime: number; | ||
refetchOnWindowFocus: _RefetchOnControl; | ||
refetchOnReconnect: _RefetchOnControl; | ||
refetchOnMount: _RefetchOnControl; | ||
enabled: MaybeRefOrGetter<boolean>; | ||
transformError: (error: unknown) => any; | ||
}; | ||
type UseQueryOptionsWithDefaults<TResult = unknown, TError = ErrorDefault> = UseQueryOptions<TResult, TError> & typeof USE_QUERY_DEFAULTS; | ||
/** | ||
* Return type of `useQuery()`. | ||
*/ | ||
interface UseQueryReturn<TResult = unknown, TError = ErrorDefault> { | ||
interface UseMutationReturn<TResult, TVars, TError> { | ||
/** | ||
* The state of the query. Contains its data, error, and status. | ||
* The combined state of the mutation. Contains its data, error, and status. It enables type narrowing based on the {@link UseMutationReturn.status}. | ||
*/ | ||
state: ComputedRef<DataState<TResult, TError>>; | ||
/** | ||
* Status of the query. Becomes `'loading'` while the query is being fetched, is `'idle'` otherwise. | ||
* The status of the mutation. | ||
* @see {@link DataStateStatus} | ||
*/ | ||
asyncStatus: ComputedRef<AsyncStatus>; | ||
status: ShallowRef<DataStateStatus>; | ||
/** | ||
* The last successful data resolved by the query. | ||
* Status of the mutation. Becomes `'loading'` while the query is being fetched, is `'idle'` otherwise. | ||
*/ | ||
asyncStatus: ShallowRef<AsyncStatus>; | ||
/** | ||
* The result of the mutation. `undefined` if the mutation has not been called yet. | ||
*/ | ||
data: ShallowRef<TResult | undefined>; | ||
/** | ||
* The error rejected by the query. | ||
* The error of the mutation. `null` if the mutation has not been called yet or if it was successful. | ||
*/ | ||
error: ShallowRef<TError | null>; | ||
/** | ||
* The status of the query. | ||
* @see {@link DataStateStatus} | ||
* Whether the mutation is currently executing. | ||
*/ | ||
status: ShallowRef<DataStateStatus>; | ||
isLoading: ComputedRef<boolean>; | ||
/** | ||
* Returns whether the request is still pending its first call. Alias for `status.value === 'pending'` | ||
* The variables passed to the mutation. | ||
*/ | ||
isPending: ComputedRef<boolean>; | ||
variables: ShallowRef<TVars | undefined>; | ||
/** | ||
* Returns whether the `data` is the `placeholderData`. | ||
* Calls the mutation and returns a promise with the result. | ||
* | ||
* @param vars - parameters to pass to the mutation | ||
*/ | ||
isPlaceholderData: ComputedRef<boolean>; | ||
mutateAsync: unknown | void extends TVars ? () => Promise<TResult> : (vars: TVars) => Promise<TResult>; | ||
/** | ||
* Returns whether the request is currently fetching data. | ||
* Calls the mutation without returning a promise to avoid unhandled promise rejections. | ||
* | ||
* @param args - parameters to pass to the mutation | ||
*/ | ||
isLoading: ShallowRef<boolean>; | ||
mutate: (...args: unknown | void extends TVars ? [] : [vars: TVars]) => void; | ||
/** | ||
* Ensures the current data is fresh. If the data is stale, refetch, if not return as is. | ||
* @returns a promise that resolves when the refresh is done | ||
* Resets the state of the mutation to its initial state. | ||
*/ | ||
refresh: () => Promise<DataState<TResult, TError>>; | ||
/** | ||
* Ignores fresh data and triggers a new fetch | ||
* @returns a promise that resolves when the fetch is done | ||
*/ | ||
refetch: () => Promise<DataState<TResult, TError>>; | ||
reset: () => void; | ||
} | ||
/** | ||
* Ensures and return a shared query state based on the `key` option. | ||
* Setups a mutation. | ||
* | ||
* @param _options - The options of the query | ||
* @param options - Options to create the mutation | ||
* @example | ||
* ```ts | ||
* const { mutate, status, error } = useMutation({ | ||
* mutation: (id: number) => fetch(`/api/todos/${id}`), | ||
* onSuccess({ queryClient }) { | ||
* queryClient.invalidateQueries('todos') | ||
* }, | ||
* }) | ||
* ``` | ||
*/ | ||
declare function useQuery<TResult, TError = ErrorDefault>(_options: UseQueryOptions<TResult, TError>): UseQueryReturn<TResult, TError>; | ||
declare function useMutation<TResult, TVars = void, TError = ErrorDefault, TContext extends Record<any, any> = _EmptyObject>(options: UseMutationOptions<TResult, TVars, TError, TContext>): UseMutationReturn<TResult, TVars, TError>; | ||
/** | ||
* Define a mutation with the given options. Similar to `useMutation(options)` but allows you to reuse the mutation in | ||
* multiple places. | ||
* | ||
* @param options - the options to define the mutation | ||
* @example | ||
* ```ts | ||
* const useCreateTodo = defineMutation({ | ||
* mutation: (todoText: string) => | ||
* fetch('/api/todos', { | ||
* method: 'POST', | ||
* body: JSON.stringify({ text: todoText }), | ||
* }), | ||
* }) | ||
* ``` | ||
*/ | ||
declare function defineMutation<TResult, TVars = void, TError = ErrorDefault, TContext extends Record<any, any> = _EmptyObject>(options: UseMutationOptions<TResult, TVars, TError, TContext>): () => UseMutationReturn<TResult, TVars, TError>; | ||
/** | ||
* Define a mutation with a function setup. Allows to return arbitrary values from the mutation function, create | ||
* contextual refs, rename the returned values, etc. | ||
* | ||
* @param setup - a function to setup the mutation | ||
* @example | ||
* ```ts | ||
* const useCreateTodo = defineMutation(() => { | ||
* const todoText = ref('') | ||
* const { data, mutate, ...rest } = useMutation({ | ||
* mutation: () => | ||
* fetch('/api/todos', { | ||
* method: 'POST', | ||
* body: JSON.stringify({ text: todoText.value }), | ||
* }), | ||
* }) | ||
* // expose the todoText ref and rename other methods for convenience | ||
* return { ...rest, createTodo: mutate, todo: data, todoText } | ||
* }) | ||
* ``` | ||
*/ | ||
declare function defineMutation<T>(setup: () => T): () => T; | ||
/** | ||
* Define a query with the given options. Similar to `useQuery(options)` but allows you to reuse the query in multiple | ||
@@ -823,2 +877,19 @@ * places. | ||
* @param options - Pinia Colada Query Hooks plugin options | ||
* | ||
* @example | ||
* ```ts | ||
* import { PiniaColada, PiniaColadaQueryHooksPlugin } from '@pinia/colada' | ||
* | ||
* const app = createApp(App) | ||
* // app setup with other plugins | ||
* app.use(PiniaColada, { | ||
* plugins: [ | ||
* PiniaColadaQueryHooksPlugin({ | ||
* onError(error, entry) { | ||
* // ... | ||
* }, | ||
* }), | ||
* ], | ||
* }) | ||
* ``` | ||
*/ | ||
@@ -825,0 +896,0 @@ declare function PiniaColadaQueryHooksPlugin(options: PiniaColadaQueryHooksPluginOptions): PiniaColadaPlugin; |
@@ -197,3 +197,3 @@ // src/use-mutation.ts | ||
invalidate(entry); | ||
return fetch(entry); | ||
return entry.active && fetch(entry); | ||
}) | ||
@@ -405,3 +405,3 @@ ); | ||
function useMutation(options) { | ||
const store = useQueryCache(); | ||
const caches = useQueryCache(); | ||
const status = shallowRef2("pending"); | ||
@@ -418,11 +418,35 @@ const asyncStatus = shallowRef2("idle"); | ||
let currentError; | ||
let context; | ||
let context = { | ||
caches | ||
}; | ||
const currentCall = pendingCall = Symbol(); | ||
try { | ||
context = await options.onMutate?.(vars); | ||
let globalOnMutateContext; | ||
const onMutateContext = await options.onMutate?.( | ||
vars, | ||
context | ||
// NOTE: the cast makes it easier to write without extra code. It's safe because { ...null, ...undefined } works and TContext must be a Record<any, any> | ||
); | ||
const newData = currentData = await options.mutation( | ||
vars, | ||
onMutateContext | ||
); | ||
context = { | ||
...globalOnMutateContext, | ||
caches, | ||
...onMutateContext | ||
// NOTE: needed for onSuccess cast | ||
}; | ||
await options.onSuccess?.( | ||
newData, | ||
vars, | ||
// NOTE: cast is safe because of the satisfies above | ||
// using a spread also works | ||
context | ||
// { | ||
// ...globalOnMutateContext!, | ||
// caches, | ||
// ...onMutateContext, | ||
// i | ||
); | ||
await options.onSuccess?.({ data: newData, vars, ...context }); | ||
if (pendingCall === currentCall) { | ||
@@ -432,17 +456,6 @@ data.value = newData; | ||
status.value = "success"; | ||
if (options.keys) { | ||
const keys = typeof options.keys === "function" ? options.keys(newData, vars) : options.keys; | ||
for (const entry of keys.flatMap( | ||
(key) => store.getEntries({ key, exact: true }) | ||
)) { | ||
store.invalidate(entry); | ||
if (entry.active) { | ||
store.fetch(entry); | ||
} | ||
} | ||
} | ||
} | ||
} catch (newError) { | ||
currentError = newError; | ||
await options.onError?.({ error: newError, vars, ...context }); | ||
await options.onError?.(newError, vars, context); | ||
if (pendingCall === currentCall) { | ||
@@ -455,8 +468,3 @@ error.value = newError; | ||
asyncStatus.value = "idle"; | ||
await options.onSettled?.({ | ||
data: currentData, | ||
error: currentError, | ||
vars, | ||
...context | ||
}); | ||
await options.onSettled?.(currentData, currentError, vars, context); | ||
} | ||
@@ -480,6 +488,6 @@ return currentData; | ||
error, | ||
// FIXME: remove these and move to the variable | ||
// @ts-expect-error: it would be nice to find a type-only refactor that works | ||
// @ts-expect-error: because of the conditional type in UseMutationReturn | ||
// it would be nice to find a type-only refactor that works | ||
mutate, | ||
// @ts-expect-error: it would be nice to find a type-only refactor that works | ||
// @ts-expect-error: same as above | ||
mutateAsync, | ||
@@ -486,0 +494,0 @@ reset |
{ | ||
"name": "@pinia/colada", | ||
"type": "module", | ||
"version": "0.9.1", | ||
"version": "0.10.0", | ||
"description": "The smart data fetching layer for Pinia", | ||
@@ -84,5 +84,5 @@ "publishConfig": { | ||
"@antfu/eslint-config": "^3.7.3", | ||
"@shikijs/vitepress-twoslash": "^1.20.0", | ||
"@size-limit/preset-small-lib": "^11.1.5", | ||
"@types/node": "^22.7.3", | ||
"@shikijs/vitepress-twoslash": "^1.21.0", | ||
"@size-limit/preset-small-lib": "^11.1.6", | ||
"@types/node": "^22.7.4", | ||
"@vitejs/plugin-vue": "^5.1.4", | ||
@@ -102,16 +102,16 @@ "@vitest/coverage-v8": "^2.1.1", | ||
"p-series": "^3.0.0", | ||
"pinia": "^2.2.2", | ||
"pinia": "^2.2.4", | ||
"prettier": "^3.3.3", | ||
"semver": "^7.6.3", | ||
"simple-git-hooks": "^2.11.1", | ||
"size-limit": "^11.1.5", | ||
"size-limit": "^11.1.6", | ||
"standard-version": "^9.5.0", | ||
"tsup": "^8.3.0", | ||
"typedoc": "^0.26.7", | ||
"typedoc-plugin-markdown": "^4.2.8", | ||
"typedoc-plugin-markdown": "^4.2.9", | ||
"typescript": "~5.6.2", | ||
"vitepress": "1.3.4", | ||
"vitest": "^2.1.1", | ||
"vue": "^3.5.9", | ||
"@pinia/colada": "0.9.1" | ||
"vue": "^3.5.10", | ||
"@pinia/colada": "0.10.0" | ||
}, | ||
@@ -134,3 +134,3 @@ "simple-git-hooks": { | ||
"release": "standard-version", | ||
"test": "pnpm run test:cov && pnpm run build && pnpm run build:plugins && pnpm run test:types", | ||
"test": "pnpm run build && pnpm run test:cov && pnpm run build:plugins && pnpm run test:types", | ||
"lint": "eslint .", | ||
@@ -137,0 +137,0 @@ "lint:fix": "eslint --fix .", |
@@ -18,3 +18,3 @@ <p align="center"> | ||
# Pinia Colada (WIP) | ||
# Pinia Colada | ||
@@ -29,5 +29,6 @@ > The missing data fetching library for [Pinia](https://pinia.vuejs.org) | ||
> [!WARNING] | ||
> Pinia Colada is still experimental and not ready for production. New versions might introduce breaking changes. | ||
> [!NOTE] | ||
> Pinia Colada is in active development not ready for production. New versions might introduce breaking changes. | ||
> Feedback regarding new and existing options and features is welcome! | ||
> Documentation is a work in progress and **contributions are welcome**. | ||
@@ -44,2 +45,3 @@ Pinia Colada is an opinionated yet flexible data fetching layer on top of Pinia. It's built as a set of **pinia plugins**, **stores** and **composables** to benefit from Pinia's features and ecosystem. Pinia Colada has: | ||
- ⚙️ **SSR**: Server-side rendering support | ||
- 🔌 **Plugins**: Powerful plugin system | ||
@@ -70,6 +72,7 @@ ## Installation | ||
import { useRoute } from 'vue-router' | ||
import { useMutation, useQuery } from '@pinia/colada' | ||
import { useMutation, useQuery, useQueryCache } from '@pinia/colada' | ||
import { updateContact as _updateContact, getContactById } from '~/api/contacts' | ||
const route = useRoute() | ||
const caches = useQueryCache() | ||
@@ -83,5 +86,6 @@ const { data: contact, isLoading } = useQuery({ | ||
const { mutate: updateContact } = useMutation({ | ||
// automatically invalidates the cache for ['contacts'] and ['contacts', id] | ||
keys: ({ id }) => [['contacts'], ['contacts', id]], | ||
mutation: _updateContact, | ||
onSettled({ id }) { | ||
caches.invalidateQueries({ key: ['contacts', id], exact: true }) | ||
}, | ||
}) | ||
@@ -88,0 +92,0 @@ </script> |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
294235
2432
104