Skip to content

Commit 3668c90

Browse files
authored
feat: pass a function to the revalidate option in mutate (#2862)
feat: pass a function to revalidate option in mutate
1 parent f306893 commit 3668c90

8 files changed

+250
-14
lines changed

src/_internal/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export type MutatorCallback<Data = any> = (
315315
* @typeParam MutationData - The type of the data returned by the mutator
316316
*/
317317
export type MutatorOptions<Data = any, MutationData = Data> = {
318-
revalidate?: boolean
318+
revalidate?: boolean | ((data: Data, key: Arguments) => boolean)
319319
populateCache?:
320320
| boolean
321321
| ((result: MutationData, currentData: Data | undefined) => Data)

src/_internal/utils/mutate.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ export async function internalMutate<Data>(
6060
const rollbackOnErrorOption = options.rollbackOnError
6161
let optimisticData = options.optimisticData
6262

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

10099
const startRevalidate = () => {
101100
const revalidators = EVENT_REVALIDATORS[key]
101+
const revalidate = isFunction(options.revalidate)
102+
? options.revalidate(get().data, _k)
103+
: options.revalidate !== false
102104
if (revalidate) {
103105
// Invalidate the key by deleting the concurrent request markers so new
104106
// requests will not be deduped.

src/infinite/index.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import type {
2121
SWRHook,
2222
MutatorCallback,
2323
Middleware,
24-
MutatorOptions,
2524
GlobalState
2625
} from '../_internal'
2726
import type {
@@ -31,7 +30,8 @@ import type {
3130
SWRInfiniteKeyLoader,
3231
SWRInfiniteFetcher,
3332
SWRInfiniteCacheValue,
34-
SWRInfiniteCompareFn
33+
SWRInfiniteCompareFn,
34+
SWRInfiniteMutatorOptions
3535
} from './types'
3636
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'
3737
import { getFirstPageKey } from './serialize'
@@ -55,7 +55,7 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
5555
fn: BareFetcher<Data> | null,
5656
config: Omit<typeof SWRConfig.defaultValue, 'fetcher'> &
5757
Omit<SWRInfiniteConfiguration<Data, Error>, 'fetcher'>
58-
): SWRInfiniteResponse<Data, Error> => {
58+
) => {
5959
const didMountRef = useRef<boolean>(false)
6060
const {
6161
cache,
@@ -140,6 +140,8 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
140140
async key => {
141141
// get the revalidate context
142142
const forceRevalidateAll = get()._i
143+
const shouldRevalidatePage = get()._r
144+
set({ _r: UNDEFINED })
143145

144146
// return an array of page data
145147
const data: Data[] = []
@@ -187,7 +189,12 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
187189
(cacheData &&
188190
!isUndefined(cacheData[i]) &&
189191
!config.compare(cacheData[i], pageData))
190-
if (fn && shouldFetchPage) {
192+
if (
193+
fn &&
194+
(typeof shouldRevalidatePage === 'function'
195+
? shouldRevalidatePage(pageData, pageArg)
196+
: shouldFetchPage)
197+
) {
191198
const revalidate = async () => {
192199
const hasPreloadedRequest = pageKey in PRELOAD
193200
if (!hasPreloadedRequest) {
@@ -238,7 +245,7 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
238245
| Data[]
239246
| Promise<Data[] | undefined>
240247
| MutatorCallback<Data[]>,
241-
opts?: undefined | boolean | MutatorOptions<Data[], T>
248+
opts?: undefined | boolean | SWRInfiniteMutatorOptions<Data[], T>
242249
) {
243250
// When passing as a boolean, it's explicitly used to disable/enable
244251
// revalidation.
@@ -253,10 +260,10 @@ export const infinite = (<Data, Error>(useSWRNext: SWRHook) =>
253260
if (shouldRevalidate) {
254261
if (!isUndefined(data)) {
255262
// We only revalidate the pages that are changed
256-
set({ _i: false })
263+
set({ _i: false, _r: options.revalidate })
257264
} else {
258265
// Calling `mutate()`, we revalidate all pages
259-
set({ _i: true })
266+
set({ _i: true, _r: options.revalidate })
260267
}
261268
}
262269

src/infinite/types.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import type {
44
Arguments,
55
BareFetcher,
66
State,
7-
StrictTupleKey
7+
StrictTupleKey,
8+
MutatorOptions,
9+
MutatorCallback
810
} from '../_internal'
911

1012
type FetcherResponse<Data = unknown> = Data | Promise<Data>
@@ -41,12 +43,29 @@ export interface SWRInfiniteConfiguration<
4143
compare?: SWRInfiniteCompareFn<Data>
4244
}
4345

46+
interface SWRInfiniteRevalidateFn<Data = any> {
47+
(data: Data, key: Arguments): boolean
48+
}
49+
50+
type InfiniteKeyedMutator<Data> = <MutationData = Data>(
51+
data?: Data | Promise<Data | undefined> | MutatorCallback<Data>,
52+
opts?: boolean | SWRInfiniteMutatorOptions<Data, MutationData>
53+
) => Promise<Data | MutationData | undefined>
54+
55+
export interface SWRInfiniteMutatorOptions<Data = any, MutationData = Data>
56+
extends Omit<MutatorOptions<Data, MutationData>, 'revalidate'> {
57+
revalidate?:
58+
| boolean
59+
| SWRInfiniteRevalidateFn<Data extends unknown[] ? Data[number] : never>
60+
}
61+
4462
export interface SWRInfiniteResponse<Data = any, Error = any>
45-
extends SWRResponse<Data[], Error> {
63+
extends Omit<SWRResponse<Data[], Error>, 'mutate'> {
4664
size: number
4765
setSize: (
4866
size: number | ((_size: number) => number)
4967
) => Promise<Data[] | undefined>
68+
mutate: InfiniteKeyedMutator<Data[]>
5069
}
5170

5271
export interface SWRInfiniteHook {
@@ -134,4 +153,5 @@ export interface SWRInfiniteCacheValue<Data = any, Error = any>
134153
// same key.
135154
_l?: number
136155
_k?: Arguments
156+
_r?: boolean | SWRInfiniteRevalidateFn
137157
}

src/mutation/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SWRResponse, Key } from '../core'
1+
import type { SWRResponse, Key, Arguments } from '../core'
22

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

@@ -25,7 +25,7 @@ export type SWRMutationConfiguration<
2525
ExtraArg = any,
2626
SWRData = any
2727
> = {
28-
revalidate?: boolean
28+
revalidate?: boolean | ((data: Data, key: Arguments) => boolean)
2929
populateCache?:
3030
| boolean
3131
| ((result: Data, currentData: SWRData | undefined) => SWRData)

test/use-swr-infinite.test.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -1843,4 +1843,48 @@ describe('useSWRInfinite', () => {
18431843
screen.getByText('data:apple, banana, pineapple,')
18441844
expect(previousPageDataLogs.every(d => d === null)).toBeTruthy()
18451845
})
1846+
1847+
it('should support revalidate as a function', async () => {
1848+
// mock api
1849+
let pageData = ['apple', 'banana', 'pineapple']
1850+
1851+
const key = createKey()
1852+
function Page() {
1853+
const { data, mutate: boundMutate } = useSWRInfinite(
1854+
index => [key, index],
1855+
([_, index]) => createResponse(pageData[index]),
1856+
{
1857+
initialSize: 3
1858+
}
1859+
)
1860+
1861+
return (
1862+
<div
1863+
onClick={() => {
1864+
boundMutate(undefined, {
1865+
// only revalidate 'apple' & 'pineapple' (page=2)
1866+
revalidate: (d, [_, i]: [string, number]) => {
1867+
return d === 'apple' || i === 2
1868+
}
1869+
})
1870+
}}
1871+
>
1872+
data:{Array.isArray(data) && data.join(',')}
1873+
</div>
1874+
)
1875+
}
1876+
1877+
renderWithConfig(<Page />)
1878+
screen.getByText('data:')
1879+
1880+
await screen.findByText('data:apple,banana,pineapple')
1881+
1882+
// update response data
1883+
pageData = pageData.map(data => `[${data}]`)
1884+
1885+
// revalidate
1886+
fireEvent.click(screen.getByText('data:apple,banana,pineapple'))
1887+
1888+
await screen.findByText('data:[apple],banana,[pineapple]')
1889+
})
18461890
})

test/use-swr-local-mutation.test.tsx

+115
Original file line numberDiff line numberDiff line change
@@ -1810,4 +1810,119 @@ describe('useSWR - local mutation', () => {
18101810
[key, 'inf', 1]
18111811
])
18121812
})
1813+
it('should support revalidate as a function', async () => {
1814+
let value = 0,
1815+
mutate
1816+
const key = createKey()
1817+
function Page() {
1818+
mutate = useSWRConfig().mutate
1819+
const { data } = useSWR(key, () => value++)
1820+
return <div>data: {data}</div>
1821+
}
1822+
1823+
renderWithConfig(<Page />)
1824+
screen.getByText('data:')
1825+
1826+
// mount
1827+
await screen.findByText('data: 0')
1828+
1829+
act(() => {
1830+
// value 0 -> 0
1831+
mutate(key, 100, { revalidate: () => false })
1832+
})
1833+
await screen.findByText('data: 100')
1834+
1835+
act(() => {
1836+
// value 0 -> 1
1837+
mutate(key, 200, { revalidate: () => true })
1838+
})
1839+
await screen.findByText('data: 200')
1840+
await screen.findByText('data: 1')
1841+
})
1842+
1843+
it('the function-style relivadate option receives the key and current data', async () => {
1844+
let value = 0,
1845+
mutate
1846+
const key = createKey()
1847+
function Page() {
1848+
mutate = useSWRConfig().mutate
1849+
const { data } = useSWR(key, () => value++)
1850+
return <div>data: {data}</div>
1851+
}
1852+
1853+
renderWithConfig(<Page />)
1854+
screen.getByText('data:')
1855+
1856+
// mount
1857+
await screen.findByText('data: 0')
1858+
1859+
act(() => {
1860+
// value 0 -> 0
1861+
mutate(key, 100, { revalidate: (d, k) => k === key && d === 200 }) // revalidate = false
1862+
})
1863+
await screen.findByText('data: 100')
1864+
1865+
act(() => {
1866+
// value 0 -> 1
1867+
mutate(key, 200, { revalidate: (d, k) => k === key && d === 200 }) // revalidate = true
1868+
})
1869+
await screen.findByText('data: 200')
1870+
await screen.findByText('data: 1')
1871+
})
1872+
1873+
it('the function-style relivadate option works with mutate filter', async () => {
1874+
const key1 = createKey()
1875+
const key2 = createKey()
1876+
const key3 = createKey()
1877+
1878+
let mockData = {
1879+
[key1]: 'page1',
1880+
[key2]: 'page2',
1881+
[key3]: 'page3'
1882+
}
1883+
function Page() {
1884+
const mutate = useSWRConfig().mutate
1885+
const { data: data1 } = useSWR(key1, () => mockData[key1])
1886+
const { data: data2 } = useSWR(key2, () => mockData[key2])
1887+
const { data: data3 } = useSWR(key3, () => mockData[key3])
1888+
1889+
return (
1890+
<>
1891+
<div>data1: {data1}</div>
1892+
<div>data2: {data2}</div>
1893+
<div>data3: {data3}</div>
1894+
<button
1895+
onClick={() => {
1896+
// key1 is filtered
1897+
mutate(k => k !== key1, 'updated', {
1898+
// only revalidate key3
1899+
revalidate: (d, k) => d === 'updated' && k === key3
1900+
})
1901+
}}
1902+
>
1903+
click
1904+
</button>
1905+
</>
1906+
)
1907+
}
1908+
1909+
renderWithConfig(<Page />)
1910+
1911+
// mount
1912+
await screen.findByText('data1: page1')
1913+
await screen.findByText('data2: page2')
1914+
await screen.findByText('data3: page3')
1915+
1916+
mockData = {
1917+
[key1]: '<page1>',
1918+
[key2]: '<page2>',
1919+
[key3]: '<page3>'
1920+
}
1921+
1922+
fireEvent.click(screen.getByText('click'))
1923+
1924+
await screen.findByText('data1: page1')
1925+
await screen.findByText('data2: updated')
1926+
await screen.findByText('data3: <page3>')
1927+
})
18131928
})

test/use-swr-remote-mutation.test.tsx

+49-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'
22
import React, { useState } from 'react'
33
import useSWR from 'swr'
44
import useSWRMutation from 'swr/mutation'
5-
import { createKey, sleep, nextTick } from './utils'
5+
import { createKey, sleep, nextTick, createResponse } from './utils'
66

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

@@ -1033,4 +1033,52 @@ describe('useSWR - remote mutation', () => {
10331033
await screen.findByText('data:1,count:1')
10341034
expect(logs).toEqual([0, 1])
10351035
})
1036+
1037+
it('should support revalidate as a function', async () => {
1038+
const key = createKey()
1039+
1040+
let value = 0
1041+
1042+
function Page() {
1043+
const { data } = useSWR(key, () => createResponse(++value))
1044+
const { trigger } = useSWRMutation(key, () => {
1045+
value += 10
1046+
return createResponse(value)
1047+
})
1048+
1049+
return (
1050+
<div>
1051+
<button
1052+
onClick={() =>
1053+
trigger(undefined, {
1054+
revalidate: (d, k) => k === key && d < 30,
1055+
populateCache: true
1056+
})
1057+
}
1058+
>
1059+
trigger
1060+
</button>
1061+
<div>data:{data || 'none'}</div>
1062+
</div>
1063+
)
1064+
}
1065+
1066+
render(<Page />)
1067+
1068+
// mount
1069+
await screen.findByText('data:1')
1070+
1071+
fireEvent.click(screen.getByText('trigger'))
1072+
await screen.findByText('data:12')
1073+
fireEvent.click(screen.getByText('trigger'))
1074+
await screen.findByText('data:23')
1075+
fireEvent.click(screen.getByText('trigger'))
1076+
await screen.findByText('data:33')
1077+
1078+
// stop revalidation because value > 30
1079+
fireEvent.click(screen.getByText('trigger'))
1080+
await screen.findByText('data:43')
1081+
fireEvent.click(screen.getByText('trigger'))
1082+
await screen.findByText('data:53')
1083+
})
10361084
})

0 commit comments

Comments
 (0)