I'm using the swr
package. I successfully fetch data using useSWR
, but when I try to mutate the data - it does not work, and the cached state of swr
does not change (as it should).
I created my custom hook:
import useSWR from 'swr';
import BackendService from '@/services/backend';
const useBackend = <D, E = unknown>(path: string | null) => {
const { data, error, isLoading, mutate } = useSWR<D, E>(path, BackendService.get);
return { data, error, isLoading, mutate };
};
export default useBackend;
And this is my BackendService
:
import { preload } from 'swr';
import type { IHttpMethod } from '@/interfaces/http';
class BackendService {
private static routesWithRefreshToken: string[] = ['/user/auth'];
private static fetcher = async <R = unknown, D = unknown>(
path: string,
method: IHttpMethod,
data?: D,
) => {
const requestPath = import.meta.env.VITE_BACKEND_URL + path;
const withRefresh = this.routesWithRefreshToken.includes(path);
const token = withRefresh ? localStorage.getItem('token') : sessionStorage.getItem('token');
const res = await fetch(requestPath, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
});
if (!res.ok) {
throw new Error();
}
const resData = await res.json().catch(() => undefined);
return resData as R;
};
public static get = <R = unknown>(path: string) => {
return this.fetcher<R, null>(path, 'GET');
};
public static post = <R = unknown, D = unknown>(path: string, data?: D) => {
return this.fetcher<R, D>(path, 'POST', data);
};
public static patch = <R = unknown, D = unknown>(path: string, data?: D) => {
return this.fetcher<R, D>(path, 'PATCH', data);
};
public static delete = <R = unknown>(path: string) => {
return this.fetcher<R, null>(path, 'DELETE');
};
public static preload = (path: string) => {
return preload(path, this.get);
};
}
export default BackendService;
Now, I have the following code:
import React, { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import type { IGetAllSecretsResponseData } from '@exlint.io/common';
import useBackend from '@/hooks/use-backend';
import BackendService from '@/services/backend';
import SecretManagementView from './SecretManagement.view';
interface IProps {}
const SecretManagement: React.FC<IProps> = () => {
const navigate = useNavigate();
const { data: getAllSecretsResponseData, mutate: getAllSecretsMutate } =
useBackend<IGetAllSecretsResponseData>('/user/secrets');
const hasSecrets = useMemo(() => {
if (!getAllSecretsResponseData) {
return false;
}
return getAllSecretsResponseData.secrets.length > 0;
}, [getAllSecretsResponseData]);
const onRevokeAllSecrets = async () => {
await getAllSecretsMutate(
async () => {
await BackendService.delete('/user/secrets');
return {
secrets: [],
};
},
{
optimisticData: { secrets: [] },
rollbackOnError: true,
},
);
navigate('', { replace: true });
};
return <SecretManagementView hasSecrets={hasSecrets} onRevokeAllSecrets={onRevokeAllSecrets} />;
};
SecretManagement.displayName = 'SecretManagement';
SecretManagement.defaultProps = {};
export default React.memo(SecretManagement);
So when the onRevokeAllSecrets
is executed - the cached state does not change.
Could anyone tell why? I checked and my BackendService.delete
call completes successfully.
I have also tried to change my custom hook useBackend
as follows:
import { useCallback } from 'react';
import useSWR, { useSWRConfig, type KeyedMutator } from 'swr';
import BackendService from '@/services/backend';
const useBackend = <D, E = unknown>(path: string | null) => {
const { mutate: globalMutate } = useSWRConfig();
const { data, error, isLoading } = useSWR<D, E>(path, BackendService.get);
const mutator: KeyedMutator<D> = useCallback(
(data, options) => globalMutate(path, data, options),
[path],
);
return {
data,
error,
isLoading,
mutate: mutator,
};
};
export default useBackend;
But it still didn't help (same exact issue)
I have also tried to provide revalidate
to the mutation:
await getAllSecretsMutate(
async () => {
await BackendService.delete('/user/secrets');
return { secrets: [] };
},
{
optimisticData: { secrets: [] },
rollbackOnError: true,
revalidate: false,
},
);
Then it worked - the cache did change. But I navigate to other page and then come back - the cache is invalid again.
Anyway, if adding revalidate: false somehow does update the cache, then I'd say that when I used revalidate: true (default) the revalidation once the asynchronous update resolves brings non-empty data from my server? But that's it - I checked the response from the server and it responded with empty array:
Also, I don't think the behavior of static
methods causes that issue because I refactored the BackendService
to something like this:
import { preload } from 'swr';
import type { IHttpMethod, IRefreshTokenRoute } from '@/interfaces/http';
const routesWithRefreshToken: IRefreshTokenRoute[] = [
{ method: 'GET', path: '/user/auth' },
{ method: 'GET', path: '/user/auth/refresh-token' },
];
const fetcher = async <R = unknown, D = unknown>(path: string, method: IHttpMethod, data?: D) => {
const endpointPath = import.meta.env.VITE_BACKEND_URL + path;
const withRefresh = !!routesWithRefreshToken.find(
(route) => route.method === method && route.path === path,
);
const token = (withRefresh ? localStorage : sessionStorage).getItem('token');
const res = await fetch(endpointPath, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
});
if (!res.ok) {
throw new Error();
}
const resData = await res.json().catch(() => undefined);
return resData as R;
};
const get = <R = unknown>(path: string) => {
return fetcher<R, null>(path, 'GET');
};
const post = <R = unknown, D = unknown>(path: string, data?: D) => {
return fetcher<R, D>(path, 'POST', data);
};
const patch = <R = unknown, D = unknown>(path: string, data?: D) => {
return fetcher<R, D>(path, 'PATCH', data);
};
const deleter = <R = unknown>(path: string) => {
return fetcher<R, null>(path, 'DELETE');
};
const preloader = (path: string) => {
return preload(path, get);
};
const BackendService = {
fetcher,
get,
post,
patch,
deleter,
preloader,
};
export default BackendService;
And it did not help.