Réécrire une application de React à Svelte, à quoi s'attendre
Published: 2021-04-08
Récemment, j’ai réécrit une application de taille moyenne en React (~250 composants React) en Svelte. Le résultat de cette action est une application plus efficace, avec une maintenance plus facile et un code beaucoup plus lisible. Mais il y a eu quelques problèmes, certaines situations qui ne sont pas simples à transformer en code Svelte. Cet article décrit certaines de ces situations.
Ce n’est pas un tutoriel de Svelte pour les programmeurs React, si vous ne connaissez pas encore Svelte, le meilleur point de départ est le tutoriel officiel de Svelte. Cet article contient de nombreux liens vers des tutoriels Svelte, au lieu d’expliquer les principes de Svelte.
CSS
Svelte prend en charge le CSS de manière très agréable et propre. Dans l’application React, j’ai utilisé styled components. Il existe de nombreuses bibliothèques CSS pour React, mais toutes (à ma connaissance) utilisent le paradigme CSS-in-JS, alors que dans Svelte, vous utilisez un CSS assez propre.
Dans l’extrait suivant, je vais montrer une implémentation simplifiée d’un bouton de barre d’outils, en React (avec styled components) et avec Svelte.
Le bouton de la barre d’outils a une propriété (disabled), qui désactive la fonctionnalité de survol. Il utilise également certaines variables du thème.
// Composant React ToolbarButton({disabled, toolBarHeight})
const ToolbarButton = styled.div`
padding: 5px 15px;
height: ${props.toolBarHeight}px;
${(props) =>
!props.disabled &&
`
&:hover {
background-color: gray;
}
`}
`;
<!-- Composant Svelte -->
<script>
export let disabled;
</script>
<div class="button" class:disabled />
<style>
.button {
padding: 5px 15px;
height: var(--theme-toolbar-height); /* vous ne pouvez pas utiliser directement la prop, une variable CSS doit être utilisée */
}
.button.disabled {
background-color: gray;
}
</style>
Svelte ne prend pas en charge les variables dans le CSS. Il n’est donc pas possible de passer une propriété à un composant Svelte et de l’utiliser dans le CSS. La seule façon de le faire est d’activer/désactiver une classe CSS. Le sélecteur button.disabled est utilisé lorsque disabled est défini sur true.
Lorsque vous souhaitez utiliser certaines valeurs (par exemple, des dimensions en CSS), vous pouvez utiliser des variables CSS.
Hooks React, en particulier useEffect
Les hooks React sont une fonctionnalité très puissante. Dans Svelte, vous ne trouverez rien de similaire. Mais la plupart des hooks React ne vous manqueront pas dans Svelte.
- useState - l’état est défini avec des commandes let
- useMemo - vous pouvez utiliser des commandes réactives à la place pour recalculer l’état interne
- useCallback - les expressions de fonction sont évaluées une seule fois, donc cela n’a pas de sens
- useContext - Svelte a une API de contexte très simple et directe (getContext(), setContext())
- useReducer - vous devez créer une version Svelte, en utilisant des stores (cela peut être très facile)
- useRef - vous pouvez utiliser bind:this à la place (ou une variable let pour les variables d’instance)
- useEffect - c’est plus compliqué…
useEffect - utilisation simple
J’utilise souvent useEffect dans React, malheureusement vous devez réfléchir à la façon d’implémenter la même logique dans Svelte.
L’utilisation la plus simple est d’exécuter du code au montage et au démontage.
// React
React.useEffect(() => {
console.log('MONTAGE');
return () => console.log('DÉMONTAGE');
}, []);
Cette variante est convertie avec les méthodes onMount et onDestroy de Svelte
// Svelte
onMount(() => {
console.log('MONTAGE');
return () => console.log('DÉMONTAGE 1');
});
onDestroy(() => {
console.log('DÉMONTAGE 2');
});
Comme vous pouvez le voir, vous avez deux variantes pour exécuter du code au démontage - avec la fonction onDestroy et avec la valeur de retour de la fonction onMount.
useEffect - déclaration réactive
Lorsque vous souhaitez exécuter du code lors d’un changement d’expression, vous pouvez utiliser la déclaration réactive de Svelte.
// React
React.useEffect(() => {
console.log('HAUTEUR modifiée, nouvelle valeur :', height);
}, [height]);
// Svelte
$: console.log('HAUTEUR modifiée, nouvelle valeur :', height);
Dans Svelte, la déclaration réactive est exécutée lorsque l’une des dépendances de la déclaration est assignée. Donc, si vous devez faire quelque chose uniquement lorsque la dépendance change, vous devez vérifier la valeur par vous-même.
Si vous souhaitez énumérer explicitement les dépendances (comme dans le deuxième argument de la méthode React.useEffect), vous pouvez utiliser ce modèle :
// Svelte
$: {
height;
width;
handleChanged();
}
Cela sera appelé après l’assignation de la variable height ou width. Vous pouvez utiliser d’autres variables dans la fonction handleChanged, mais ces dépendances ne déclencheront pas cette déclaration réactive.
useEffect - déclaration réactive avec nettoyage
React useEffect peut également être utilisé pour allouer certaines ressources, qui dépendent de la valeur de la propriété. Dans l’exemple suivant, le composant affiche une liste de tables dans une base de données SQL. Il écoute les changements de structure de la base de données. Lorsque la propriété connectionId est modifiée, il est nécessaire de se déconnecter de l’ancien socket et de se connecter au nouveau socket.
// React
function SqlTableList({ connectionId }) {
React.useEffect(() => {
socket.on(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
return () => {
socket.off(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
};
}, [connectionId]);
}
Svelte ne prend pas en charge ce scénario de manière aussi directe que React. Mais vous pouvez utiliser un simple truc pour accomplir ce comportement.
// Svelte
const useEffect = subscribe => ({ subscribe });
$: effect = useEffect(() => {
socket.on(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
return () => {
socket.off(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
};
});
$: $effect;
Comment fonctionne ce truc ? Vous devez connaître les stores, en particulier les stores personnalisés (tutoriel). La fonction useEffects crée un store personnalisé avec une méthode d’abonnement. La méthode subscribe du store doit retourner une méthode de désabonnement, qui est appelée lorsque l’abonnement au store n’est plus nécessaire.
La dernière ligne ( $: $effect ) gère l’abonnement automatique au store (tutoriel). Ainsi, elle appelle correctement socket.on puis socket.off, lorsque la valeur de connectionId est modifiée (ou simplement assignée dans Svelte), de manière similaire à ce que fait l’équivalent React.
Pourquoi les props React me manquent dans Svelte
Bien sûr, il y a aussi des props dans Svelte. Elles fonctionnent de manière similaire à React. Mais dans React, la seule interface externe du composant est constituée des props. Dans Svelte, vous avez plusieurs mécanismes pour gérer le comportement de vos composants :
- props - fonctionnent comme dans React
- événements - utilisent une syntaxe spéciale, les événements ne font pas partie de l’objet $$props contenant toutes les props
- actions (syntaxe use:action) - mécanisme pour réutiliser la logique liée aux éléments HTML
- slots - le but est le même que la propriété children de React, avec quelques extensions
Tous ces mécanismes sont très utiles, mais ils n’ont pas d’accès unifié comme les props dans React. Ci-dessous sont discutés certains problèmes que j’ai rencontrés.
Transférer des événements
Si vous créez des hiérarchies de composants plus complexes, certains composants ne font que transférer des données des composants parents.
// React - transfère tous les événements, ils font partie des props
function Outer(props) {
return <Inner {...props} />;
}
<!-- Svelte - seuls les événements explicitement nommés click et keydown sont transférés -->
<Inner {...$$props} on:click on:keydown />;
}
Dans Svelte, vous ne pouvez pas transférer tous les événements, mais vous pouvez transférer des événements énumérés. Mais si vous devez transférer tous les événements, vous pouvez utiliser des fonctions de rappel (onClick au lieu de on:click), alors onClick fera partie de $$props.
Implémenter TabControl
Dans React, il est assez facile d’implémenter un composant TabControl, qui sera utilisé comme suit :
// React
<TabControl>
<TabPage label='Page 1'>
Contenu de la page 1
</TabPage>
<TabPage label='Page 2'>
Contenu de la page 2
</TabPage>
</TabControl>
L’implémentation énumérera le tableau des enfants et pourra facilement extraire les informations souhaitées (titre et contenu de la page). Cette approche ne fonctionne pas dans Svelte, il n’a rien de tel que children. Vous pouvez utiliser des fragments Svelte, l’utilisation ressemblera à ceci :
<!-- Svelte -->
<TabControl tabs={[
{ label: 'Page 1', slot: 1},
{ label: 'Page 2', slot: 2},
]}>
<svelte:fragment slot='1'>
Contenu de la page 1
</svelte:fragment>
<svelte:fragment slot='2'>
Contenu de la page 2
</svelte:fragment>
</TabControl>
Les onglets sont définis dans un tableau, la mise en page des onglets (enfants) est définie dans des fragments, qui sont passés comme slots au composant TabControl. Ce n’est pas aussi intuitif que dans React, mais cela fonctionne. Un seul assez gros écueil, que vous pouvez voir dans l’implémentation de 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>
Les noms de slots doivent être des chaînes statiques, donc vous devez faire quelque chose comme ça pour que cela fonctionne.
Utiliser un composant dynamique
Une autre approche pourrait être de définir un composant par page d’onglet, mais alors vous aurez beaucoup de petits fichiers, car chaque composant Svelte doit être défini dans son propre fichier.
<!-- Svelte -->
<TabControl tabs={[
{ label: 'Page 1', component: Tab1},
{ label: 'Page 2', component: Tab2},
]}/>
// Tab1.svelte
Contenu de la page 1
// Tab2.svelte
Contenu de la page 2
L’implémentation utilisera svelte:component pour instancier l’onglet approprié.
Utiliser le contexte
C’est la méthode la moins évidente. Mais en fait, vous pouvez obtenir la même syntaxe que dans React.
<!-- Svelte -->
<TabControl>
<TabPage label='Page 1'>
Contenu de la page 1
</TabPage>
<TabPage label='Page 2'>
Contenu de la page 2
</TabPage>
</TabControl>
Dans le composant TabControl, un “point de collecte” doit être défini, par exemple un tableau d’onglets enfants
<!-- TabControl.svelte -->
<script>
const tabs = [];
setContext('tabs', tabs);
</script>
Et dans la page d’onglet, nous enregistrons simplement l’onglet dans le tableau parent :
<!-- TabPage.svelte -->
<script>
export let label;
const tabs = getContext('tabs');
tabs.push({ label });
</script>
Le seul écueil de cette méthode est qu’elle ne préserve pas l’ordre défini des pages d’onglets, si certaines d’entre elles sont rendues conditionnellement.
<TabControl>
{#if condition_will_be_true_later}
<TabPage label='Page 1'>
Contenu de la page 1
</TabPage>
{/if}
<TabPage label='Page 2'>
Contenu de la page 2
</TabPage>
</TabControl>
Cela sera rendu avec d’abord la Page 2 puis la Page 1, ce qui n’est probablement pas le résultat souhaité.
Attention lors de l’utilisation de bind:clientHeight et bind:clientWidth
Lier les dimensions (tutoriel) est une fonctionnalité géniale. Mais soyez prudent lorsque vous l’utilisez. Elle est implémentée avec un iframe caché (vous pouvez le voir dans ce REPL). D’après mon expérience, parfois dans des situations plus compliquées, cela ne fonctionnait pas dans FireFox. Parfois, il est plus sûr d’utiliser ResizeObserver (voir cette implémentation d’action)
Limite d’erreur
React a un excellent concept de limites d’erreur (“try-catch” pour les composants). Il n’est pas pris en charge dans les composants fonctionnels (en fait, ErrorBoundary était le seul composant de classe dans mon application React), mais il n’y avait aucun problème à utiliser ce composant de classe dans une application React fonctionnelle.
// React
<ErrorBoundary>
{(null).read()}
</ErrorBoundary>
Sans limite d’erreur, lorsque vous avez ce code dans une application React, l’application entière échouera. Avec une limite d’erreur, seule l’intérieur de la limite échouera, les autres parties de l’application se rendront comme d’habitude.
Malheureusement, Svelte n’a rien de tel. Il a un package NPM nommé svelte-error-boundary, mais en fait, il ne résout qu’une petite partie des problèmes et la majorité des erreurs dans l’application Svelte causent toujours un plantage de l’application.
Svelte a sa propre façon de planter - les composants Svelte cesseront d’être réactifs, donc l’application semble gelée.
La seule chose que vous pouvez faire est de détecter cette situation et de laisser l’utilisateur recharger la page.
<!-- Svelte -->
<script>
let counter = 0;
$: counterCopy = counter;
const onunhandledrejection = async e => {
console.log('Erreur non gérée, vérification si plantage', e);
const oldCounter = counter;
counter++;
window.setTimeout(() => {
if (counterCopy <= oldCounter) {
console.log('PLANTAGE DÉTECTÉ !!!');
if (window.confirm('Désolé, l'application a planté.\nRecharger la page ?')) {
window.location.reload();
}
}
}, 500);
};
</script>
<svelte:window on:unhandledrejection={onunhandledrejection} />
Pour la détection, l’événement unhandledrejection est utilisé. Cet événement peut être déclenché dans plusieurs situations, certaines d’entre elles ne causent pas de plantage de Svelte. C’est la raison de l’utilisation des variables counter et counterCopy. Si la déclaration réactive ne fonctionne pas, cela signifie que tout Svelte est planté et le seul moyen de récupérer est de recharger la page.
Vous pouvez voir le composant complet ErrorHandler.
Conclusion
Malgré ces problèmes, Svelte est un excellent framework et je suis très satisfait du résultat de la conversion de mon application de React à Svelte. Certaines fonctionnalités, qui sont maintenant implémentées avec Svelte, seraient presque impossibles à réaliser avec React.
Bien sûr, si vous utilisez beaucoup de bibliothèques tierces, qui ne sont disponibles que pour React, cela pourrait être un problème sérieux. Mais ce n’était pas mon cas, j’avais des dépendances minimales avec React, dans ce cas, la réécriture en Svelte pourrait également être assez facile et rapide.