Skip to content

Commit

Permalink
fix: allow use mutate to pre fetch data in key before use in hook
Browse files Browse the repository at this point in the history
  • Loading branch information
edumudu committed Nov 21, 2022
1 parent 712a498 commit e9d4e00
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 81 deletions.
160 changes: 94 additions & 66 deletions lib/composables/global-swr-config/global-swr-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ import {

import { useSWRConfig, configureGlobalSWR } from '.';

const throwError = (error: Error) => {
throw error;
};

vi.mock('vue', async () => {
const original = (await vi.importActual('vue')) as Record<string, unknown>;

return {
...original,
provide: vi.fn(original.provide as AnyFunction),
};
});

describe('useSWRConfig', () => {
it.each([
[{}],
Expand All @@ -37,15 +50,6 @@ describe('useSWRConfig', () => {
});

describe('configureGlobalSWR', () => {
vi.mock('vue', async () => {
const original = (await vi.importActual('vue')) as Record<string, unknown>; // Step 2.

return {
...original,
provide: vi.fn(original.provide as AnyFunction),
};
});

const provideMock = provide as Mock<any[], any>;

it('should provide the default config if none is provided', () => {
Expand Down Expand Up @@ -78,37 +82,57 @@ describe('configureGlobalSWR', () => {
});
});

describe('mutate', () => {
const cacheProvider = mockedCache;
// eslint-disable-next-line prettier/prettier
describe.each([
'default',
'injected'
])('mutate - %s', (approach) => {
const isDefaultApproach = approach === 'default';
const cacheProvider = isDefaultApproach ? defaultConfig.cacheProvider : mockedCache;
const defaultKey = 'Default key';

const useSWRConfigWrapped = () =>
useInjectedSetup(
() => configureGlobalSWR({ cacheProvider }),
() => useSWRConfig(),
);
isDefaultApproach
? useSetup(() => useSWRConfig())
: useInjectedSetup(
() => configureGlobalSWR({ cacheProvider }),
() => useSWRConfig(),
);

beforeEach(() => {
cacheProvider.clear();
setDataToMockedCache(defaultKey, { data: 'cached data' });
setDataToMockedCache(defaultKey, { data: 'cached data' }, cacheProvider);
});

it('should write in the cache the value resolved from promise passed to mutate', async () => {
const { mutate } = useSWRConfigWrapped();
it.each([
['sync value', 'sync value'],
[Promise.resolve('resolved value'), 'resolved value'],
[{ data: 'sync obj' }, { data: 'sync obj' }],
[() => 'returned value', 'returned value'],
[() => Promise.resolve('returned promise'), 'returned promise'],
[() => ({ data: 'returned obj' }), { data: 'returned obj' }],
])(
'should write in the cache the value returned from function passed to mutate when that key does not exists in the cache yet: #%#',
async (mutateVal, expected) => {
cacheProvider.clear();

await mutate(defaultKey, Promise.resolve('resolved value'));
const { mutate } = useSWRConfigWrapped();
const key = 'key-1';

expect(getDataFromMockedCache(defaultKey)?.data).toEqual('resolved value');
});
await mutate(key, mutateVal);
expect(getDataFromMockedCache(key, cacheProvider)).toBeDefined();
expect(getDataFromMockedCache(key, cacheProvider)?.data).toEqual(expected);
},
);

it('should write in the cache the value returned from function passed to mutate', async () => {
const { mutate } = useSWRConfigWrapped();

await mutate(defaultKey, () => 'sync resolved value');
expect(getDataFromMockedCache(defaultKey)?.data).toEqual('sync resolved value');
expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('sync resolved value');

await mutate(defaultKey, () => Promise.resolve('async resolved value'));
expect(getDataFromMockedCache(defaultKey)?.data).toEqual('async resolved value');
expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('async resolved value');
});

it.each([
Expand All @@ -121,12 +145,9 @@ describe('mutate', () => {
(cachedData) => {
const updateFn = vi.fn();

setDataToMockedCache(defaultKey, { data: cachedData });
setDataToMockedCache(defaultKey, { data: cachedData }, cacheProvider);

const { mutate } = useInjectedSetup(
() => configureGlobalSWR({ cacheProvider }),
() => useSWRConfig(),
);
const { mutate } = useSWRConfigWrapped();

mutate(defaultKey, updateFn);

Expand All @@ -137,61 +158,68 @@ describe('mutate', () => {

it('should use the value resolved from updateFn for mutate`s return value', async () => {
const { mutate } = useSWRConfigWrapped();
const expected = 'resolved value';

expect(await mutate(defaultKey, () => 'resolved data')).toEqual('resolved data');
expect(await mutate(defaultKey, () => Promise.resolve('resolved data'))).toEqual(
'resolved data',
);
expect.assertions(2);
expect(await mutate(defaultKey, expected)).toEqual(expected);
expect(await mutate(defaultKey, () => expected)).toEqual(expected);
expect(await mutate(defaultKey, () => Promise.resolve(expected))).toEqual(expected);
expect.assertions(3);
});

it('should re-throw if an error ocours inside updateFn or promise passed rejects', async () => {
it.each([
[new Error('sync error'), () => throwError(new Error('sync error'))],
[new Error('async error'), () => Promise.reject(new Error('async error'))],
])('should re-throw if an error ocours inside updateFn: #%#', async (error, updateFn) => {
const { mutate } = useSWRConfigWrapped();

const syncError = new Error('sync error');
const asyncError = new Error('async error');
const promiseError = new Error('promise error');
await expect(mutate(defaultKey, updateFn)).rejects.toThrowError(error);

await expect(
mutate(defaultKey, () => {
throw syncError;
}),
).rejects.toThrowError(syncError);
expect.assertions(1);
});

await expect(mutate(defaultKey, () => Promise.reject(asyncError))).rejects.toThrowError(
asyncError,
);
await expect(mutate(defaultKey, Promise.reject(promiseError))).rejects.toThrowError(
promiseError,
);
expect.assertions(3);
it('should re-throw if promise passed to mutate rejects', async () => {
const { mutate } = useSWRConfigWrapped();

const promiseError = new Error('promise error');
const promise = Promise.reject(promiseError);

await expect(mutate(defaultKey, promise)).rejects.toThrowError(promiseError);
expect.assertions(1);
});

it('should write `optimisticData` to cache right away and ser to resolved value from updateFn after', async () => {
it('should write `optimisticData` to cache right away and set to resolved value from updateFn after', async () => {
const { mutate } = useSWRConfigWrapped();

const promise = mutate(defaultKey, Promise.resolve('resolved data'), {
optimisticData: 'optimistic data',
});

expect(getDataFromMockedCache(defaultKey)?.data).toEqual('optimistic data');
expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('optimistic data');

await promise;
expect(getDataFromMockedCache(defaultKey)?.data).toEqual('resolved data');
expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('resolved data');
});

it('should write rollback data writed in cache whe using `opoptimisticData` and `rollbackOnError`', async () => {
const { mutate } = useSWRConfigWrapped();

try {
await mutate(defaultKey, Promise.reject(), {
optimisticData: 'optimistic data',
rollbackOnError: true,
});
} catch (error) {
expect(getDataFromMockedCache(defaultKey)?.data).toEqual('cached data');
}

expect.assertions(1);
});
it.each([
{ optimisticData: 'optimistic data', updateFn: () => Promise.reject() },
{ optimisticData: 'optimistic data', updateFn: () => throwError(new Error()) },
{ optimisticData: undefined, updateFn: () => Promise.reject() },
{ optimisticData: undefined, updateFn: () => throwError(new Error()) },
])(
'should rollback data writed in cache when `optimisticData = $optimisticData` and `rollbackOnError = true`: #%#',
async ({ optimisticData, updateFn }) => {
const { mutate } = useSWRConfigWrapped();

try {
await mutate(defaultKey, updateFn, {
optimisticData,
rollbackOnError: true,
});
} catch (error) {
expect(getDataFromMockedCache(defaultKey, cacheProvider)?.data).toEqual('cached data');
}

expect.assertions(1);
},
);
});
30 changes: 19 additions & 11 deletions lib/composables/global-swr-config/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { computed, inject, provide, unref, shallowReadonly, toRefs } from 'vue';
import { computed, inject, provide, unref, shallowReadonly, toRefs, ref } from 'vue';
import { MaybeRef } from '@vueuse/core';

import { defaultConfig, globalConfigKey } from '@/config';
import { AnyFunction, Key, SWRConfig } from '@/types';
import { isUndefined, mergeConfig, serializeKey } from '@/utils';
import { AnyFunction, CacheState, Key, SWRConfig } from '@/types';
import { isUndefined, mergeConfig, serializeKey, isFunction } from '@/utils';
import { useScopeState } from '@/composables/scope-state';

export type MutateOptions = {
Expand All @@ -12,6 +12,13 @@ export type MutateOptions = {
revalidate?: boolean;
};

const createCacheState = (data: unknown): CacheState => ({
data,
error: undefined,
isValidating: false,
fetchedIn: new Date(),
});

export const useSWRConfig = () => {
const contextConfig = inject(
globalConfigKey,
Expand All @@ -28,18 +35,17 @@ export const useSWRConfig = () => {
options: MutateOptions = {},
) => {
const { key } = serializeKey(_key);
const cacheState = contextConfig.value.cacheProvider.get(key);
const cache = cacheProvider.value;
const cacheState = cache.get(key);
const hasCache = !isUndefined(cacheState);
const { optimisticData, rollbackOnError, revalidate = true } = options;

if (!cacheState) return;

const { data } = toRefs(cacheState);
const { data } = hasCache ? toRefs(cacheState) : { data: ref() };
const dataInCache = data.value;

const resultPromise: unknown | Promise<unknown> =
typeof updateFnOrPromise === 'function'
? updateFnOrPromise(cacheState.data)
: updateFnOrPromise;
const resultPromise: unknown | Promise<unknown> = isFunction(updateFnOrPromise)
? updateFnOrPromise(dataInCache)
: updateFnOrPromise;

if (optimisticData) {
data.value = optimisticData;
Expand All @@ -55,6 +61,8 @@ export const useSWRConfig = () => {
throw error;
}

cache.set(key, hasCache ? cacheState : createCacheState(data));

const revalidationCallbackcs = revalidateCache.value.get(key) || [];

if (revalidate && revalidationCallbackcs.length) {
Expand Down
12 changes: 8 additions & 4 deletions lib/utils/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,25 @@ export const useSetupInServer = <V>(setup: () => V) => {

export const mockedCache = reactive<CacheProvider>(new Map());

export const setDataToMockedCache = (key: Key, data: UnwrapRef<Partial<CacheState>>) => {
export const setDataToMockedCache = (
key: Key,
data: UnwrapRef<Partial<CacheState>>,
cache = mockedCache,
) => {
const { key: serializedKey } = serializeKey(key);

mockedCache.set(serializedKey, {
cache.set(serializedKey, {
error: data.error,
data: data.data,
isValidating: data.isValidating || false,
fetchedIn: data.fetchedIn || new Date(),
});
};

export const getDataFromMockedCache = (key: Key) => {
export const getDataFromMockedCache = (key: Key, cache = mockedCache) => {
const { key: serializedKey } = serializeKey(key);

return mockedCache.get(serializedKey);
return cache.get(serializedKey);
};

export const dispatchEvent = (eventName: string, target: Element | Window | Document) => {
Expand Down

0 comments on commit e9d4e00

Please sign in to comment.