javascript - mutația SWR nu actualizează cache-ul folosind o metodă statică a clasei ca fetcher


Folosesc pachetul swr. Obțin datele cu succes folosind useSWR, dar atunci când încerc să modific datele - nu funcționează, iar starea memorată în cache a swr nu se schimbă (așa cum ar trebui).

Mi-am creat hook-ul personalizat:

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;

Și acesta este BackendService-ul meu:

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;

Acum, am următorul cod:

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);

Așadar, atunci când onRevokeAllSecrets este executat - starea memorată în cache nu se schimbă.

Ar putea cineva să-mi spună de ce? Am verificat și apelul meu BackendService.delete se finalizează cu succes.


Am încercat, de asemenea, să schimb hook-ul meu personalizat useBackend după cum urmează:

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;

Dar tot nu a ajutat (aceeași problemă exactă)


Am încercat, de asemenea, să ofer revalidate pentru mutație:

        await getAllSecretsMutate(
            async () => {
                await BackendService.delete('/user/secrets');

                return { secrets: [] };
            },
            {
                optimisticData: { secrets: [] },
                rollbackOnError: true,
                revalidate: false,
            },
        );

Apoi a funcționat - cache-ul s-a schimbat. Dar navighez la altă pagină și apoi revin - cache-ul este din nou invalid.

Oricum, dacă adăugarea lui revalidate: false cumva actualizează cache-ul, atunci aș spune că atunci când am folosit revalidate: true (implicit) revalidarea, odată ce actualizarea asincronă se rezolvă, aduce date nenule de pe serverul meu? Dar asta e tot - am verificat răspunsul de la server și a răspuns cu un array gol:


De asemenea, nu cred că comportamentul metodelor static cauzează acea problemă deoarece am refactorizat BackendService într-un mod asemănător cu acesta:

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;

Și nu a ajutat.

Răspuns #1

Este aceasta o problemă cu backend-ul tău?

SWR se așteaptă ca valoarea pe care o oferi cu mutate să fie aceeași sau cea mai bună aproximare a ceea ce ar obține dacă ar apela din nou GET /user/secrets.

Funcția de revalidare este apoi utilizată pentru a asigura că datele din cache sunt cele mai actualizate date furnizate de funcția ta de preluare (GET /user/secrets).

Când furnizați revalidate: false ca una dintre opțiunile pentru mutate, nu va prelua cele mai recente date furnizate de funcția dvs. de fetch (GET /user/secrets) și, în schimb, va presupune că garantați că valoarea furnizată de dvs. este la zi.

Când montezi un component cu useSWR, configurația sa implicită este setată să revalideze datele dacă nu este deja în procesul de a face acest lucru. De aceea, atunci când ai navigat departe de pagină și apoi înapoi la pagina în cauză, datele din cache au fost actualizate cu datele furnizate de funcția ta fetcher (GET /user/secrets).

Scenario Mutat:

  1. Datele X preluate prin GET /user/secrets sunt stocate în cache.
  2. mutate este declanșat.
  3. DELETE /user/secrets a fost apelat.
  4. Tablou gol stocat în cache.
  5. revalidarea este declanșată.
  6. Datele Y (posibil aceleași cu Datele X) obținute prin GET /user/secrets stocate în cache.

Scenariu de navigare:

  1. Datele X preluate prin GET /user/secrets sunt stocate în cache.
  2. mutate este declanșat.
  3. DELETE /user/secrets a fost apelat.
  4. Tablou gol stocat în cache.
  5. Utilizatorul navighează de pe pagină, demontând SecretManagement.
  6. Utilizatorul navighează către pagină, montând SecretManagement.
  7. revalidarea este declanșată.
  8. Datele Y (posibil aceleași cu Datele X) obținute prin GET /user/secrets stocate în cache.

TLDR:

Având în vedere modul în care încerci să folosești SWR, se poate presupune că după ce se apelează DELETE /user/secrets, GET /user/secrets ar returna un array gol (ceea ce se pare că nu se întâmplă).

Răspunsul #2

Am pus codul tău într-un sandbox, cu un fetch simulant, și funcționează...

ar putea exista un bug în codul de ștergere din backend?

când apelezi SWR mutate, va pune valoarea returnată în cache ȘI va re-prelua datele, apelează din nou get("user/secrets"). vezi documentația.

Dacă setezi revalidate: false, SWR nu va re-prelua datele și vei vedea valoarea actualizată optimist (până când componenta este remontată - navighezi - și datele sunt re-preluate). Acesta este motivul pentru care cred că ștergerea de pe backend nu șterge cu adevărat secretele.

sper că ajută