Reagujte na vzor stavu načítání pomocí Toast &SWR

Úvod

Správa stavu načítání reakce může být trochu otravná, musíme ji před načítáním nastavit na isLoading a po dokončení ji nastavit zpět na false. Potom jej také musíme nastavit na tlačítko, abychom mohli zobrazit stav načítání nebo dát nějaký text jako indikátor.

Takto to vypadá s běžným vzorem načítání:

const [pokemon, setPokemon] = React.useState<Array<Pokemon>>();
const [isLoading, setIsLoading] = React.useState<boolean>(false);

const getData = () => {
  setIsLoading(true);

  axios
    .get<PokemonList>('https://pokeapi.co/api/v2/pokemon?limit=20')
    .then((res) => {
      setPokemon(res.data.results);
    })
    .finally(() => setIsLoading(false));
};

return <button disabled={isLoading}>{isLoading ? 'loading' : 'fetch'}</button>;

je to nepříjemné a ještě jsme se ani nezabývali chybovým stavem.

Co bychom měli spravovat v procesu načítání?

Když načítáme data, musíme udělat nějaké věci, aby bylo čekání snesitelnější. Zde je několik věcí, které můžeme udělat:

Indikátor načítání

Uživatelé potřebují vědět, kdy je jejich aplikace ve stavu načítání. To je důležité, aby nečekali tupě a získali myšlení, že by měli chvíli počkat.

Indikátor načítání může být číselník, normální text, některé animace nebo toast.

Indikátor úspěchu

Musíme uživateli sdělit, zda načítání proběhlo úspěšně, aby mohl pokračovat ve své práci.

Indikátor chyby

Když se načítání dat pokazí, musíme o tom uživatele informovat.

Akce blokování

Běžným příkladem je, že když odesíláme formulář, nechceme, aby uživatel odeslal dvakrát. Můžeme to udělat deaktivací tlačítka, když probíhá stav načítání.

Dalším příkladem je zablokování tlačítka modálního uzavření při načítání, aby jej uživatel omylem nezavřel.

Snadný způsob

Zjistil jsem, že tento vzor je nejvíce bezproblémový a můžeme použít vlastní háčky k zachycení stavu načítání.

Zde je to, co budeme stavět:

Popis videa:

  1. klikne se na tlačítko getData a poté se zobrazí načítání toastu.
  2. Při načítání je tlačítko deaktivováno a zobrazuje načítání číselník
  3. Po 2 sekundách se načítání toastu změní na chybný toast
  4. znovu se klikne na tlačítko getData a poté se zobrazí načítání toastu
  5. Po 2 sekundách se načítání toastu změní na úspěšný toast a poté se všechna data načtou správně.

ps:kurzor čekání je v nahrávce nějak divný.

S tímto vzorem snadno pokryjeme všechny 4 věci .

  1. Stav načítání získáme pomocí toastu
  2. Můžeme zobrazit indikátor chyby a zobrazit chybovou zprávu z rozhraní API
  3. Můžeme zobrazit indikátor úspěchu
  4. Všechna tlačítka jsou deaktivována.

Používáme React Hot Toast pro indikátor načítání, úspěchu a chyb. Všechny jsou spravovány pouze pomocí 1 funkce obalu, jako je tato:

toast.promise(
  axios
    .get<PokemonList>('https://pokeapi.co/api/v2/pokemon?limit=20')
    .then((res) => {
      setPokemon(res.data.results);
    }),
  {
    loading: 'Loading...',
    success: 'Data fetched successfully',
    error: (err: any) =>
      err?.response?.data?.msg ?? 'Something is wrong, please try again',
  }
);

Konfigurace

Nejprve musíme nainstalovat reagovat-hot-toast

yarn add react-hot-toast

Pro demo používám Next.js, ale konfigurace pro CRA je v podstatě stejná. Přidejte to do _app.tsx

import { AppProps } from 'next/app';

import '@/styles/globals.css';

import DismissableToast from '@/components/DismissableToast';

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <DismissableToast />
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

Přidal jsem zavřít tlačítko, protože ho ve výchozím nastavení nemá, můžete chytit DismissableToast kód z mé knihovny.

Použití

Řekněme, že chceme načíst data při připojení z externího API pomocí Axios.

Jen potřebujeme zabalit volání axios funkcí toast.

React.useEffect(() => {
  toast.promise(
    axios
      .get<PokemonList>('https://pokeapi.co/api/v2/pokemon?limit=20')
      .then((res) => {
        setPokemon(res.data.results);
      }),
    {
      loading: 'Loading...',
      success: 'Data fetched successfully',
      error: (err: any) =>
        err?.response?.data?.msg ?? 'Something is wrong, please try again',
    }
  );
}, []);

A je to! Toast zobrazí stav při načítání a kdy je úspěšný nebo chybný.

Další opětovné použití

Můžete jej vytvořit ještě více, když deklarujete defaultToastMessage a v případě potřeby jej přepište.

export const defaultToastMessage = {
  loading: 'Loading...',
  success: 'Data fetched successfully',
  // you can type this with axios error
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  error: (err: any) =>
    err?.response?.data?.msg ?? 'Something is wrong, please try again',
};

toast.promise(axios, {
  ...defaultToastMessage,
  loading: 'Override loading',
});

Přístup ke stavu načítání

Můžeme to udělat s toastovým API, které jsem zabalil do vlastního háčku.

import { useToasterStore } from 'react-hot-toast';

/**
 * Hook to get information whether something is loading
 * @returns true if there is a loading toast
 * @example const isLoading = useLoadingToast();
 */
export default function useLoadingToast(): boolean {
  const { toasts } = useToasterStore();
  const isLoading = toasts.some((toast) => toast.type === 'loading');
  return isLoading;
}

A můžeme to použít právě takto

const isLoading = useLoadingToast();

<button disabled={isLoading}></button>;

S isLoading stavu, zbytek je veškerá vaše kreativita, můžete ukázat nějakou kostru, změnit text načítání, dát číselníky načítání, cokoliv chcete.

Gotcha:2 Axios Calls

Pokud máte 2 volání axios, můžete zřetězit další volání axios a přidat další then získat hodnotu.

toast.promise(
  axios
    .post('/user/login', data)
    .then((res) => {
      const { jwt: token } = res.data.data;
      tempToken = token;
      localStorage.setItem('token', token);

      // chaining axios in 1 promise
      return axios.get('/user/get-user-info');
    })
    .then((user) => {
      const role = user.data.data.user_role;
      dispatch('LOGIN', { ...user.data.data, token: tempToken });

      history.replace('/');
    }),
  {
    ...defaultToastMessage,
  }
);

Integrace SWR

Použití SWR k načtení dat je ještě úžasnější, protože stav načítání potřebujeme ukázat pouze při prvním načtení. Zde je ukázka:

Popis videa:

  1. Při první návštěvě se zobrazí načítaný toast a poté se změní v úspěšný toast.
  2. Při druhé návštěvě nedochází k načítání toastu a data jsou předem vyplněna mezipamětí.

Toto je syntaxe SWR:

const { data, error } = useSWR<PokemonList>(
  'https://pokeapi.co/api/v2/pokemon?limit=20'
);

Můžeme použít jiný vlastní háček ✨

Udělal jsem tento háček, abychom mohli zabalit useSWR stejně jako toast.promise funkce.

useWithToast pro SWR

import * as React from 'react';
import toast from 'react-hot-toast';
import { SWRResponse } from 'swr';

import { defaultToastMessage } from '@/lib/helper';

import useLoadingToast from '@/hooks/useLoadingToast';

type OptionType = {
  runCondition?: boolean;
  loading?: string;
  success?: string;
  error?: string;
};

export default function useWithToast<T, E>(
  swr: SWRResponse<T, E>,
  { runCondition = true, ...customMessages }: OptionType = {}
) {
  const { data, error } = swr;

  const toastStatus = React.useRef<string>(data ? 'done' : 'idle');

  const toastMessage = {
    ...defaultToastMessage,
    ...customMessages,
  };

  React.useEffect(() => {
    if (!runCondition) return;

    // if toastStatus is done,
    // then it is not the first render or the data is already cached
    if (toastStatus.current === 'done') return;

    if (error) {
      toast.error(toastMessage.error, { id: toastStatus.current });
      toastStatus.current = 'done';
    } else if (data) {
      toast.success(toastMessage.success, { id: toastStatus.current });
      toastStatus.current = 'done';
    } else {
      toastStatus.current = toast.loading(toastMessage.loading);
    }

    return () => {
      toast.dismiss(toastStatus.current);
    };
  }, [
    data,
    error,
    runCondition,
    toastMessage.error,
    toastMessage.loading,
    toastMessage.success,
  ]);

  return { ...swr, isLoading: useLoadingToast() };
}

Navíc jsem do return přidal isLoading, takže nemusíme volat useLoadingToast už háčky

Použití

const { data: pokemonData, isLoading } = useWithToast(
  useSWR<PokemonList>('https://pokeapi.co/api/v2/pokemon?limit=20')
);

Skvělé, vypadá to dobře a čistě.

Zprávy toastů můžete stále přepsat takto

const { data: pokemonData, isLoading } = useWithToast(
  useSWR<PokemonList>('https://pokeapi.co/api/v2/pokemon?limit=20'),
  {
    loading: 'Override Loading',
  }
);

Závěr

Doufám, že to rozšíří vaši sbírku vzorů.

Můžete se podívat na zdrojový kód ukázky na githubu, ale mějte na paměti, že existuje další příslib zpoždění načítání.