Skip to content

Commit

Permalink
feat: mutation cache duration (#2963)
Browse files Browse the repository at this point in the history
* feat: mutation cachetime

stramline queryCache / mutationCache events by combining them into notifiable.ts

* feat: mutation cachetime

removable

* feat: mutation cachetime

add gc to mutations

* feat: mutation cachetime

streamline event types between queries and mutations

* feat: mutation cachetime

tests, and I forgot to implement optionalRemove, so make it abstract

* feat: mutation cachetime

replicate gc behavior from #2950 and add more tests

* feat: mutation cachetime

get test coverage back to 100%

* feat: mutation cachetime

docs

* feat: mutation cachetime

try to make tests more resilient

* feat: mutation cachetime

fix imports after merge conflict
  • Loading branch information
TkDodo committed Nov 17, 2021
1 parent d1a7520 commit ac1eefd
Show file tree
Hide file tree
Showing 14 changed files with 335 additions and 85 deletions.
27 changes: 27 additions & 0 deletions docs/src/pages/guides/migrating-to-react-query-4.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ For the same reason, those have also been combined:

This flag defaults to `active` because `refetchActive` defaulted to `true`. This means we also need a way to tell `invalidateQueries` to not refetch at all, which is why a fourth option (`none`) is also allowed here.

### Streamlined NotifyEvents

Subscribing manually to the `QueryCache` has always given you a `QueryCacheNotifyEvent`, but this was not true for the `MutationCache`. We have streamlined the behavior and also adapted event names accordingly.

#### QueryCacheNotifyEvent

```diff
- type: 'queryAdded'
+ type: 'added'
- type: 'queryRemoved'
+ type: 'removed'
- type: 'queryUpdated'
+ type: 'updated'
```

#### MutationCacheNotifyEvent

The `MutationCacheNotifyEvent` uses the same types as the `QueryCacheNotifyEvent`.

> Note: This is only relevant if you manually subscribe to the caches via `queryCache.subscribe` or `mutationCache.subscribe`
### The `src/react` directory was renamed to `src/reactjs`

Previously, react-query had a directory named `react` which imported from the `react` module. This could cause problems with some Jest configurations, resulting in errors when running tests like:
Expand All @@ -110,3 +131,9 @@ If you were importing anything from `'react-query/react'` directly in your proje
- import { QueryClientProvider } from 'react-query/react';
+ import { QueryClientProvider } from 'react-query/reactjs';
```

## New Features 馃殌

### Mutation Cache Garbage Collection

Mutations can now also be garbage collected automatically, just like queries. The default `cacheTime` for mutations is also set to 5 minutes.
6 changes: 3 additions & 3 deletions docs/src/pages/reference/MutationCache.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,16 @@ const mutations = mutationCache.getAll()
The `subscribe` method can be used to subscribe to the mutation cache as a whole and be informed of safe/known updates to the cache like mutation states changing or mutations being updated, added or removed.

```js
const callback = mutation => {
console.log(mutation)
const callback = event => {
console.log(event.type, event.mutation)
}

const unsubscribe = mutationCache.subscribe(callback)
```

**Options**

- `callback: (mutation?: Mutation) => void`
- `callback: (mutation?: MutationCacheNotifyEvent) => void`
- This function will be called with the mutation cache any time it is updated.

**Returns**
Expand Down
6 changes: 5 additions & 1 deletion docs/src/pages/reference/useMutation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ const {
reset,
status,
} = useMutation(mutationFn, {
cacheTime,
mutationKey,
onError,
onMutate,
onSettled,
onSuccess,
useErrorBoundary,
meta,
meta
})

mutate(variables, {
Expand All @@ -39,6 +40,9 @@ mutate(variables, {
- **Required**
- A function that performs an asynchronous task and returns a promise.
- `variables` is an object that `mutate` will pass to your `mutationFn`
- `cacheTime: number | Infinity`
- The time in milliseconds that unused/inactive cache data remains in memory. When a mutation's cache becomes unused or inactive, that cache data will be garbage collected after this duration. When different cache times are specified, the longest one will be used.
- If set to `Infinity`, will disable garbage collection
- `mutationKey: string`
- Optional
- A mutation key can be set to inherit defaults set with `queryClient.setMutationDefaults` or to identify the mutation in the devtools.
Expand Down
12 changes: 6 additions & 6 deletions src/broadcastQueryClient-experimental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,20 @@ export function broadcastQueryClient({
} = queryEvent

if (
queryEvent.type === 'queryUpdated' &&
queryEvent.type === 'updated' &&
queryEvent.action?.type === 'success'
) {
channel.postMessage({
type: 'queryUpdated',
type: 'updated',
queryHash,
queryKey,
state,
})
}

if (queryEvent.type === 'queryRemoved') {
if (queryEvent.type === 'removed') {
channel.postMessage({
type: 'queryRemoved',
type: 'removed',
queryHash,
queryKey,
})
Expand All @@ -61,7 +61,7 @@ export function broadcastQueryClient({
tx(() => {
const { type, queryHash, queryKey, state } = action

if (type === 'queryUpdated') {
if (type === 'updated') {
const query = queryCache.get(queryHash)

if (query) {
Expand All @@ -77,7 +77,7 @@ export function broadcastQueryClient({
},
state
)
} else if (type === 'queryRemoved') {
} else if (type === 'removed') {
const query = queryCache.get(queryHash)

if (query) {
Expand Down
45 changes: 43 additions & 2 deletions src/core/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { MutationCache } from './mutationCache'
import type { MutationObserver } from './mutationObserver'
import { getLogger } from './logger'
import { notifyManager } from './notifyManager'
import { Removable } from './removable'
import { Retryer } from './retryer'
import { noop } from './utils'

Expand Down Expand Up @@ -81,7 +82,7 @@ export class Mutation<
TError = unknown,
TVariables = void,
TContext = unknown
> {
> extends Removable {
state: MutationState<TData, TError, TVariables, TContext>
options: MutationOptions<TData, TError, TVariables, TContext>
mutationId: number
Expand All @@ -92,6 +93,8 @@ export class Mutation<
private retryer?: Retryer<TData, TError>

constructor(config: MutationConfig<TData, TError, TVariables, TContext>) {
super()

this.options = {
...config.defaultOptions,
...config.options,
Expand All @@ -101,6 +104,9 @@ export class Mutation<
this.observers = []
this.state = config.state || getDefaultState()
this.meta = config.meta

this.updateCacheTime(this.options.cacheTime)
this.scheduleGc()
}

setState(state: MutationState<TData, TError, TVariables, TContext>): void {
Expand All @@ -110,11 +116,42 @@ export class Mutation<
addObserver(observer: MutationObserver<any, any, any, any>): void {
if (this.observers.indexOf(observer) === -1) {
this.observers.push(observer)

// Stop the mutation from being garbage collected
this.clearGcTimeout()

this.mutationCache.notify({
type: 'observerAdded',
mutation: this,
observer,
})
}
}

removeObserver(observer: MutationObserver<any, any, any, any>): void {
this.observers = this.observers.filter(x => x !== observer)

if (this.cacheTime) {
this.scheduleGc()
} else {
this.mutationCache.remove(this)
}

this.mutationCache.notify({
type: 'observerRemoved',
mutation: this,
observer,
})
}

protected optionalRemove() {
if (!this.observers.length) {
if (this.state.status === 'loading') {
this.scheduleGc()
} else {
this.mutationCache.remove(this)
}
}
}

cancel(): Promise<void> {
Expand Down Expand Up @@ -252,7 +289,11 @@ export class Mutation<
this.observers.forEach(observer => {
observer.onMutationUpdate(action)
})
this.mutationCache.notify(this)
this.mutationCache.notify({
mutation: this,
type: 'updated',
action,
})
})
}
}
Expand Down
57 changes: 41 additions & 16 deletions src/core/mutationCache.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { MutationObserver } from './mutationObserver'
import type { MutationOptions } from './types'
import type { QueryClient } from './queryClient'
import { notifyManager } from './notifyManager'
import { Mutation, MutationState } from './mutation'
import { Action, Mutation, MutationState } from './mutation'
import { matchMutation, MutationFilters, noop } from './utils'
import { Subscribable } from './subscribable'
import { Notifiable } from './notifiable'

// TYPES

Expand All @@ -12,21 +13,53 @@ interface MutationCacheConfig {
error: unknown,
variables: unknown,
context: unknown,
mutation: Mutation<unknown, unknown, unknown, unknown>
mutation: Mutation<unknown, unknown, unknown>
) => void
onSuccess?: (
data: unknown,
variables: unknown,
context: unknown,
mutation: Mutation<unknown, unknown, unknown, unknown>
mutation: Mutation<unknown, unknown, unknown>
) => void
}

type MutationCacheListener = (mutation?: Mutation) => void
interface NotifyEventMutationAdded {
type: 'added'
mutation: Mutation<any, any, any, any>
}
interface NotifyEventMutationRemoved {
type: 'removed'
mutation: Mutation<any, any, any, any>
}

interface NotifyEventMutationObserverAdded {
type: 'observerAdded'
mutation: Mutation<any, any, any, any>
observer: MutationObserver<any, any, any>
}

interface NotifyEventMutationObserverRemoved {
type: 'observerRemoved'
mutation: Mutation<any, any, any, any>
observer: MutationObserver<any, any, any>
}

interface NotifyEventMutationUpdated {
type: 'updated'
mutation: Mutation<any, any, any, any>
action: Action<any, any, any, any>
}

type MutationCacheNotifyEvent =
| NotifyEventMutationAdded
| NotifyEventMutationRemoved
| NotifyEventMutationObserverAdded
| NotifyEventMutationObserverRemoved
| NotifyEventMutationUpdated

// CLASS

export class MutationCache extends Subscribable<MutationCacheListener> {
export class MutationCache extends Notifiable<MutationCacheNotifyEvent> {
config: MutationCacheConfig

private mutations: Mutation<any, any, any, any>[]
Expand Down Expand Up @@ -62,13 +95,13 @@ export class MutationCache extends Subscribable<MutationCacheListener> {

add(mutation: Mutation<any, any, any, any>): void {
this.mutations.push(mutation)
this.notify(mutation)
this.notify({ type: 'added', mutation })
}

remove(mutation: Mutation<any, any, any, any>): void {
this.mutations = this.mutations.filter(x => x !== mutation)
mutation.cancel()
this.notify(mutation)
this.notify({ type: 'removed', mutation })
}

clear(): void {
Expand Down Expand Up @@ -97,14 +130,6 @@ export class MutationCache extends Subscribable<MutationCacheListener> {
return this.mutations.filter(mutation => matchMutation(filters, mutation))
}

notify(mutation?: Mutation<any, any, any, any>) {
notifyManager.batch(() => {
this.listeners.forEach(listener => {
listener(mutation)
})
})
}

onFocus(): void {
this.resumePausedMutations()
}
Expand Down
12 changes: 12 additions & 0 deletions src/core/notifiable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Subscribable } from './subscribable'
import { notifyManager } from '../core/notifyManager'

export class Notifiable<TEvent> extends Subscribable<(event: TEvent) => void> {
notify(event: TEvent) {
notifyManager.batch(() => {
this.listeners.forEach(listener => {
listener(event)
})
})
}
}

0 comments on commit ac1eefd

Please sign in to comment.