Prepísať aplikáciu z Reactu do Svelte, čo môžete očakávať

Published: 2021-04-08

Nedávno som prepísal stredne veľkú aplikáciu v Reacte (~250 React komponentov) do Svelte. Výsledkom tejto akcie je aplikácia, ktorá je efektívnejšia, s jednoduchšou údržbou a oveľa čitateľnejším kódom. Ale boli tu aj nejaké problémy, niektoré situácie, ktoré nie sú priamočiare na transformáciu do Svelte kódu. Tento článok popisuje niektoré z týchto situácií.

Toto nie je tutoriál Svelte pre React programátorov, ak ešte nepoznáte Svelte, najlepším východiskovým bodom je oficiálny Svelte tutoriál. Tento článok obsahuje veľa odkazov na Svelte tutoriály, namiesto vysvetľovania princípov Svelte.

CSS

Svelte podporuje CSS priamo veľmi pekným a čistým spôsobom. V React aplikácii som používal styled components. Existuje mnoho CSS knižníc pre React, ale všetky (ako viem) používajú paradigmu CSS-in-JS, zatiaľ čo v Svelte používate celkom čisté CSS.

V nasledujúcom úryvku ukážem zjednodušenú implementáciu tlačidla na paneli nástrojov, v Reacte (so styled components) a so Svelte.

Tlačidlo na paneli nástrojov má jednu vlastnosť (disabled), ktorá deaktivuje funkciu hover. Tiež používa niektoré premenné z témy.

// React komponent ToolbarButton({disabled, toolBarHeight})

const ToolbarButton = styled.div`
  padding: 5px 15px;
  height: ${props.toolBarHeight}px;

  ${(props) =>
    !props.disabled &&
    `
  &:hover {
    background-color: gray;
  }
  `}
`;
<!-- Svelte komponent -->

<script>
  export let disabled;
</script>

<div class="button" class:disabled />

<style>
.button {
  padding: 5px 15px;
  height: var(--theme-toolbar-height); /* nemôžete použiť priamo prop, musí sa použiť CSS premenná */
}
.button.disabled {
  background-color: gray;
}
</style>

Svelte nepodporuje žiadne premenné v CSS. Takže nie je možné predať vlastnosť do Svelte komponentu a použiť ju v CSS. Jediný spôsob, ako to urobiť, je prepínať CSS triedu ON/OFF. Selektor button.disabled sa používa, keď je disabled nastavené na true.

Keď chcete použiť nejaké hodnoty (napr. rozmery v CSS), môžete použiť CSS premenné.

React hooks, najmä useEffect

React hooks sú veľmi silná funkcia. V Svelte nenájdete nič podobné. Ale väčšinu React hooks vám v Svelte nebude chýbať.

  • useState - stav je definovaný pomocou let príkazov
  • useMemo - môžete použiť reaktívne príkazy namiesto prepočítania vnútorného stavu
  • useCallback - funkčné výrazy sú vyhodnotené len raz, takže nemá zmysel
  • useContext - Svelte má veľmi jednoduché a priamočiare API pre kontext (getContext(), setContext())
  • useReducer - musíte vytvoriť Svelte verziu, pomocou obchodov (to môže byť veľmi jednoduché)
  • useRef - môžete použiť bind:this namiesto toho (alebo let premennú pre inštančné premenné)
  • useEffect - toto je zložitejšie…

useEffect - jednoduché použitie

Používam useEffect v Reacte veľmi často, bohužiaľ musíte premýšľať o tom, ako implementovať rovnakú logiku v Svelte.

Najjednoduchšie použitie je vykonanie nejakého kódu pri montáži a demontáži.

// React
React.useEffect(() => {
  console.log('MOUNT');
  return () => console.log('UNMOUNT');
}, []);

Táto varianta je pokrytá svelte metódami onMount a onDestroy

// Svelte

onMount(() => {
  console.log('MOUNT');
  return () => console.log('UNMOUNT 1');
});
onDestroy(() => {
  console.log('UNMOUNT 2');
});

Ako vidíte, máte dve varianty, ako vykonať kód pri demontáži - s funkciou onDestroy a s návratovou hodnotou funkcie onMount.

useEffect - reaktívny príkaz

Keď chcete vykonať nejaký kód pri zmene výrazu, môžete použiť Svelte reaktívny príkaz.

// React
React.useEffect(() => {
  console.log('HEIGHT changed, new value:', height);
}, [height]);
// Svelte
$: console.log('HEIGHT changed, new value:', height); 

V Svelte je reaktívny príkaz vykonaný, keď je priradená niektorá z jeho závislostí. Takže ak potrebujete niečo urobiť len pri zmene závislosti, musíte si hodnotu skontrolovať sami.

Ak chcete explicitne vymenovať závislosti (ako v druhom argumente metódy React.useEffect), môžete použiť tento vzor:

// Svelte

$: {
  height;
  width;
  handleChanged(); 
}

Toto bude volané po priradení premennej height alebo width. Môžete použiť iné premenné vo funkcii handleChanged, ale tieto závislosti nespustia tento reaktívny príkaz.

useEffect - reaktívny príkaz s čistením

React useEffect môže byť použitý aj na alokáciu niektorých zdrojov, ktoré sú závislé na hodnote vlastnosti. V nasledujúcom príklade komponent zobrazuje zoznam tabuliek v SQL databáze. Počúva na zmeny štruktúry databázy. Keď sa zmení vlastnosť connectionId, je potrebné odpojiť sa od starého socketu a pripojiť sa k novému socketu.

// React
function SqlTableList({ connectionId }) {
  React.useEffect(() => {
    socket.on(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
    return () => {
      socket.off(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
    };
  }, [connectionId]);
}

Svelte nepodporuje tento scenár tak priamočiaro ako React. Ale môžete použiť jednoduchý trik na dosiahnutie tohto správania.

// Svelte

const useEffect = subscribe => ({ subscribe });

$: effect = useEffect(() => {
  socket.on(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
  return () => {
    socket.off(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
  };
});

$: $effect;

Ako tento trik funguje? Musíte vedieť o obchodoch, najmä o vlastných obchodoch (tutoriál). Funkcia useEffects vytvára vlastný obchod s metódou subscribe. Metóda subscribe obchodu musí vrátiť metódu unsubscribe, ktorá je volaná, keď už nie je potrebné predplatné obchodu.

Posledný riadok ( $: $effect ) spravuje automatické predplatné obchodu (tutoriál). Takže správne volá socket.on a potom socket.off, keď sa zmení hodnota connectionId (alebo je len priradená v Svelte), podobne ako to robí React.

Prečo mi chýbajú React props v Svelte

Samozrejme, aj v Svelte sú props. Fungujú podobne ako v Reacte. Ale v Reacte sú jediným externým rozhraním komponentu props. V Svelte máte niekoľko mechanizmov, ako spravovať správanie vašich komponentov:

  • props - fungujú ako v Reacte
  • events - používajú špeciálnu syntax, udalosti nie sú súčasťou $$props objektu obsahujúceho všetky props
  • actions (syntax use:action) - mechanizmus, ako znovu použiť logiku viazanú na HTML elementy
  • slots - účel je rovnaký ako React property children, s niektorými rozšíreniami

Všetky tieto mechanizmy sú veľmi užitočné, ale nemajú jednotný prístup ako props v Reacte. Nižšie sú diskutované niektoré problémy, na ktoré som narazil.

Preposielanie udalostí

Ak vytvárate zložitejšie hierarchie komponentov, niektoré komponenty len preposielajú dáta z rodičovských komponentov.

// React - preposiela všetky udalosti, sú súčasťou props

function Outer(props) {
  return <Inner {...props} />;
}
 <!-- Svelte - len explicitne pomenované udalosti click a keydown sú preposielané -->

 <Inner {...$$props} on:click on:keydown />;
}

V Svelte nemôžete preposielať všetky udalosti, ale môžete preposielať vymenované udalosti. Ale ak potrebujete preposielať všetky udalosti, môžete použiť spätné volania (onClick namiesto on:click), potom onClick bude súčasťou $$props.

Implementácia TabControl

V Reacte je celkom jednoduché implementovať komponent TabControl, ktorý bude použitý nasledovne:

// React

<TabControl>
  <TabPage label='Page 1'>
    Page 1 content
  </TabPage>
  <TabPage label='Page 2'>
    Page 2 content
  </TabPage>
</TabControl>

Implementácia prejde cez pole children a môže ľahko extrahovať požadované informácie (názov stránky a obsah). Tento prístup nefunguje v Svelte, nemá nič ako children. Môžete použiť svelte fragmenty, použitie bude vyzerať nasledovne:

 <!-- Svelte -->

<TabControl tabs={[
  { label: 'Page 1', slot: 1},
  { label: 'Page 2', slot: 2},
  ]}>
  <svelte:fragment slot='1'>
    Page 1 content
  </svelte:fragment>
  <svelte:fragment slot='2'>
    Page 2 content
  </svelte:fragment>
</TabControl>

Karty sú definované v poli, rozloženie kariet (children) je definované vo fragmentoch, ktoré sú odovzdané ako sloty do komponentu TabControl. Nie je to tak intuitívne ako v Reacte, ale funguje to. Iba jeden dosť veľký úskok, ktorý môžete vidieť v implementácii TabControl:

// TabControl.svelte

<script>
  export let tabs = []
</script>

<div>
 {#each _.compact(tabs) as tab, index}
   <div class="container" class:isInline class:tabVisible={index == value}>
     {#if tab.slot == 0}<slot name="0" />
     {:else if tab.slot == 1}<slot name="1" />
     {:else if tab.slot == 2}<slot name="2" />
     {:else if tab.slot == 3}<slot name="3" />
     {:else if tab.slot == 4}<slot name="4" />
     {:else if tab.slot == 5}<slot name="5" />
     {:else if tab.slot == 6}<slot name="6" />
     {:else if tab.slot == 7}<slot name="7" />
     {/if}
   </div>
 {/each}
</div>

Názvy slotov musia byť statické reťazce, takže musíte urobiť niečo takéto, aby to fungovalo.

Použitie dynamického komponentu

Iný prístup by mohol byť definovať komponent pre každú kartu, ale potom budete mať veľa malých súborov, pretože každý Svelte komponent musí byť definovaný vo vlastnom súbore.

 <!-- Svelte -->

<TabControl tabs={[
  { label: 'Page 1', component: Tab1},
  { label: 'Page 2', component: Tab2},
  ]}/>

// Tab1.svelte
Page 1 content
// Tab2.svelte
Page 2 content

Implementácia použije svelte:component na inštancovanie správnej karty.

Použitie kontextu

Toto je najmenej zrejmý spôsob. Ale v skutočnosti môžete dosiahnuť rovnakú syntax ako v Reacte.

 <!-- Svelte -->

<TabControl>
  <TabPage label='Page 1'>
    Page 1 content
  </TabPage>
  <TabPage label='Page 2'>
    Page 2 content
  </TabPage>
</TabControl>

V komponente TabControl musí byť definovaný nejaký “zberný bod”, napr. pole detských kariet

<!-- TabControl.svelte -->
<script>
  const tabs = [];
  setContext('tabs', tabs);
</script>

A v karte stránky len zaregistrujeme kartu v rodičovskom poli:

<!-- TabPage.svelte -->
<script>
  export let label;
  const tabs = getContext('tabs');
  tabs.push({ label });
</script>

Jediný úskok tejto metódy je, že nezachováva definovaný poriadok kariet, ak sú niektoré z nich renderované podmienečne.


<TabControl>
  {#if condition_will_be_true_later}
    <TabPage label='Page 1'>
      Page 1 content
    </TabPage>
  {/if}
  <TabPage label='Page 2'>
    Page 2 content
  </TabPage>
</TabControl>

Toto bude renderované s prvou Page 2 a potom Page 1, čo pravdepodobne nie je požadovaný výsledok.

Pozor pri použití bind:clientHeight a bind:clientWidth

Viazanie rozmerov (tutoriál) je skvelá funkcia. Ale buďte opatrní pri jeho použití. Je implementované so skrytým iframe (môžete to vidieť v tomto REPL). Z mojich skúseností, niekedy v zložitejších situáciách to nefungovalo vo FireFoxe. Niekedy je bezpečnejšie použiť ResizeObserver (pozrite si túto implementáciu akcie)

Error boundary

React má skvelý koncept error boundaries (“try-catch” pre komponenty). Nie je podporovaný vo funkčných komponentoch (v skutočnosti, ErrorBoundary bol jediný triedny komponent v mojej React aplikácii), ale nebol problém použiť tento triedny komponent vo funkčnej React aplikácii.

// React
<ErrorBoundary>
  {(null).read()}
</ErrorBoundary>

Bez error boundary, keď máte tento kód v React aplikácii, celá aplikácia zlyhá. S error boundary zlyhá len vnútro boundary, ostatné časti aplikácie sa vykreslia ako obvykle.

Bohužiaľ, Svelte nemá nič také. Má NPM balík nazvaný svelte-error-boundary, ale v skutočnosti rieši len malú časť problémov a väčšina chýb v Svelte aplikácii stále spôsobuje pád aplikácie.

Svelte má svoj vlastný spôsob pádu - Svelte komponenty prestanú byť reaktívne, takže aplikácia vyzerá ako zamrznutá.

Jediné, čo môžete urobiť, je detekovať túto situáciu a nechať používateľa obnoviť stránku.

<!-- Svelte -->

<script>
  let counter = 0;
  $: counterCopy = counter;
  const onunhandledrejection = async e => {
    console.log('Nezachytená chyba, kontrola, či došlo k pádu', e);
    const oldCounter = counter;
    counter++;
    window.setTimeout(() => {
      if (counterCopy <= oldCounter) {
        console.log('DETEKOVANÝ PÁD!!!');
        if (window.confirm('Prepáčte, aplikácia zlyhala.\nObnoviť stránku?')) {
          window.location.reload();
        }
      }
    }, 500);
  };
</script>

<svelte:window on:unhandledrejection={onunhandledrejection} />

Na detekciu sa používa udalosť unhandledrejection. Táto udalosť môže byť vyvolaná v rôznych situáciách, niektoré z nich nespôsobujú pád Svelte. Toto je dôvod použitia premenných counter a counterCopy. Ak reaktívny príkaz nefunguje, znamená to, že celé Svelte je zlyhané a jediný spôsob, ako sa zotaviť, je obnoviť stránku.

Môžete vidieť celý ErrorHandler komponent.

Záver

Napriek týmto problémom je Svelte skvelý rámec a som veľmi spokojný s výsledkom prepisovania mojej aplikácie z Reactu do Svelte. Niektoré funkcie, ktoré sú teraz implementované so Svelte, by bolo takmer nemožné urobiť s Reactom.

Samozrejme, ak používate veľa knižníc tretích strán, ktoré sú dostupné len pre React, môže to byť vážny problém. Ale to nebol môj prípad, mal som minimálne závislosti s Reactom, v tomto prípade môže byť prepisovanie do Svelte celkom jednoduché a rýchle.