From 60135459fd6f2bc7c6aeaaeac90ff467faeb0cf8 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Sun, 13 Nov 2022 19:20:16 +0100 Subject: [PATCH 1/3] feat: support functional rollbackOnError --- _internal/types.ts | 2 +- _internal/utils/mutate.ts | 17 +++++++++++++--- test/use-swr-local-mutation.test.tsx | 29 +++++++++++++++++++++++----- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/_internal/types.ts b/_internal/types.ts index 249c91565..8d4a5e365 100644 --- a/_internal/types.ts +++ b/_internal/types.ts @@ -266,7 +266,7 @@ export type MutatorOptions = { | boolean | ((result: any, currentData: Data | undefined) => Data) optimisticData?: Data | ((currentData?: Data) => Data) - rollbackOnError?: boolean + rollbackOnError?: boolean | ((error: unknown, currentData?: Data) => boolean) throwOnError?: boolean } diff --git a/_internal/utils/mutate.ts b/_internal/utils/mutate.ts index eaa513837..758741d63 100644 --- a/_internal/utils/mutate.ts +++ b/_internal/utils/mutate.ts @@ -55,10 +55,16 @@ export async function internalMutate( ) let populateCache = options.populateCache + + const rollbackOnErrorOption = options.rollbackOnError let optimisticData = options.optimisticData const revalidate = options.revalidate !== false - const rollbackOnError = options.rollbackOnError !== false + const rollbackOnError = (error: unknown, currentData?: Data): boolean => { + return typeof rollbackOnErrorOption === 'function' + ? rollbackOnErrorOption(error, currentData) + : rollbackOnErrorOption !== false + } const throwOnError = options.throwOnError // If the second argument is a key filter, return the mutation results for all @@ -126,7 +132,8 @@ export async function internalMutate( // that is going to be overridden by a `committedData`, or get reverted back. // `committedData` is the validated value that comes from a fetch or mutation. const displayedData = state.data - const committedData = isUndefined(state._c) ? displayedData : state._c + const currentData = state._c + const committedData = isUndefined(currentData) ? displayedData : currentData // Do optimistic data update. if (hasOptimisticData) { @@ -162,7 +169,11 @@ export async function internalMutate( if (beforeMutationTs !== MUTATION[key][0]) { if (error) throw error return data - } else if (error && hasOptimisticData && rollbackOnError) { + } else if ( + error && + hasOptimisticData && + rollbackOnError(error, currentData) + ) { // Rollback. Always populate the cache in this case but without // transforming the data. populateCache = true diff --git a/test/use-swr-local-mutation.test.tsx b/test/use-swr-local-mutation.test.tsx index 9a72bf152..fddc52dc6 100644 --- a/test/use-swr-local-mutation.test.tsx +++ b/test/use-swr-local-mutation.test.tsx @@ -1337,11 +1337,12 @@ describe('useSWR - local mutation', () => { expect(renderedData).toEqual([undefined, 'foo', 'bar', 'baz', 'qux', 'baz']) }) - it('should not rollback optimistic updates if `rollbackOnError`', async () => { + it('should not rollback optimistic updates if `rollbackOnError` is disabled', async () => { const key = createKey() const renderedData = [] let mutate let cnt = 0 + let callbackArgs function Page() { const { data, mutate: boundMutate } = useSWR(key, () => @@ -1362,17 +1363,35 @@ describe('useSWR - local mutation', () => { try { await executeWithoutBatching(() => - mutate(createResponse(new Error('baz'), { delay: 20 }), { - optimisticData: 'bar', + mutate(createResponse(new Error('baz-1'), { delay: 20 }), { + optimisticData: 'bar-1', rollbackOnError: false }) ) } catch (e) { - expect(e.message).toEqual('baz') + expect(e.message).toEqual('baz-1') + } + + await sleep(30) + expect(renderedData).toEqual([undefined, 0, 'bar-1', 1]) + + try { + await executeWithoutBatching(() => + mutate(createResponse(new Error('baz-2'), { delay: 20 }), { + optimisticData: 'bar-2', + rollbackOnError: (error, currentData) => { + callbackArgs = [error.message, currentData] + return false + } + }) + ) + } catch (e) { + expect(e.message).toEqual('baz-2') } await sleep(30) - expect(renderedData).toEqual([undefined, 0, 'bar', 1]) + expect(renderedData).toEqual([undefined, 0, 'bar-1', 1, 'bar-2', 2]) + expect(callbackArgs).toEqual(['baz-2', undefined]) }) it('should support transforming the result with `populateCache` before writing back', async () => { From d45fbfd233a86c14250e21aed3e7b6c178854742 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 14 Nov 2022 22:47:52 +0100 Subject: [PATCH 2/3] only keep error arg --- _internal/types.ts | 2 +- _internal/utils/mutate.ts | 10 +++------- test/use-swr-local-mutation.test.tsx | 7 ++++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/_internal/types.ts b/_internal/types.ts index 8d4a5e365..cb94a8fc5 100644 --- a/_internal/types.ts +++ b/_internal/types.ts @@ -266,7 +266,7 @@ export type MutatorOptions = { | boolean | ((result: any, currentData: Data | undefined) => Data) optimisticData?: Data | ((currentData?: Data) => Data) - rollbackOnError?: boolean | ((error: unknown, currentData?: Data) => boolean) + rollbackOnError?: boolean | ((error: unknown) => boolean) throwOnError?: boolean } diff --git a/_internal/utils/mutate.ts b/_internal/utils/mutate.ts index 758741d63..d07d2d45a 100644 --- a/_internal/utils/mutate.ts +++ b/_internal/utils/mutate.ts @@ -60,9 +60,9 @@ export async function internalMutate( let optimisticData = options.optimisticData const revalidate = options.revalidate !== false - const rollbackOnError = (error: unknown, currentData?: Data): boolean => { + const rollbackOnError = (error: unknown): boolean => { return typeof rollbackOnErrorOption === 'function' - ? rollbackOnErrorOption(error, currentData) + ? rollbackOnErrorOption(error) : rollbackOnErrorOption !== false } const throwOnError = options.throwOnError @@ -169,11 +169,7 @@ export async function internalMutate( if (beforeMutationTs !== MUTATION[key][0]) { if (error) throw error return data - } else if ( - error && - hasOptimisticData && - rollbackOnError(error, currentData) - ) { + } else if (error && hasOptimisticData && rollbackOnError(error)) { // Rollback. Always populate the cache in this case but without // transforming the data. populateCache = true diff --git a/test/use-swr-local-mutation.test.tsx b/test/use-swr-local-mutation.test.tsx index fddc52dc6..f6656c8c2 100644 --- a/test/use-swr-local-mutation.test.tsx +++ b/test/use-swr-local-mutation.test.tsx @@ -1375,12 +1375,13 @@ describe('useSWR - local mutation', () => { await sleep(30) expect(renderedData).toEqual([undefined, 0, 'bar-1', 1]) + let rollbackErrorMessage try { await executeWithoutBatching(() => mutate(createResponse(new Error('baz-2'), { delay: 20 }), { optimisticData: 'bar-2', - rollbackOnError: (error, currentData) => { - callbackArgs = [error.message, currentData] + rollbackOnError: error => { + rollbackErrorMessage = error.message return false } }) @@ -1391,7 +1392,7 @@ describe('useSWR - local mutation', () => { await sleep(30) expect(renderedData).toEqual([undefined, 0, 'bar-1', 1, 'bar-2', 2]) - expect(callbackArgs).toEqual(['baz-2', undefined]) + expect(rollbackErrorMessage).toEqual('baz-2') }) it('should support transforming the result with `populateCache` before writing back', async () => { From 036a85078b298ce493aa28610112617c1a964bbe Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 14 Nov 2022 23:18:08 +0100 Subject: [PATCH 3/3] fix lint --- test/use-swr-local-mutation.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/test/use-swr-local-mutation.test.tsx b/test/use-swr-local-mutation.test.tsx index a564595ab..1a3c328d2 100644 --- a/test/use-swr-local-mutation.test.tsx +++ b/test/use-swr-local-mutation.test.tsx @@ -1342,7 +1342,6 @@ describe('useSWR - local mutation', () => { const renderedData = [] let mutate let cnt = 0 - let callbackArgs function Page() { const { data, mutate: boundMutate } = useSWR(key, () =>