Introduzione

Nell’isterico mondo dello sviluppo web, dove una tecnologia è superata dopo appena qualche mese di “hype”, React rappresenta un’eccezione, essendo sulla cresta dell’onda da almeno un paio di anni.

Molto spesso quando si parla di React si danno per scontato una serie di altre tecnologie e tool di corredo che diventano indispensabili nel caso in cui si voglia utilizzare React per realizzare Single Page Application.

In questo contesto la curva di apprendimento è tutt’altro che piatta, e lo sviluppo di una nuova funzionalità consiste tipicamente nell’assemblare insieme più parti, relative a librerie differenti, solo per iniziare a “vedere qualcosa”.

È questo il contesto dove l’utilizzo di un tool come storybook può cambiare l’esperienza dello sviluppatore React.

Cos’è Storybook

All’interno di un’applicazione web è abbastanza comune avere molti componenti visuali, ciascuno dei quali con uno o più stati. Lo stato di un componente dipende poi spesso da uno più globale (dell’applicazione o di un componente più in alto nelle gerarchie).

Si prenda ad esempio un form con un pulsante di submit, il quale supponiamo essere abilitato solo nel caso in cui i campi di un form siano tutti valorizzati e validati. Ogni volta quindi che dovremo modificare o testare il pulsante di submit sarà necessario valorizzare nuovamente il form con dati sensati, con ovvio spreco di tempo.

Inoltre è più facile di quanto si pensi definire lo stile CSS del componente con regole ereditate dal componente padre, in questo caso il form. Ed è quindi altrettanto facile che il nostro pulsante di submit non sia direttamente utilizzabile in un altro componente, che potremmo ipotizzare essere una barra di ricerca. A questo punto siamo di fronte ad un problema con due possibili soluzioni:

Una soluzione “manuale” a questo problema utilizzata spesso da programmatori frontend più esperti era quella di realizzare una pagina showcase dei componenti (a volte coincidente con lo style guide dell’applicazione) per svilupparli e testarli in isolamento.

Storybook riprende e formalizza questo concetto permettendo di fare il rendering dei componenti che compongono la nostra applicazione in modo completamente separato dalla stessa.

React Storybook

Installazione e configurazione

Nato come progetto open-source di una startup chiamata kadira, Storybook ha vissuto una recente fase di transizione verso uno sviluppo completamente community-driven dopo che la startup che ne ha iniziato lo sviluppo ha dovuto chiudere i battenti.

Tecnicamente, Storybook è un server webpack che renderizza ciascuna storia definita dallo sviluppatore in un iframe dedicato. Che lo si utilizzi per sviluppare un nuovo componente o solo successivamente per testarlo, fare il rendering di un componente al di fuori della normale gerarchia dei componenti che compongono l’applicazione (e quindi il relativo html e CSS) permette di individuare con facilità allineamenti o proprietà dipendenti da regole definite dal padre o da un qualche antenato.

Storybook è compatibile con React e React Native ed è previsto dalla roadmap ufficiale il porting agli altri principali framework per lo sviluppo di SPA come Angular, Vue, ecc…

Storybook viene distribuito su npm in più pacchetti, fra i quali un’interfaccia command line che è possibile installare globalmente per configurarlo automaticamente per il proprio progetto. Ma anche per coloro che non amano installare pacchetti npm globalmente, configurare Storybook è molto semplice.

Dopo aver installato il pacchetto principale per React con:

npm i --save-dev @storybook/react

ci ritroveremo un eseguibile chiamato start-storybook che andremo tipicamente ad aggiungere ai run scripts del package.json come:

"storybook": "start-storybook -p 9001 -c .storybook"

nel quale .storybook è la directory dove Storybook cercherà la propria configurazione e 9001 la porta su cui girerà il server webpack. E’ quindi possibile e persino consigliato far girare Storybook su un’altra porta rispetto a quella dell’applicazione vera e propria in modo che sia possibile lanciare entrambi i server allo stesso momento, evitando così perdite di tempo dovute alla fase di generazione del bundle con webpack non proprio immediata.

A questo punto non ci rimane altro che definire il file di configurazione .storybook/config.js in questo modo:

import { configure } from '@storybook/react';

function loadStories() {
  require('../src/stories/index.js');
}

configure(loadStories, module);

e lanciare Storybook con npm run storybook.

Definire le storie

Supponiamo di avere un componente Button che si trovi nella directory src/components. In una configurazione tipica di Storybook le storie potrebbero essere caricate a partire dal file src/stories/index.js (o da quello di cui è stata fatta la require nel file di configurazione) e una possibile storia del Button potrebbe essere definita come:

import React from 'react';
import { storiesOf } from '@storybook/react';
import Button from '../components/Button';

storiesOf('Button', module)
  .add('default', () => (
    <button>Hello Button</button>
  ));

Una volta dato un nome di nostro gradimento ad una storia essa potrà prevedere più casi aggiunti con il metodo add(, ).

Questo torna particolarmente utile in caso di componenti in cui aspetto o comportamento varia da uno stato all’altro: si pensi ad esempio ad un componente tabellare che mostra una lista di utenti.

Probabilmente si vorrà mostrare uno spinner nel caso in cui questi utenti non siano stati ancora caricati. Potremmo quindi definire più storie per il componente ciascuna con i propri dati (statici) in modo tale da poterci occupare dell’aspetto e del comportamento in ognuno di essi:

import React from 'react';
import { storiesOf } from '@storybook/react';
import UsersTable from '../components/UsersTable';

storiesOf('UsersTable', module)
  .add('default', () => {
    const users = [
      {name: 'Gianni', surname: 'Valdambrini'},    
      {name: 'Mario', surname: 'Rossi'},
    ];
    return ;
  })
  .add('to be loaded', () => (
    return ;
  ));

Storybook Driven Development

Come in qualsiasi * Driven Development, l’idea alla base è molto semplice. Un’applicazione React tipicamente consiste in React stesso, in un gestore dello stato (spesso Redux) e in qualcosa che permetta di fare routing client-side (spesso react-router).

Creare il componente dopo aver definito le regole di routing, modificato opportunamente il reducer e implementato almeno le azioni di load dei dati vuol dire scrivere una buona quantità di codice “al buio”, di cui ci troveremo a fare il debugging in un colpo solo.

È evidente che questa situazione non sia affatto ideale: Storybook tuttavia ci permette di definire una storia per renderizzare un componente date certe proprietà ancora prima di integrarlo con Redux e react-router (o tool similari).

Come suggerito anche dalla documentazione ufficiale l’approccio ideale allo sviluppo di un componente React è quello di realizzare prima una versione statica del componente stesso in cui siano hardcodati nel codice i dati che il componente si aspetta e solo successivamente aggiungere dinamismo. Se i dati hardcodati nel codice sono passati al componente tramite props, una volta in possesso dei dati veri il componente potrà rimanere (pressoché) invariato, e sarà sufficiente sostituire i dati statici con quelli dinamici.

Questo workflow si sposa perfettamente con Storybook: una storia conterrà tipicamente la versione statica di un componente nella quale i dati hardcodati passati al componente nella storia saranno quelli che ne definiscono l’aspetto e il comportamento. Una volta definito quello che in termini Redux è spesso chiamato “dumb” o “presentational” component sarà semplice andare ad integrarlo con il resto dell’app e con librerie come le già citate Redux, react-router, ecc…

Storybook: decoratori e addons

Benché ad un primo sguardo ciò che viene offerto da Storybook possa sembrare assolutamente sufficiente allo sviluppo in isolamento dei componenti, al crescere della dimensione dell’applicazione e quindi del numero dei componenti capita non di rado di affrontare alcuni problemi (che vedremo fra poco) o di voler semplificare il codice delle storie. Da queste esperienze sono nati i decoratori e gli addons.

Decoratori

I decoratori sono componenti React che fanno il wrap di una storia e che possono essere utilizzati sia per ragioni “estetiche” che per ragioni funzionali.

Ad esempio un decoratore potrebbe essere utilizzato per includere il contenuto di una storia in un contenitore avente una specifica dimensione ed un certo colore di background:

const CenterDecorator = (story) => (
  <div style={{
    width: 400,
    height: 300,
    backgroundColor: '#c0c0c0',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center'}}>
    {story()}
  </div>
);

Esistono poi un buon numero di librerie anche molto comuni come Redux, react-router o react-dnd che utilizzano il context di React per fornire funzionalità a carattere globale. Ad esempio react-dnd richiede che l’applicazione (o meglio, la parte che fa uso del drag&drop) sia wrappata da un elemento chiamato DragDropContextProvider.

In queste situazioni l’unico modo per utilizzare il componente al di fuori dell’applicazione è definire un decoratore che vada a costruire l’elemento React che fornisce il context ai componenti dell’applicazione. Nel caso quindi di react-dnd sarà necessario definire un decoratore così fatto:

const DragDropDecorator = (story) => {
  return (

      {story()}

  );
};

Che si definisca un proprio decoratore o che se ne utilizzi uno già definito (ad esempio host o storyrouter) il decoratore potrà essere applicato alla singola storia tramite la funzione addDecorator:

storiesOf('Button', module)
  .addDecorator(CenterDecorator)
  .add('default', () => (
    <button>Hello Button</button>
  ));

o in modo simile globalmente a tutte le storie:

import { storiesOf, addDecorator } from '@storybook/react';
addDecorator(CenterDecorator);

Gli addons sono componenti React che utilizzano Storybook come piattaforma per fornire funzionalità integrate con la finestra stessa di Storybook.

Addons

Storybook è dotato di un buon numero di addons, fra cui il più comune è l’addon actions.

actions è un addon molto semplice che può essere utilizzato per fare il logging di azioni / funzioni chiamate da un componente:

import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';

storiesOf('Button', module)
  .add('default view', () => (
    <button>
      Hello World!
    </button>
  ));

come è possibile intuire dal codice soprastante action restituisce una funzione che può essere passata dovunque sia attesa una funzione come callback. Quando la callback viene chiamata l’addon stamperà, nel pannello sottostante la preview del componente, il nome specificato alla action seguito dagli argomenti passati alla funzione.

Un altro addon piuttosto usato è links, che permette di linkare azioni fra di loro. Il funzionamento è simile a quello dell’addon actions: una volta importata la funzione linkTo sarà possibile costruire una callback che quando invocata dal componente vada a rendere attiva la storia specificata come argomento:

import { storiesOf } from '@storybook/react'
import { linkTo } from '@storybook/addon-links'

storiesOf('Button', module)
  .add('First', () => (
    <button>Go to "Second"</button>
  ))
  .add('Second', () => (
    <button>Go to "First"</button>
  ));

Grazie all’utilizzo di links è possibile quindi creare demo o prototipi a partire da una serie di storie in modo simile a quanto offerto da tool come Invision.

Storyshots

Quando si parla di testing applicato ad interfacce grafiche ci si addentra tipicamente in un terreno scivoloso, nel quale la giusta volontà nel fare testing della propria applicazione a volte si scontra con tempi di sviluppo e manutenzione dei test troppo estesi per poter poi essere affrontati nella pratica.

Anche il mondo React non è esente da problemi: una delle soluzioni più diffuse per fare unit-testing è utilizzare enzyme e fare lo shallow rendering per renderizzare il nostro componente facendo poi asserzioni su quanto renderizzato. Il problema in questo caso è che essere troppo scrupolosi su quanto renderizzato rende necessario aggiornare i test anche in caso di cambiamenti non significativi, mentre essere testare solo le cose principali lascia la porta aperta a problemi che sebbene siano non di primaria importanza sono comunque disdicevoli da avere in una app coperta da test.

Jest ha recentemente un nuovo modo di fare testing, chiamato snapshot testing e che è alla base di un addon speciale chiamato Storyshots.

Con gli snapshot test il componente viene renderizzato in modo simile a quanto fatto da enzyme, ma non è richiesto al programmatore di scrivere alcun genere di asserzione.

E allora come possono funzionare questi test?

Il rendering del componente (o meglio la sua descrizione, che è semplicemente un oggetto javascript) viene salvato in un file, e le esecuzioni successive del test andranno a confrontare quanto renderizzato con quanto salvato nello snapshot. Quando un test differisce, viene riportato un fallimento e starà poi allo sviluppatore andare a verificare se il cambiamento effettuato nel componente è stato intenzionale oppure no. Nel primo caso sarà quindi possibile andare ad aggiornare lo snapshot, nel secondo caso invece si potrà andare a correggere la regressione.

In tal senso, gli snapshot test possono essere visti come dei warning che devono attirare l’attenzione dello sviluppatore su uno o più componenti per dire “questo componente è cambiato, l’hai fatto di proposito?”.

Storyshots permette di far vedere a Jest le storie come dei test di cui fare lo snapshot testing. Non c’è bisogno quindi di fare niente di specifico durante la scrittura delle storie, tutto quello che serve è configurare Storyshots creando un nuovo file che è usanza chiamare storyshots.test.js dentro cui definire:

import initStoryshots from '@storybook/addon-storyshots';
initStoryshots();

Conclusioni

Sorprendente è come, abbracciando in pieno lo strumento e sviluppando secondo lo stile “Storybook Driven Design”, l’utilizzo di Storybook possa semplificare e quindi velocizzare lo sviluppo di applicazioni React.

Sviluppare un componente su Storybook e solo successivamente integrarlo con l’applicazione ha il grosso beneficio di azzerare praticamente il costo dello sviluppo delle storie. È infatti da sottolineare come scrivere una storia in gran parte dei casi significa passare delle proprietà ad un componente importato, componente che sarà poi incluso nell’applicazione dove gli verranno passate le vere proprietà.

La quantità aggiuntiva di codice da scrivere è quindi assolutamente ridotta (fondamentalmente limitata alle proprietà hardcodate da passare al componente), e grazie a decoratori e addons, la duplicazione di codice di “supporto” è quasi eliminata del tutto.

Ovviamente Storybook è solo uno strumento e come tale è fondamentale l’uso che se ne fa: se si utilizza solo una volta completato il lavoro nell’app per verificare che i componenti siano ben fatti è si utile, ma diventa una cosa in più da fare. Se invece lo si usa fin dall’inizio dello sviluppo di nuovi componenti adattando il proprio workflow di lavoro per integrarlo, è allora che se ne può trarre il massimo beneficio senza avere un impatto negativo sulla velocità di sviluppo.

Dopo qualche mese di incertezza dovuto al triste esito della startup che per prima ha portato avanti il suo sviluppo, Storybook è oggi attivamente sviluppato ed utilizzato da alcuni big player come airbnb, slack e cloudera. Ma come sempre vedere alcuni esempi di cosa si può fare con uno strumento è una grande fonte di ispirazione:

Compatibile con React e React Native, Storybook promette di diventare uno strumento ancor più diffuso ed apprezzato non appena saranno completati i porting per due big player nel mondo delle SPA come Angular e Vue. Ma già oggi è strumento diffuso e apprezzato, non a caso incluso nella documentazione ufficiale di create react app, il nuovo e “raccomandato” modo di fare il setup di applicazioni React. Non resta che provare, e se quanto scritto finora non è abbastanza, un breve video introduttivo del creatore di Storybook potrà chiarire i rimanenti dubbi.