La reattività è un tema sempre più popolare negli ultimi mesi, con molti framework e librerie che la incorporano nel loro nucleo o ne sono fortemente influenzati. Vue.js ha la reattività granulare al suo centro, mentre l’idiomatico Angular ha adottato RxJS e MobX è diventato popolare tra gli sviluppatori di React come alternativa al comune pattern Redux. La reattività è stata una delle principali ispirazioni alla base della filosofia originale di React.
Tuttavia, la maggior parte delle librerie utilizza ancora il VDOM o una sorta di processo di batching in background per replicare il comportamento reattivo, anche quando la reattività è di prima classe.
Solid.js è una libreria reattiva che dà la priorità a una reattività profonda e granulare ed è progettata per offrire prestazioni e reattività eccellenti senza affidarsi al VDOM o ai processi di batching. Pur offrendo un’esperienza di sviluppo simile a React, Solid.js richiede un approccio diverso al ragionamento sui componenti. Diamo un’occhiata da vicino.
SolidJS in tre parole: reattivo, versatile e potente
A un primo esame, il codice di Solid.js può sembrare simile a quello di React, con una sintassi unica. Tuttavia, l’API fornita da Solid.js è più versatile e potente, poiché non è limitata dal VDOM e si basa invece su una reattività profonda e granulare. In Solid.js, i componenti sono usati principalmente per organizzare e raggruppare il codice e non esiste il concetto di rendering. Possiamo esplorare un semplice esempio:
Il nostro componente React si renderizzerà all’infinito. Eseguiamo la nostra interfaccia, per poi renderla immediatamente obsoleta richiamando setValue
. Ogni mutazione di stato locale in React innesca un re-rendering, poiché i componenti producono i nodi del VDOM. Il processo di ri-renderizzazione dell’interfaccia è complesso e richiede molte risorse, anche con l’uso del VDOM e delle ottimizzazioni interne di React. Sebbene React e Vue.js abbiano implementato tecniche per evitare il lavoro non necessario, ci sono ancora molte operazioni complesse che avvengono in background.
Solid.js aggiorna il valore e basta; una volta che il componente è montato, non è necessario eseguirlo di nuovo. A differenza di React, Solid.js non richiama il componente funzionale. Solid.js non si preoccupa nemmeno che createSignal
sia nello stesso ambito del componente:
In Solid.js, i componenti sono definiti “evanescenti” perché vengono usati solo per organizzare l’interfaccia in blocchi riutilizzabili e non servono ad altri scopi oltre al montaggio e allo smontaggio.
Solid.js offre una maggiore flessibilità rispetto a React per quanto riguarda la gestione dello stato dei componenti. A differenza di React, Solid.js non richiede l’adesione alle “rules of hooks” e consente invece agli sviluppatori di ragionare sugli ambiti dei moduli e delle funzioni per determinare quali componenti accedono a quali stati. Questa granularità fine significa che solo l’elemento che visualizza il valore del segnale deve essere aggiornato; tutte le operazioni necessarie per mantenere un VDOM non sono necessarie.
Solid.js utilizza i proxy per nascondere le sottoscrizioni all’interno della funzione che visualizza il valore. Questo permette agli elementi che consumano i segnali di diventare i contesti che vengono richiamati attivamente. A differenza di React, le funzioni di Solid.js sono simili a costruttori che restituiscono un metodo di rendering (come lo scheletro JSX), mentre le funzioni di React sono più simili al metodo di rendering stesso.
Gestire i props
In Solid JS, i getter sono più di un semplice valore, quindi per mantenere la reattività, i props devono essere gestiti in modo speciale. L’uso della funzione deriveProps mantiene la reattività, mentre la diffusione dell’oggetto parametro la interrompe. Questo processo è più complesso dell’uso degli operatori spread e rest di React.
Si noti che nel caso seguente non stiamo usando le parentesi per richiamare un getter:
Possiamo anche accedere direttamente al valore.
Anche se il processo può sembrare familiare, il meccanismo sottostante è completamente diverso. React esegue nuovamente il rendering dei componenti figli quando i props cambiano, il che può causare molto lavoro in background per riconciliare il nuovo DOM virtuale con il vecchio. Vue.js evita questo problema facendo semplici confronti tra i props, in modo simile al wrapping di un componente funzionale all’interno del metodo memo
di React. Solid.js si propaga lungo la gerarchia dei segnali e solo gli elementi che consumano il segnale vengono eseguiti di nuovo.
Effetti collaterali
I side effects sono un concetto comune nella programmazione funzionale e si verificano quando una funzione si affida o modifica qualcosa al di fuori dei suoi parametri. Esempi di side effects sono la sottoscrizione di eventi, la chiamata di API e l’esecuzione di calcoli costosi che coinvolgono lo stato esterno. In Solid.js, gli effetti sono simili agli elementi e sottoscrivono valori reattivi. L’uso di un getter semplifica la sintassi rispetto a React.
In React, l’hook useEffect
viene utilizzato per gestire i side effects. Quando si usa useEffect, una funzione che esegue il lavoro viene passata come argomento, insieme a un array opzionale di dipendenze che potrebbero cambiare. React esegue un confronto superficiale dei valori nell’array ed esegue nuovamente l’effetto se uno di essi cambia.
Quando si usa React, può essere frustrante serializzare tutti i valori come oggetti o stati per evitare problemi con il confronto superficiale eseguito da React. Indicare un oggetto non è una buona soluzione, perché potrebbe fare riferimento a una istanza anonima che è diversa a ogni rendering, causando una nuova esecuzione dell’effetto. Le soluzioni a questo problema prevedono la dichiarazione di più oggetti o una maggiore verbosità, che aggiunge complessità.
In Solid.js, gli effetti vengono eseguiti su qualsiasi mutazione del segnale. Il riferimento al segnale è anche la dipendenza.
Proprio come React, l’effetto verrà eseguito di nuovo quando i valori cambiano, senza dichiarare un array di dipendenze o alcun confronto in background. Ciò consente di risparmiare tempo e lavoro, evitando i bug legati alle dipendenze. Tuttavia, è sempre possibile creare un ciclo infinito mutando il segnale a cui l’effetto è in ascolto, quindi è da evitare.
createEffect
deve essere considerato come l’equivalente Solid.js della sottoscrizione agli osservabili in RxJS, in cui ascoltiamo tutti gli osservabili “consumati” – i nostri segnali – allo stesso tempo. Gli utenti di React potrebbero avere familiarità con il fatto che useEffect sostituisce componentDidMount
, componentWillUnmount
, and componentDidUpdate
. Solid.js fornisce hook dedicati per la gestione dei componenti: onMount
e onCleanup
. Questi hook vengono eseguiti rispettivamente ogni volta che il componente ritorna per primo o viene tolto dal DOM. Il loro scopo in Solid.js è più esplicito rispetto all’uso di useEffect
in React.
Gestire porzioni di stato dell’applicativo
Nelle applicazioni complesse, l’uso degli hook useState
e useEffect
potrebbe non essere sufficiente. Passare molte variabili tra i componenti, richiamare metodi in profondità e mantenere i vari elementi sincronizzati tra loro può essere impegnativo. Il carrello della spesa, il selettore di lingua, il login dell’utente e i temi sono solo alcuni esempi delle molte applicazioni che richiedono una sorta di fetta di stato.
In React sono disponibili varie tecniche per gestire applicazioni complesse. Un approccio è quello di utilizzare un contesto per consentire ai componenti discendenti di accedere a uno stato condiviso. Tuttavia, per evitare rendering inutili, è importante selezionare solo una parte dello stato e memoizzarlo. React fornisce metodi nativi come useReducer
e funzioni di memoizzazione come useMemo
o il wrapping dei componenti in React.memo
per ottimizzare il rendering.
In alternativa, molti sviluppatori scelgono di definire il proprio store Redux e ciascuna delle slice. Con l’evoluzione di Redux è diventato molto più facile da usare rispetto agli inizi. Gli sviluppatori hanno ora la possibilità di utilizzare hook e costruttori di funzioni, che gestiscono i problemi in modo dichiarativo. Questo elimina la necessità di definire costanti, creatori di azioni e altri elementi correlati in file separati per ogni slice.
Solid.js fornisce il supporto per diverse librerie di gestione degli stati e offre diversi metodi per implementare diversi modelli. Un metodo utile è la possibilità di avvolgere le richieste utilizzando le risorse.
A differenza degli hook di stato di React, che si agganciano al DOM virtuale, i segnali di Solid.js sono unità indipendenti che consentono agli sviluppatori di scrivere JavaScript idiomatico. Ciò consente di inserire i segnali in altri moduli e di limitare l’accesso attraverso i metodi, trasformando di fatto i segnali in singleton privati.
Pertanto, i moduli possono agire come porzioni di stato, esportando metodi pubblici per interagire con i dati senza l’uso di alcuna libreria esterna. Dichiarando i segnali nell’ambito di un modulo, si possono esporre interfacce pubblicamente disponibili allo stato condiviso in tutti i componenti. Se invece i segnali venissero dichiarati nei componenti, sarebbero assegnati al contesto della funzione, in modo simile al comportamento di useState
in React.
Inoltre, in Solid.js, le chiamate API possono essere gestite facilmente con il metodo createResource
. Questo metodo consente agli sviluppatori di recuperare dati da un’API e di controllare lo stato della richiesta in modo standardizzato. Questa funzione è simile al metodo createSignal
di Solid.js, che crea un segnale che tiene traccia di un singolo valore e può cambiare nel tempo, e alla popolare libreria useQuery
di React.
Sebbene possa funzionare gestire i segnali come getter diversi, a un certo punto sarà necessario gestire oggetti complessi e profondi, mutando i valori a diversi livelli, accedendo a fette granulari e in generale operando su oggetti e array. Il modulo solid-js/store
fornisce un insieme di utilità per la creazione di un store, che è un albero di segnali a cui accedere e che può essere mutato individualmente in modo completamente reattivo. Si tratta di un’alternativa agli store di altre librerie, come Redux o Pinia in Vue.js.
Per impostare i dati in un negozio Solid.js, possiamo usare il metodo set
, che è simile ai segnali. Il metodo set
ha due modalità: possiamo passare un oggetto che verrà unito allo stato esistente, oppure passare una serie di argomenti che esploreranno il nostro store fino alla proprietà o all’oggetto che verrà mutato.
Per esempio, supponiamo di avere il negozio mostrato sotto:
Possiamo impostare l’età dell’utente a 35 anni, passando un oggetto con le proprietà che vogliamo aggiornare, insieme a un percorso che specifica in quale punto dell’albero degli stati applicare l’aggiornamento:
Questo aggiornerà la proprietà age
dell’oggetto user
nell’albero degli stati. Inoltre, possiamo aggiornare l’oggetto store passando a un oggetto che verrà unito a quello attuale.
Se si omettesse l’attributo user
come primo parametro, si sostituirebbe interamente l’oggetto user:
Poiché il negozio è un albero di segnali, che è a sua volta un proxy, possiamo accedere direttamente ai valori usando la sintassi dei punti. La modifica di un singolo valore causerà il rendering dell’elemento, proprio come la sottoscrizione di un valore di segnale.
Altri strumenti utili
Abbiamo due metodi utili per aggiornare il nostro stato. Se siamo abituati a mutare un negozio Redux usando la libreria immer
, possiamo mutare i valori in loco usando una sintassi simile con il metodo produce
:
The produce
method returns a draft version of the original object, which is a new object, and any changes made to the draft object are tracked similarly to using immer
. We can also pass a reconcile
function call to setState
. This is particularly useful when we want to match elements in the array based on a unique identifier, rather than simply overriding the entire array. For instance, we can update a specific object based on its id
property by passing a reconcile
function that matches the object with the same id
:
Il metodo produce
restituisce una versione in bozza dell’oggetto originale, che è un nuovo oggetto, e tutte le modifiche apportate all’oggetto in bozza sono tracciate in modo simile all’uso di immer
. Si può anche passare una chiamata di funzione reconcile
a setState
. Questo è particolarmente utile quando si vuole far corrispondere gli elementi dell’array in base a un identificatore unico, invece di sovrascrivere semplicemente l’intero array. Ad esempio, possiamo aggiornare un oggetto specifico in base alla sua proprietà id
, passando una funzione reconcile
che corrisponde all’oggetto con lo stesso id
:
Questo aggiornerà l’oggetto nell’array con lo stesso id
, oppure lo aggiungerà alla fine dell’array se non viene trovato alcun oggetto corrispondente.
È possibile raggruppare più aggiornamenti dello stato in un’unica transazione, utilizzando il metodo di utilità transaction
. Questo può essere utile quando è necessario effettuare più aggiornamenti allo stato in modo atomico, come nel caso dell’aggiornamento di più proprietà di un oggetto:
Questo aggiornerà le proprietà name
ed age
dell’oggetto user
in un’unica transazione, assicurando che gli eventuali sottoscrittori dello stato ricevano una sola notifica della modifica, anziché una per ogni aggiornamento.
Interoperabilità con RxJS
Possiamo lavorare facilmente sia con SolidJs che con RxJS, un’altra libreria reattiva molto diffusa, utilizzando un paio di funzioni adattatore. Il metodo reduce di cui abbiamo appena parlato è mostrato come esempio di sottoscrizione, simile a come vengono gestiti i servizi in Angular.
Da RxJS a Solid.js
Possiamo trasformare qualsiasi produttore che esponga un metodo di sottoscrizione in un segnale:
Questo gestisce direttamente la sottoscrizione e la pulizia quando il segnale viene abbandonato. Possiamo definire il nostro segnale passando una funzione per tracciare il valore e come ripulirlo. Il metodo set
emette il valore ai contesti in ascolto.
Trasformare i segnali in osservabili
Possiamo trasformare il nostro segnale in un osservabile che espone un metodo di sottoscrizione, consentendogli di agire come un osservabile nativo di RxJS.
Quindi, utilizzando il metodo from fornito da RxJS, possiamo trasformare il nostro segnale in un osservabile RxJS a tutti gli effetti.
Una scelta Solid.js
Sebbene sia relativamente nuovo, Solid.js ha guadagnato popolarità tra gli sviluppatori grazie alle sue caratteristiche uniche e alle sue prestazioni eccezionali. Rispetto a React, Solid.js offre strumenti utili e performanti come framework come Svelte, senza bisogno di compilatori. È particolarmente adatto alle interfacce che richiedono molti aggiornamenti del DOM ed è costantemente veloce anche nelle applicazioni complesse che gestiscono aggiornamenti in tempo reale.
Solid.js offre un’esperienza di sviluppo simile a quella di React, ma con metodi più puliti e più scelte. La libreria gestisce molti pattern diversi e offre una maggiore trasparenza nel codice grazie al funzionamento nativo degli scope in JavaScript. A differenza dell’uso degli hook in React, non ci sono comportamenti nascosti quando si creano segnali in Solid.js.
L’uso di Solid.js con TypeScript risolve molti dei problemi che gli sviluppatori devono affrontare con le applicazioni complesse realizzate con React o Vue.js, riducendo il time to market e il tempo dedicato al debug dei problemi con il VDOM. Lo raccomandiamo per qualsiasi nuovo progetto a partire da oggi.
Autore: Federico Muzzo, Senior Front End Developer @ Bitrock