Skip to content

Commit

Permalink
feat: pass a function to the revalidate option in mutate (#2862)
Browse files Browse the repository at this point in the history
feat: pass a function to revalidate option in mutate
  • Loading branch information
koba04 committed Feb 15, 2024
1 parent f306893 commit 3668c90
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 14 deletions.
2 changes: 1 addition & 1 deletion src/_internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export type MutatorCallback<Data = any> = (
* @typeParam MutationData - The type of the data returned by the mutator
*/
export type MutatorOptions<Data = any, MutationData = Data> = {
revalidate?: boolean
revalidate?: boolean | ((data: Data, key: Arguments) => boolean)
populateCache?:
| boolean
| ((result: MutationData, currentData: Data | undefined) => Data)
Expand Down
4 changes: 3 additions & 1 deletion src/_internal/utils/mutate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export async function internalMutate<Data>(
const rollbackOnErrorOption = options.rollbackOnError
let optimisticData = options.optimisticData

const revalidate = options.revalidate !== false
const rollbackOnError = (error: unknown): boolean => {
return typeof rollbackOnErrorOption === 'function'
? rollbackOnErrorOption(error)
Expand Down Expand Up @@ -99,6 +98,9 @@ export async function internalMutate<Data>(

const startRevalidate = () => {
const revalidators = EVENT_REVALIDATORS[key]
const revalidate = isFunction(options.revalidate)
? options.revalidate(get().data, _k)
: options.revalidate !== false
if (revalidate) {
// Invalidate the key by deleting the concurrent request markers so new
// requests will not be deduped.
Expand Down
21 changes: 14 additions & 7 deletions src/infinite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import type {
SWRHook,
MutatorCallback,
Middleware,
MutatorOptions,
GlobalState
} from '../_internal'
import type {
Expand All @@ -31,7 +30,8 @@ import type {
SWRInfiniteKeyLoader,
SWRInfiniteFetcher,
SWRInfiniteCacheValue,
SWRInfiniteCompareFn
SWRInfiniteCompareFn,
SWRInfiniteMutatorOptions
} from './types'
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'
import { getFirstPageKey } from './serialize'
Expand All @@ -55,7 +55,7 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
fn: BareFetcher<Data> | null,
config: Omit<typeof SWRConfig.defaultValue, 'fetcher'> &
Omit<SWRInfiniteConfiguration<Data, Error>, 'fetcher'>
): SWRInfiniteResponse<Data, Error> => {
) => {
const didMountRef = useRef<boolean>(false)
const {
cache,
Expand Down Expand Up @@ -140,6 +140,8 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
async key => {
// get the revalidate context
const forceRevalidateAll = get()._i
const shouldRevalidatePage = get()._r
set({ _r: UNDEFINED })

// return an array of page data
const data: Data[] = []
Expand Down Expand Up @@ -187,7 +189,12 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
(cacheData &&
!isUndefined(cacheData[i]) &&
!config.compare(cacheData[i], pageData))
if (fn && shouldFetchPage) {
if (
fn &&
(typeof shouldRevalidatePage === 'function'
? shouldRevalidatePage(pageData, pageArg)
: shouldFetchPage)
) {
const revalidate = async () => {
const hasPreloadedRequest = pageKey in PRELOAD
if (!hasPreloadedRequest) {
Expand Down Expand Up @@ -238,7 +245,7 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
| Data[]
| Promise<Data[] | undefined>
| MutatorCallback<Data[]>,
opts?: undefined | boolean | MutatorOptions<Data[], T>
opts?: undefined | boolean | SWRInfiniteMutatorOptions<Data[], T>
) {
// When passing as a boolean, it's explicitly used to disable/enable
// revalidation.
Expand All @@ -253,10 +260,10 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
if (shouldRevalidate) {
if (!isUndefined(data)) {
// We only revalidate the pages that are changed
set({ _i: false })
set({ _i: false, _r: options.revalidate })
} else {
// Calling `mutate()`, we revalidate all pages
set({ _i: true })
set({ _i: true, _r: options.revalidate })
}
}

Expand Down
24 changes: 22 additions & 2 deletions src/infinite/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import type {
Arguments,
BareFetcher,
State,
StrictTupleKey
StrictTupleKey,
MutatorOptions,
MutatorCallback
} from '../_internal'

type FetcherResponse<Data = unknown> = Data | Promise<Data>
Expand Down Expand Up @@ -41,12 +43,29 @@ export interface SWRInfiniteConfiguration<
compare?: SWRInfiniteCompareFn<Data>
}

interface SWRInfiniteRevalidateFn<Data = any> {
(data: Data, key: Arguments): boolean
}

type InfiniteKeyedMutator<Data> = <MutationData = Data>(
data?: Data | Promise<Data | undefined> | MutatorCallback<Data>,
opts?: boolean | SWRInfiniteMutatorOptions<Data, MutationData>
) => Promise<Data | MutationData | undefined>

export interface SWRInfiniteMutatorOptions<Data = any, MutationData = Data>
extends Omit<MutatorOptions<Data, MutationData>, 'revalidate'> {
revalidate?:
| boolean
| SWRInfiniteRevalidateFn<Data extends unknown[] ? Data[number] : never>
}

export interface SWRInfiniteResponse<Data = any, Error = any>
extends SWRResponse<Data[], Error> {
extends Omit<SWRResponse<Data[], Error>, 'mutate'> {
size: number
setSize: (
size: number | ((_size: number) => number)
) => Promise<Data[] | undefined>
mutate: InfiniteKeyedMutator<Data[]>
}

export interface SWRInfiniteHook {
Expand Down Expand Up @@ -134,4 +153,5 @@ export interface SWRInfiniteCacheValue<Data = any, Error = any>
// same key.
_l?: number
_k?: Arguments
_r?: boolean | SWRInfiniteRevalidateFn
}
4 changes: 2 additions & 2 deletions src/mutation/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SWRResponse, Key } from '../core'
import type { SWRResponse, Key, Arguments } from '../core'

type FetcherResponse<Data> = Data | Promise<Data>

Expand All @@ -25,7 +25,7 @@ export type SWRMutationConfiguration<
ExtraArg = any,
SWRData = any
> = {
revalidate?: boolean
revalidate?: boolean | ((data: Data, key: Arguments) => boolean)
populateCache?:
| boolean
| ((result: Data, currentData: SWRData | undefined) => SWRData)
Expand Down
44 changes: 44 additions & 0 deletions test/use-swr-infinite.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1843,4 +1843,48 @@ describe('useSWRInfinite', () => {
screen.getByText('data:apple, banana, pineapple,')
expect(previousPageDataLogs.every(d => d === null)).toBeTruthy()
})

it('should support revalidate as a function', async () => {
// mock api
let pageData = ['apple', 'banana', 'pineapple']

const key = createKey()
function Page() {
const { data, mutate: boundMutate } = useSWRInfinite(
index => [key, index],
([_, index]) => createResponse(pageData[index]),
{
initialSize: 3
}
)

return (
<div
onClick={() => {
boundMutate(undefined, {
// only revalidate 'apple' & 'pineapple' (page=2)
revalidate: (d, [_, i]: [string, number]) => {
return d === 'apple' || i === 2
}
})
}}
>
data:{Array.isArray(data) && data.join(',')}
</div>
)
}

renderWithConfig(<Page />)
screen.getByText('data:')

await screen.findByText('data:apple,banana,pineapple')

// update response data
pageData = pageData.map(data => `[${data}]`)

// revalidate
fireEvent.click(screen.getByText('data:apple,banana,pineapple'))

await screen.findByText('data:[apple],banana,[pineapple]')
})
})
115 changes: 115 additions & 0 deletions test/use-swr-local-mutation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1810,4 +1810,119 @@ describe('useSWR - local mutation', () => {
[key, 'inf', 1]
])
})
it('should support revalidate as a function', async () => {
let value = 0,
mutate
const key = createKey()
function Page() {
mutate = useSWRConfig().mutate
const { data } = useSWR(key, () => value++)
return <div>data: {data}</div>
}

renderWithConfig(<Page />)
screen.getByText('data:')

// mount
await screen.findByText('data: 0')

act(() => {
// value 0 -> 0
mutate(key, 100, { revalidate: () => false })
})
await screen.findByText('data: 100')

act(() => {
// value 0 -> 1
mutate(key, 200, { revalidate: () => true })
})
await screen.findByText('data: 200')
await screen.findByText('data: 1')
})

it('the function-style relivadate option receives the key and current data', async () => {
let value = 0,
mutate
const key = createKey()
function Page() {
mutate = useSWRConfig().mutate
const { data } = useSWR(key, () => value++)
return <div>data: {data}</div>
}

renderWithConfig(<Page />)
screen.getByText('data:')

// mount
await screen.findByText('data: 0')

act(() => {
// value 0 -> 0
mutate(key, 100, { revalidate: (d, k) => k === key && d === 200 }) // revalidate = false
})
await screen.findByText('data: 100')

act(() => {
// value 0 -> 1
mutate(key, 200, { revalidate: (d, k) => k === key && d === 200 }) // revalidate = true
})
await screen.findByText('data: 200')
await screen.findByText('data: 1')
})

it('the function-style relivadate option works with mutate filter', async () => {
const key1 = createKey()
const key2 = createKey()
const key3 = createKey()

let mockData = {
[key1]: 'page1',
[key2]: 'page2',
[key3]: 'page3'
}
function Page() {
const mutate = useSWRConfig().mutate
const { data: data1 } = useSWR(key1, () => mockData[key1])
const { data: data2 } = useSWR(key2, () => mockData[key2])
const { data: data3 } = useSWR(key3, () => mockData[key3])

return (
<>
<div>data1: {data1}</div>
<div>data2: {data2}</div>
<div>data3: {data3}</div>
<button
onClick={() => {
// key1 is filtered
mutate(k => k !== key1, 'updated', {
// only revalidate key3
revalidate: (d, k) => d === 'updated' && k === key3
})
}}
>
click
</button>
</>
)
}

renderWithConfig(<Page />)

// mount
await screen.findByText('data1: page1')
await screen.findByText('data2: page2')
await screen.findByText('data3: page3')

mockData = {
[key1]: '<page1>',
[key2]: '<page2>',
[key3]: '<page3>'
}

fireEvent.click(screen.getByText('click'))

await screen.findByText('data1: page1')
await screen.findByText('data2: updated')
await screen.findByText('data3: <page3>')
})
})
50 changes: 49 additions & 1 deletion test/use-swr-remote-mutation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'
import React, { useState } from 'react'
import useSWR from 'swr'
import useSWRMutation from 'swr/mutation'
import { createKey, sleep, nextTick } from './utils'
import { createKey, sleep, nextTick, createResponse } from './utils'

const waitForNextTick = () => act(() => sleep(1))

Expand Down Expand Up @@ -1033,4 +1033,52 @@ describe('useSWR - remote mutation', () => {
await screen.findByText('data:1,count:1')
expect(logs).toEqual([0, 1])
})

it('should support revalidate as a function', async () => {
const key = createKey()

let value = 0

function Page() {
const { data } = useSWR(key, () => createResponse(++value))
const { trigger } = useSWRMutation(key, () => {
value += 10
return createResponse(value)
})

return (
<div>
<button
onClick={() =>
trigger(undefined, {
revalidate: (d, k) => k === key && d < 30,
populateCache: true
})
}
>
trigger
</button>
<div>data:{data || 'none'}</div>
</div>
)
}

render(<Page />)

// mount
await screen.findByText('data:1')

fireEvent.click(screen.getByText('trigger'))
await screen.findByText('data:12')
fireEvent.click(screen.getByText('trigger'))
await screen.findByText('data:23')
fireEvent.click(screen.getByText('trigger'))
await screen.findByText('data:33')

// stop revalidation because value > 30
fireEvent.click(screen.getByText('trigger'))
await screen.findByText('data:43')
fireEvent.click(screen.getByText('trigger'))
await screen.findByText('data:53')
})
})

0 comments on commit 3668c90

Please sign in to comment.