Přepsání aplikace z Reactu do Svelte, co můžete očekávat

Published: 2021-04-08

Nedávno jsem přepsal středně velkou aplikaci v Reactu (~250 React komponent) do Svelte. Výsledkem této akce je aplikace, která je efektivnější, s jednodušší údržbou a mnohem čitelnějším kódem. Ale byly zde některé problémy, některé situace, které nejsou přímočaré k transformaci do Svelte kódu. Tento článek popisuje některé z těchto situací.

Toto není tutoriál Svelte pro programátory v Reactu, pokud ještě neznáte Svelte, nejlepším výchozím bodem je oficiální Svelte tutoriál. Tento článek obsahuje mnoho odkazů na Svelte tutoriály, místo vysvětlování principů Svelte.

CSS

Svelte podporuje CSS přímo velmi pěkným a čistým způsobem. V React aplikaci jsem používal styled components. Existuje mnoho CSS knihoven pro React, ale všechny (jak vím) používají paradigma CSS-in-JS, zatímco v Svelte používáte docela čisté CSS.

V následujícím úryvku ukážu zjednodušenou implementaci tlačítka nástrojové lišty, v Reactu (s styled components) a se Svelte.

Tlačítko nástrojové lišty má jednu vlastnost (disabled), která zakazuje funkci hover. Také používá některé proměnné z tématu.

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

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

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

<script>
  export let disabled;
</script>

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

<style>
.button {
  padding: 5px 15px;
  height: var(--theme-toolbar-height); /* nelze použít prop přímo, musí být použita CSS proměnná */
}
.button.disabled {
  background-color: gray;
}
</style>

Svelte nepodporuje žádné proměnné v CSS. Takže není možné předat vlastnost do Svelte komponenty a použít ji v CSS. Jediný způsob, jak to udělat, je přepínání CSS třídy ON/OFF. Selektor button.disabled se používá, když je disabled nastaveno na true.

Pokud chcete použít nějaké hodnoty (např. rozměry v CSS), můžete použít CSS proměnné.

React hooks, zejména useEffect

React hooks jsou velmi silná funkce. V Svelte nenajdete nic podobného. Ale většinu React hooks vám v Svelte chybět nebude.

  • useState - stav je definován pomocí let příkazů
  • useMemo - můžete použít reaktivní příkazy místo toho, abyste přepočítávali vnitřní stav
  • useCallback - funkční výrazy jsou vyhodnoceny pouze jednou, takže to nemá smysl
  • useContext - Svelte má velmi jednoduché a přímočaré API pro kontext (getContext(), setContext())
  • useReducer - musíte vytvořit Svelte verzi, pomocí store (to může být velmi snadné)
  • useRef - můžete použít bind:this místo toho (nebo let proměnnou pro instance proměnné)
  • useEffect - to je složitější…

useEffect - jednoduché použití

Používám useEffect v Reactu velmi často, bohužel musíte přemýšlet o tom, jak implementovat stejnou logiku v Svelte.

Nejjednodušší použití je vykonání nějakého kódu při montáži a demontáži.

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

Tato varianta je pokryta s Svelte metodami onMount a onDestroy

// Svelte

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

Jak vidíte, máte dvě varianty, jak vykonat kód při demontáži - s funkcí onDestroy a s návratovou hodnotou funkce onMount.

useEffect - reaktivní příkaz

Když chcete vykonat nějaký kód při změně výrazu, můžete použít Svelte reaktivní příkaz.

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

V Svelte je reaktivní příkaz vykonán, když je přiřazena jakákoli z jeho závislostí. Takže pokud potřebujete něco udělat pouze při změně závislosti, musíte hodnotu zkontrolovat sami.

Pokud chcete explicitně vyjmenovat závislosti (jako ve druhém argumentu metody React.useEffect), můžete použít tento vzor:

// Svelte

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

Toto bude voláno po přiřazení proměnné height nebo width. Můžete použít jiné proměnné ve funkci handleChanged, ale tyto závislosti nevyvolají tento reaktivní příkaz.

useEffect - reaktivní příkaz s úklidem

React useEffect může být také použit k alokaci některých zdrojů, které závisí na hodnotě vlastnosti. V následujícím příkladu komponenta zobrazuje seznam tabulek v SQL databázi. Poslouchá změny struktury databáze. Když se změní vlastnost connectionId, je nutné se odpojit od starého socketu a připojit 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 scénář tak přímočaře jako React. Ale můžete použít jednoduchý trik k dosažení tohoto chování.

// Svelte

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

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

$: $effect;

Jak tento trik funguje? Musíte vědět o store, zejména o vlastních store (tutoriál). Funkce useEffects vytváří vlastní store s metodou subscribe. Metoda subscribe store musí vrátit metodu unsubscribe, která je volána, když už není potřeba předplatné store.

Poslední řádek ( $: $effect ) spravuje automatické předplatné store (tutoriál). Takže správně volá socket.on a poté socket.off, když se změní hodnota connectionId (nebo je pouze přiřazena v Svelte), podobně jako to dělá protějšek v Reactu.

Proč mi chybí React props v Svelte

Samozřejmě, i v Svelte existují props. Fungují podobně jako v Reactu. Ale v Reactu jsou jediným externím rozhraním komponenty props. V Svelte máte několik mechanismů, jak spravovat chování vašich komponent:

  • props - fungují jako v Reactu
  • events - používají speciální syntaxi, události nejsou součástí $$props objektu obsahujícího všechny props
  • actions (syntaxe use:action) - mechanismus, jak znovu použít logiku vázanou na HTML elementy
  • slots - účel je stejný jako React vlastnost children, s některými rozšířeními

Všechny tyto mechanismy jsou velmi užitečné, ale nemají sjednocený přístup jako props v Reactu. Níže jsou diskutovány některé problémy, na které jsem narazil.

Přeposílání událostí

Pokud vytváříte složitější hierarchie komponent, některé komponenty pouze přeposílají data z rodičovských komponent.

// React - přeposílá všechny události, jsou součástí props

function Outer(props) {
  return <Inner {...props} />;
}
 <!-- Svelte - pouze explicitně pojmenované události click a keydown jsou přeposílány -->

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

V Svelte nemůžete přeposílat všechny události, ale můžete přeposílat vyjmenované události. Ale pokud potřebujete přeposílat všechny události, můžete použít callback funkce (onClick místo on:click), pak onClick bude součástí $$props.

Implementace TabControl

V Reactu je docela snadné implementovat komponentu TabControl, která bude použita následujícím způsobem:

// React

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

Implementace projde polem children a může snadno extrahovat požadované informace (název stránky a obsah). Tento přístup nefunguje v Svelte, nemá nic jako children. Můžete použít svelte fragmenty, použití bude vypadat následovně:

 <!-- 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 jsou definovány v poli, rozložení karet (children) je definováno ve fragmentech, které jsou předány jako sloty do komponenty TabControl. Není to tak intuitivní jako v Reactu, ale funguje to. Jen jeden docela velký úskalí, které můžete vidět v implementaci 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 slotů musí být statické řetězce, takže musíte udělat něco takového, aby to fungovalo.

Použití dynamické komponenty

Jiný přístup by mohl být definovat komponentu pro každou kartu, ale pak budete mít spoustu malých souborů, protože každá Svelte komponenta musí být definována ve svém vlastním souboru.

 <!-- Svelte -->

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

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

Implementace použije svelte:component k instanciaci správné karty.

Použití kontextu

Toto je nejméně zřejmý způsob. Ale ve skutečnosti můžete dosáhnout stejné syntaxe jako v Reactu.

 <!-- Svelte -->

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

V komponentě TabControl musí být definován nějaký “sběrný bod”, např. pole dětských karet

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

A v kartě stránky pouze zaregistrujeme kartu v rodičovském poli:

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

Jediné úskalí této metody je, že nezachovává definované pořadí karet, pokud jsou některé z nich vykresleny podmíněně.


<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 vykresleno s první Page 2 a poté Page 1, což pravděpodobně není požadovaný výsledek.

Pozor při použití bind:clientHeight a bind:clientWidth

Vázání rozměrů (tutoriál) je skvělá funkce. Ale buďte opatrní při jeho použití. Je implementováno s skrytým iframe (můžete to vidět v tomto REPL). Z mých zkušeností, někdy v složitějších situacích to nefungovalo ve FireFoxu. Někdy je bezpečnější použít ResizeObserver (viz tato implementace akce)

Error boundary

React má skvělý koncept error boundaries (“try-catch” pro komponenty). Není podporován ve funkčních komponentách (ve skutečnosti, ErrorBoundary byla jediná class komponenta v mé React aplikaci), ale nebyl problém použít tuto class komponentu ve funkční React aplikaci.

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

Bez error boundary, když máte tento kód v React aplikaci, celá aplikace selže. S error boundary selže pouze vnitřek boundary, ostatní části aplikace se vykreslí jako obvykle.

Bohužel, Svelte nemá nic takového. Má NPM balíček nazvaný svelte-error-boundary, ale ve skutečnosti řeší pouze malou část problémů a většina chyb v Svelte aplikaci stále způsobuje pád aplikace.

Svelte má svůj vlastní způsob pádu - Svelte komponenty přestanou být reaktivní, takže aplikace vypadá jako zamrzlá.

Jediné, co můžete udělat, je detekovat tuto situaci a nechat uživatele znovu načíst stránku.

<!-- Svelte -->

<script>
  let counter = 0;
  $: counterCopy = counter;
  const onunhandledrejection = async e => {
    console.log('Nezpracovaná chyba, kontrola, zda došlo k pádu', e);
    const oldCounter = counter;
    counter++;
    window.setTimeout(() => {
      if (counterCopy <= oldCounter) {
        console.log('DETEKOVÁN PÁD!!!');
        if (window.confirm('Omlouváme se, aplikace selhala.\nNačíst stránku znovu?')) {
          window.location.reload();
        }
      }
    }, 500);
  };
</script>

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

Pro detekci je použit událost unhandledrejection. Tato událost může být vyvolána v různých situacích, některé z nich nezpůsobují pád Svelte. To je důvod použití proměnných counter a counterCopy. Pokud reaktivní příkaz nefunguje, znamená to, že celé Svelte je zhroucené a jediný způsob, jak se zotavit, je znovu načíst stránku.

Můžete vidět plnou ErrorHandler komponentu.

Závěr

Navzdory těmto problémům je Svelte skvělý framework a jsem velmi spokojen s výsledkem převodu mé aplikace z Reactu do Svelte. Některé funkce, které jsou nyní implementovány se Svelte, by bylo téměř nemožné udělat s Reactem.

Samozřejmě, pokud používáte mnoho knihoven třetích stran, které jsou dostupné pouze pro React, může to být vážný problém. Ale to nebyl můj případ, měl jsem minimální závislosti s Reactem, v tomto případě může být přepis do Svelte docela snadný a rychlý.