Skip to content

Commit

Permalink
feat(mutate): allow revalidate composables via mutate function
Browse files Browse the repository at this point in the history
  • Loading branch information
edumudu committed Sep 8, 2022
1 parent d00fd46 commit dcc177b
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 14 deletions.
17 changes: 15 additions & 2 deletions lib/composables/global-swr-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useScopeState } from '@/composables/scope-state';
export type MutateOptions = {
optimisticData?: unknown;
rollbackOnError?: boolean;
revalidate?: boolean;
};

export const useSWRConfig = () => {
Expand All @@ -17,14 +18,18 @@ export const useSWRConfig = () => {
computed(() => defaultConfig),
);

const cacheProvider = computed(() => contextConfig.value.cacheProvider);

const { revalidateCache } = useScopeState(cacheProvider);

const mutate = async <U extends unknown | Promise<unknown> | AnyFunction>(
_key: Key,
updateFnOrPromise?: U,
options: MutateOptions = {},
) => {
const { key } = serializeKey(_key);
const cachedValue = contextConfig.value.cacheProvider.get(key);
const { optimisticData, rollbackOnError } = options;
const { optimisticData, rollbackOnError, revalidate = true } = options;

if (!cachedValue) return;

Expand All @@ -41,7 +46,7 @@ export const useSWRConfig = () => {
}

try {
data.value = await resultPromise;
data.value = isUndefined(resultPromise) ? data.value : await resultPromise;
} catch (error) {
if (rollbackOnError) {
data.value = currentData;
Expand All @@ -50,6 +55,14 @@ export const useSWRConfig = () => {
throw error;
}

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

if (revalidate && revalidationCallbackcs.length) {
const [firstRevalidateCallback] = revalidationCallbackcs;

await firstRevalidateCallback();
}

return data.value;
};

Expand Down
25 changes: 25 additions & 0 deletions lib/composables/scope-state/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { computed, ComputedRef, toRefs, unref, watch } from 'vue';
import { MaybeRef, toReactive } from '@vueuse/core';

import { globalState } from '@/config';
import { CacheProvider, CacheState, ScopeState } from '@/types';

const initScopeState = (cacheProvider: CacheProvider<CacheState>) => {
globalState.set(cacheProvider, { revalidateCache: new Map() });
};

export const useScopeState = (_cacheProvider: MaybeRef<CacheProvider<CacheState>>) => {
const cacheProvider = computed(() => unref(_cacheProvider));
const scopeState = computed(() => globalState.get(cacheProvider.value));

const onScopeStateChange = () => {
if (!scopeState.value) initScopeState(cacheProvider.value);
};

watch(scopeState, onScopeStateChange, { immediate: true });

return {
scopeState: scopeState as ComputedRef<ScopeState>,
...toRefs(toReactive(scopeState as ComputedRef<ScopeState>)),
};
};
59 changes: 59 additions & 0 deletions lib/composables/scope-state/scope-state.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useInjectedSetup, mockedCache } from '@/utils/test';
import { globalState } from '@/config';

import { useScopeState } from '.';

const cacheProvider = mockedCache;

const useScopeStateWrapped = () =>
useInjectedSetup(
() => {},
() => useScopeState(cacheProvider),
);

describe('useSWR - mutate', () => {
beforeEach(() => {
vi.resetAllMocks();
cacheProvider.clear();
});

beforeAll(() => {
vi.useFakeTimers();
});

afterAll(() => {
vi.useRealTimers();
});

it('should init scope in case that the scope does not have values', async () => {
const { scopeState, revalidateCache } = useScopeStateWrapped();

expect(scopeState.value).not.toBeUndefined();
expect(scopeState.value.revalidateCache).toBeInstanceOf(Map);

// toRefs values
expect(revalidateCache.value).toBeInstanceOf(Map);
});

it('should return values for current scope', async () => {
const key = 'key1';
const revalidateCb = vi.fn();

globalState.set(cacheProvider, {
revalidateCache: new Map([[key, [revalidateCb]]]),
});

const { scopeState, revalidateCache } = useScopeStateWrapped();

expect(scopeState.value.revalidateCache).toBeInstanceOf(Map);
expect(scopeState.value.revalidateCache.size).toBe(1);
expect(scopeState.value.revalidateCache.get(key)).toHaveLength(1);
expect(scopeState.value.revalidateCache.get(key)).toContain(revalidateCb);

// toRefs values
expect(revalidateCache.value).toBeInstanceOf(Map);
expect(revalidateCache.value.size).toBe(1);
expect(revalidateCache.value.get(key)).toHaveLength(1);
expect(revalidateCache.value.get(key)).toContain(revalidateCb);
});
});
40 changes: 28 additions & 12 deletions lib/composables/swr/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { computed, readonly, watch, toRefs, unref, customRef } from 'vue';
import { toReactive, useEventListener, useIntervalFn } from '@vueuse/core';
import { computed, readonly, watch, toRefs, unref, customRef, onUnmounted } from 'vue';
import { createUnrefFn, toReactive, useEventListener, useIntervalFn } from '@vueuse/core';

import type {
MaybeRef,
Expand All @@ -8,10 +8,11 @@ import type {
SWRFetcher,
SWRKey,
} from '@/types';
import { serializeKey } from '@/utils';
import { serializeKey, subscribeCallback } from '@/utils';
import { mergeConfig } from '@/utils/merge-config';
import { isClient } from '@/config';
import { useSWRConfig } from '@/composables/global-swr-config';
import { useScopeState } from '@/composables/scope-state';

type UseCachedRefOptions = {
cache: any;
Expand Down Expand Up @@ -45,6 +46,8 @@ export const useSWR = <Data = any, Error = any>(
config: SWRComposableConfig = {},
) => {
const { config: contextConfig, mutate } = useSWRConfig();
const { revalidateCache } = useScopeState(contextConfig.value.cacheProvider);

const mergedConfig = mergeConfig(contextConfig.value, config);

const {
Expand Down Expand Up @@ -104,6 +107,8 @@ export const useSWR = <Data = any, Error = any>(
}
};

let unsubRevalidateCb: ReturnType<typeof subscribeCallback> | undefined;

const onRefresh = () => {
const shouldSkipRefreshOffline = !refreshWhenOffline && !navigator.onLine;
const shouldSkipRefreshHidden = !refreshWhenHidden && document.visibilityState === 'hidden';
Expand All @@ -121,6 +126,24 @@ export const useSWR = <Data = any, Error = any>(
fetchData();
};

const onRevalidate = async () => {
if (!key.value) {
return;
}

await fetchData();
};

const onKeyChange = (newKey: string, oldKey?: string) => {
if (!!newKey && newKey !== oldKey && (revalidateIfStale || !data.value)) {
fetchData();
}

unsubRevalidateCb?.();

subscribeCallback(newKey, onRevalidate, revalidateCache.value);
};

if (isClient && revalidateOnFocus && (revalidateIfStale || !data.value)) {
useEventListener(window, 'focus', onWindowFocus);
}
Expand All @@ -133,15 +156,8 @@ export const useSWR = <Data = any, Error = any>(
useIntervalFn(onRefresh, refreshInterval);
}

watch(
key,
(newKey, oldKey) => {
if (!!newKey && newKey !== oldKey && (revalidateIfStale || !data.value)) {
fetchData();
}
},
{ immediate: true },
);
watch(key, onKeyChange, { immediate: true });
onUnmounted(() => unsubRevalidateCb?.());

if (!hasCachedValue.value) {
cacheProvider.set(key.value, {
Expand Down
139 changes: 139 additions & 0 deletions lib/composables/swr/swr-mutate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { nextTick } from 'vue';

import { SWRComposableConfig } from '@/types';
import { useInjectedSetup, setDataToMockedCache, mockedCache } from '@/utils/test';
import { globalState } from '@/config';

import { useSWR } from '.';
import { configureGlobalSWR, useSWRConfig } from '../global-swr-config';

const cacheProvider = mockedCache;
const defaultKey = 'defaultKey';
const defaultFetcher = vi.fn((key: string) => key);
const defaultOptions: SWRComposableConfig = { dedupingInterval: 0 };

const setTimeoutPromise = (ms: number, resolveTo: unknown) =>
new Promise((resolve) => {
setTimeout(() => resolve(resolveTo), ms);
});

const useSWRWrapped: typeof useSWR = (...params) => {
return useInjectedSetup(
() => configureGlobalSWR({ cacheProvider, dedupingInterval: 0 }),
() => useSWR(...params),
);
};

describe('useSWR - mutate', () => {
beforeEach(() => {
vi.resetAllMocks();
cacheProvider.clear();
globalState.delete(cacheProvider);

vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(true);
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible');
});

beforeAll(() => {
vi.useFakeTimers();
});

afterAll(() => {
vi.useRealTimers();
});

it('should change local data variable value when mutate resolves', async () => {
const { mutate, data } = useSWRWrapped(defaultKey, () => 'FetcherResult');

await nextTick();
expect(data.value).toEqual('FetcherResult');

await mutate(() => 'newValue', { revalidate: false });
await nextTick();
expect(data.value).toEqual('newValue');
});

it('should change local data variable value when mutate is called with `optimistcData`', async () => {
setDataToMockedCache(defaultKey, { data: 'cachedData' });

const { mutate, data } = useInjectedSetup(
() => configureGlobalSWR({ cacheProvider }),
() => useSWR(defaultKey, () => 'FetcherResult'),
);

expect(data.value).toEqual('cachedData');

mutate(() => setTimeoutPromise(1000, 'newValue'), { optimisticData: 'optimistcData' });
await nextTick();
expect(data.value).toEqual('optimistcData');
});

it('should update all hooks with the same key when call mutates', async () => {
setDataToMockedCache(defaultKey, { data: 'cachedData' });

const { datas, mutate, differentData } = useInjectedSetup(
() => configureGlobalSWR({ cacheProvider }),
() => {
const { data: data1, mutate: localMutate } = useSWR(defaultKey, defaultFetcher);
const { data: data2 } = useSWR(defaultKey, defaultFetcher);
const { data: data3 } = useSWR(defaultKey, defaultFetcher);
const { data: data4 } = useSWR(defaultKey, defaultFetcher);
const { data: differentData1 } = useSWR('key-2', () => 'should not change');

return {
differentData: differentData1,
datas: [data1, data2, data3, data4],
mutate: localMutate,
};
},
);

expect(datas.map((data) => data.value)).toEqual([
'cachedData',
'cachedData',
'cachedData',
'cachedData',
]);

await mutate(() => 'mutated value');
expect(datas.map((data) => data.value)).toEqual([
'mutated value',
'mutated value',
'mutated value',
'mutated value',
]);

expect(differentData.value).toEqual('should not change');
});

it('should trigger revalidation programmatically', async () => {
let value = 0;

const { mutate, data, globalMutate } = useInjectedSetup(
() => configureGlobalSWR({ cacheProvider }),
() => {
const { mutate: localGlobalMutate } = useSWRConfig();
// eslint-disable-next-line no-plusplus
const swrResult = useSWR(defaultKey, () => value++, { dedupingInterval: 0 });

return {
globalMutate: localGlobalMutate,
...swrResult,
};
},
);

await nextTick();
expect(data.value).toEqual(0);

await mutate();

await nextTick();
expect(data.value).toEqual(1);

await globalMutate(defaultKey);

await nextTick();
expect(data.value).toEqual(2);
});
});
8 changes: 8 additions & 0 deletions lib/config/global-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { reactive } from 'vue';

import { CacheProvider, CacheState, ScopeState } from '@/types';

/**
* Holds the scope's states, isolated using cacheProvider as key
*/
export const globalState = reactive(new WeakMap<CacheProvider<CacheState>, ScopeState>());
1 change: 1 addition & 0 deletions lib/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './swr-config';
export * from './injection-keys';
export * from './enviroment';
export * from './global-state';
4 changes: 4 additions & 0 deletions lib/types/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export type CacheState = {
fetchedIn: Date;
};

export type ScopeState = {
revalidateCache: Map<string, Array<() => void | Promise<void>>>; // callbacks to revalidate when key changes
};

export type SWRConfig<Data = any, Err = any> = {
/**
* stores the cached values
Expand Down
1 change: 1 addition & 0 deletions lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './hash';
export * from './serialize-key';
export * from './merge-config';
export * from './chain-fn';
export * from './subscribe-key';
Loading

0 comments on commit dcc177b

Please sign in to comment.