Skip to content

Commit

Permalink
feat(mutation): add use mutating hook (#23)
Browse files Browse the repository at this point in the history
* feat(mutation): add use mutating hook
  • Loading branch information
amen-souissi committed Mar 22, 2021
1 parent b98e737 commit 653c640
Show file tree
Hide file tree
Showing 16 changed files with 259 additions and 11 deletions.
5 changes: 5 additions & 0 deletions docs/src/manifests/manifest.json
Expand Up @@ -273,6 +273,11 @@
"path": "/reference/useIsFetching",
"editUrl": "/reference/useIsFetching.md"
},
{
"title": "useIsMutating",
"path": "/reference/useIsMutating",
"editUrl": "/reference/useIsMutating.md"
},
{
"title": "QueryClient",
"path": "/reference/QueryClient",
Expand Down
23 changes: 23 additions & 0 deletions docs/src/pages/reference/useIsMutating.md
@@ -0,0 +1,23 @@
---
id: useIsMutating
title: useIsMutating
---

`useIsMutating` is an optional hook that returns the `number` of mutations that your application is fetching (useful for app-wide loading indicators).

```js
import { useIsMutating } from '@sveltestack/svelte-query'
// How many mutations are fetching?
const isMutating = useIsMutating()
// How many mutations matching the posts prefix are fetching?
const isMutatingPosts = useIsMutating(['posts'])
```

**Options**

- `filters?: MutationFilters`: [Query Filters](../guides/filters#mutation-filters)

**Returns**

- `isMutating: number`
- Will be the `number` of the mutations that your application is currently fetching.
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "@sveltestack/svelte-query",
"private": false,
"version": "1.2.1",
"version": "1.3.0",
"description": "Hooks for managing, caching and syncing asynchronous and remote data in Svelte",
"license": "MIT",
"svelte": "svelte/index.js",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -5,6 +5,7 @@ export * from "./query";
export * from "./infiniteQuery";
export * from "./queries";
export * from "./isFetching";
export * from "./isMutating";
export * from "./mutation";
export * from "./hydration";

Expand Down
13 changes: 13 additions & 0 deletions src/isMutating/IsMutating.svelte
@@ -0,0 +1,13 @@
<script lang="ts">
import type { MutationFilters } from '../queryCore/core/utils'
import useIsMutating from './useIsMutating'
export let filters: MutationFilters
export let isMutating
$: isMutatingResult = useIsMutating(filters)
$: isMutating = $isMutatingResult
</script>

<slot name="isMutating" {isMutating} />
2 changes: 2 additions & 0 deletions src/isMutating/index.ts
@@ -0,0 +1,2 @@
export { default as IsMutating } from "./IsMutating.svelte";
export { default as useIsMutating } from "./useIsMutating";
27 changes: 27 additions & 0 deletions src/isMutating/useIsMutating.ts
@@ -0,0 +1,27 @@
import { readable } from 'svelte/store'
import type { Readable } from "svelte/store";

import { notifyManager, QueryClient } from '../queryCore/core'
import { useQueryClient } from '../queryClientProvider'
import type { MutationFilters } from '../queryCore/core/utils'

export default function useIsMutating(filters?: MutationFilters): Readable<number> {
const client: QueryClient = useQueryClient()
const cache = client.getMutationCache()
// isMutating is the prev value initialized on mount *
let isMutating = client.isMutating(filters)

const { subscribe } = readable(isMutating, set => {
return cache.subscribe(
notifyManager.batchCalls(() => {
const newIisMutating = client.isMutating(filters)
if (isMutating !== newIisMutating) {
// * and update with each change
isMutating = newIisMutating
set(isMutating)
}
}))
})

return { subscribe }
}
21 changes: 20 additions & 1 deletion src/queryCore/core/mutationCache.ts
Expand Up @@ -2,7 +2,7 @@ import type { MutationOptions } from './types'
import type { QueryClient } from './queryClient'
import { notifyManager } from './notifyManager'
import { Mutation, MutationState } from './mutation'
import { noop } from './utils'
import { matchMutation, MutationFilters, noop } from './utils'
import { Subscribable } from './subscribable'

// TYPES
Expand Down Expand Up @@ -76,6 +76,25 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
return this.mutations
}

find<
TData = unknown,
TError = unknown,
TVariables = any,
TContext = unknown
>(
filters: MutationFilters
): Mutation<TData, TError, TVariables, TContext> | undefined {
if (typeof filters.exact === 'undefined') {
filters.exact = true
}

return this.mutations.find(mutation => matchMutation(filters, mutation))
}

findAll(filters: MutationFilters): Mutation[] {
return this.mutations.filter(mutation => matchMutation(filters, mutation))
}

notify(mutation?: Mutation<any, any, any, any>) {
notifyManager.batch(() => {
this.listeners.forEach(listener => {
Expand Down
7 changes: 6 additions & 1 deletion src/queryCore/core/queryClient.ts
Expand Up @@ -7,6 +7,7 @@ import {
parseQueryArgs,
partialMatchKey,
hashQueryKeyByOptions,
MutationFilters,
} from './utils'
import type {
DefaultOptions,
Expand Down Expand Up @@ -99,6 +100,10 @@ export class QueryClient {
return this.queryCache.findAll(filters).length
}

isMutating(filters?: MutationFilters): number {
return this.mutationCache.findAll({ ...filters, fetching: true }).length
}

getQueryData<TData = unknown>(
queryKey: QueryKey,
filters?: QueryFilters
Expand Down Expand Up @@ -207,7 +212,7 @@ export class QueryClient {
const refetchFilters: QueryFilters = {
...filters,
active: filters.refetchActive ?? true,
inactive: filters.refetchInactive ?? false,
inactive: filters.refetchInactive,
}

return notifyManager.batch(() => {
Expand Down
7 changes: 6 additions & 1 deletion src/queryCore/core/queryObserver.ts
Expand Up @@ -57,6 +57,7 @@ export class QueryObserver<
TQueryData
>
private previousQueryResult?: QueryObserverResult<TData, TError>
private previousSelectError: Error | null
private staleTimeoutId?: number
private refetchIntervalId?: number
private trackedProps!: Array<keyof QueryObserverResult>
Expand All @@ -70,6 +71,7 @@ export class QueryObserver<
this.client = client
this.options = options
this.trackedProps = []
this.previousSelectError = null
this.bindMethods()
this.setOptions(options)
}
Expand Down Expand Up @@ -415,7 +417,8 @@ export class QueryObserver<
if (
prevResult &&
state.data === prevResultState?.data &&
options.select === prevResultOptions?.select
options.select === prevResultOptions?.select &&
!this.previousSelectError
) {
data = prevResult.data
} else {
Expand All @@ -424,9 +427,11 @@ export class QueryObserver<
if (options.structuralSharing !== false) {
data = replaceEqualDeep(prevResult?.data, data)
}
this.previousSelectError = null
} catch (selectError) {
getLogger().error(selectError)
error = selectError
this.previousSelectError = selectError
errorUpdatedAt = Date.now()
status = 'error'
}
Expand Down
6 changes: 3 additions & 3 deletions src/queryCore/core/types.ts
Expand Up @@ -3,7 +3,7 @@ import type { QueryBehavior } from './query'
import type { RetryValue, RetryDelayValue } from './retryer'
import type { QueryFilters } from './utils'

export type QueryKey = string | unknown[]
export type QueryKey = string | readonly unknown[]

export type QueryFunction<T = unknown> = (
context: QueryFunctionContext<any>
Expand Down Expand Up @@ -448,7 +448,7 @@ export type InfiniteQueryObserverResult<TData = unknown, TError = unknown> =
| InfiniteQueryObserverRefetchErrorResult<TData, TError>
| InfiniteQueryObserverSuccessResult<TData, TError>

export type MutationKey = string | unknown[]
export type MutationKey = string | readonly unknown[]

export type MutationStatus = 'idle' | 'loading' | 'success' | 'error'

Expand All @@ -463,7 +463,7 @@ export interface MutationOptions<
TContext = unknown
> {
mutationFn?: MutationFunction<TData, TVariables>
mutationKey?: string | unknown[]
mutationKey?: MutationKey
variables?: TVariables
onMutate?: (variables: TVariables) => Promise<TContext> | TContext
onSuccess?: (
Expand Down
59 changes: 55 additions & 4 deletions src/queryCore/core/utils.ts
@@ -1,3 +1,4 @@
import type { Mutation } from './mutation'
import type { Query } from './query'
import type {
MutationFunction,
Expand Down Expand Up @@ -41,6 +42,25 @@ export interface QueryFilters {
fetching?: boolean
}

export interface MutationFilters {
/**
* Match mutation key exactly
*/
exact?: boolean
/**
* Include mutations matching this predicate function
*/
predicate?: (mutation: Mutation<any, any, any>) => boolean
/**
* Include mutations matching this mutation key
*/
mutationKey?: MutationKey
/**
* Include or exclude fetching mutations
*/
fetching?: boolean
}

export type DataUpdateFunction<TInput, TOutput> = (input: TInput) => TOutput

export type Updater<TInput, TOutput> =
Expand Down Expand Up @@ -187,6 +207,40 @@ export function matchQuery(
return true
}

export function matchMutation(
filters: MutationFilters,
mutation: Mutation<any, any>
): boolean {
const { exact, fetching, predicate, mutationKey } = filters
if (isQueryKey(mutationKey)) {
if (!mutation.options.mutationKey) {
return false
}
if (exact) {
if (
hashQueryKey(mutation.options.mutationKey) !== hashQueryKey(mutationKey)
) {
return false
}
} else if (!partialMatchKey(mutation.options.mutationKey, mutationKey)) {
return false
}
}

if (
typeof fetching === 'boolean' &&
(mutation.state.status === 'loading') !== fetching
) {
return false
}

if (predicate && !predicate(mutation)) {
return false
}

return true
}

export function hashQueryKeyByOptions(
queryKey: QueryKey,
options?: QueryOptions<any, any>
Expand Down Expand Up @@ -222,10 +276,7 @@ export function stableValueHash(value: any): string {
/**
* Checks if key `b` partially matches with key `a`.
*/
export function partialMatchKey(
a: string | unknown[],
b: string | unknown[]
): boolean {
export function partialMatchKey(a: QueryKey, b: QueryKey): boolean {
return partialDeepEqual(ensureArray(a), ensureArray(b))
}

Expand Down
8 changes: 8 additions & 0 deletions stories/isMutating/App.svelte
@@ -0,0 +1,8 @@
<script lang="ts">
import { QueryClientProvider } from '../../src'
import IsFetching from './IsMutating.svelte'
</script>

<QueryClientProvider>
<IsFetching />
</QueryClientProvider>
60 changes: 60 additions & 0 deletions stories/isMutating/IsMutating.svelte
@@ -0,0 +1,60 @@
<script lang="ts">
import { IsMutating, useMutation } from '../../src'
import { useIsMutating } from '../../src/isMutating'
const later = (delay, value) =>
new Promise(resolve => setTimeout(resolve, delay, value))
// the mutation
const mutationFn = () => later(500, 'My response')
const useMutationResult = useMutation(mutationFn)
// the mutation 2
const mutationFn2 = () => later(500, 'My response 2')
const useMutationResult2 = useMutation(mutationFn2)
let isMutating = 0
let history = []
$: {
history = [...history, isMutating]
}
// useIsMutating
const isMutatingResult = useIsMutating()
let useHistory = []
$: {
useHistory = [...useHistory, $isMutatingResult]
}
</script>

<main>
<h3>IsFetching</h3>
<IsMutating bind:isMutating>
<div slot="isMutating">
isMutating change log:
<span>{JSON.stringify(history)}</span>
</div>
</IsMutating>

<h3>useIsMutating</h3>
<div>
useIsMutating change log:
{JSON.stringify(useHistory)}
<div>
<button
on:click={() => {
$useMutationResult.mutate()
$useMutationResult2.mutate()
}}>
Mutate All
</button>

<h3>Mutation 1</h3>
<button on:click={() => $useMutationResult.mutate()}>mutate</button>
{$useMutationResult.isLoading ? 'Mutation loading ...' : $useMutationResult.data || ''}

<h3>Mutation 2</h3>
<button on:click={() => $useMutationResult2.mutate()}>mutate 2</button>
{$useMutationResult2.isLoading ? 'Mutation 2 loading ...' : $useMutationResult2.data || ''}
</div>
</div>
</main>
10 changes: 10 additions & 0 deletions stories/isMutating/isMutating.stories.ts
@@ -0,0 +1,10 @@
import App from './App.svelte';

export default {
title: 'Is mutating',
component: App,
};

export const History = () => ({
Component: App
});

0 comments on commit 653c640

Please sign in to comment.