Foto Andrea Mangano
Andrea Mangano

Come creare un set di Icone in React.js

25 novembre 2018
11m
Ti è mai capitato di dover gestire un set di Icone in React? Forse si, forse ancora no. In questo tutorial mostro un approccio d'implementazione per creare una libreria di icone su misura, in modo semplice e modulare e con la massima flessibilità di utilizzo e di aggiornamento.
React.js
Icone
Componenti
Tutorial
Condividi su

Le icone, care vecchie amiche

Le icone, fin dagli albori del web, sono sempre state uno degli elementi più usati nelle interfacce utente per la loro capacità di racchiudere un significato, di rappresentare qualcuno, qualcosa o un’azione.

Questa potenza è espressa attraverso simboli caratteristici, spesso minimali ed essenziali, facili da riconoscere e ricordare.

Le icone sono ovunque. Lo sanno bene i designer che non ne possono far a meno per progettare le loro interfacce, per snellire la struttura delle viste e ricavare preziosi spazi visivi che facilitano l’interazione.

Ebbene, dietro chi progetta le icone per il web, c’è chi come me deve trovare il modo di gestirle durante il processo di sviluppo dell’interfaccia.

La gestione delle icone sulle UI è uno di quei problemi ricorrenti ma mai definitivamente risolto.

Dal semplice utilizzo di immagini o gif nei preistorici siti web, son passato nel corso degli anni all’uso di Sprite CSS, Icon Font fino a giungere all’uso di SVG Sprite con la tecnica degli xLink href.

Poi ogni cosa sull’interfaccia è diventata un componente, così anche le icone!

Scopo del tutorial

In questo tutorial mostro come implementare una libreria di icone attraverso un unico componente React. Costruisco tutto passo dopo passo, con ragionamenti e miglioramenti graduali, in modo che tu possa facilmente seguirmi.

Il risultato ottenuto consente di avere flessibilità nell’utilizzo (scelta icona, colore, dimensione ed eventuale orientamento) e nell’aggiornamento della libreria stessa:

  • Le icone sono componenti (con tutti i vantaggi di utilizzo derivanti)
  • Le icone sono renderizzate come SVG ed embeddate in pagina (il peso è minuscolo e non ci sono richieste http)
  • Il formato vettoriale SVG permette la massima riadattabilità senza perdita di qualità
  • Aggiungere una nuova icona al set è semplice

Infine, questa pratica permette di essere totalmente indipendenti da librerie esterne e di costruire un set di icone su misura.

Iniziamo!

Le icone

Come anticipato, per creare il set di icone utilizzo delle Icone in formato SVG. Non sto qui a discutere i vantaggi nell’avere dei simboli vettoriali. Sicuramente già li conosci.

In caso contrario, troverai decine di articoli e approfondimenti in merito :)

Quando non ho bisogno di icone specifiche o iper-personalizzate, spesso utilizzo il sito https://materialdesignicons.com/ da cui prelevo le icone di mia necessità. Il sito raccoglie le icone appartenenti al Material Design di Google e molte altre che estendono, in linea con lo stile, l’insieme di base.

La cosa che amo di questo sito, è la possibilità di visualizzare l’intero markup SVG dell’icona selezionata. Questa funzionalità mi aiuta a velocizzare il lavoro, a prelevare i path senza la noia di scaricare i file .svg ed aprirli nell’editor.

svg viewer su materialdesignicons.com

Componente Icona: idea base

Prima che inizi a costruire la libreria di icone, faccio qualche riflessione.

Una prima idea implementativa potrebbe essere quella di creare un componente funzionale per ogni icona occorrente nell’interfaccia. Ad esempio:

const UserIcon = ({color, size}) => (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
    >
      <path
        fill={color}
        d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" 
      />
	</svg>
)

Questa scelta mi porterebbe ad avere tanti componenti quante sono le icone presenti.

Posso semplificare! Ciò che varia in ogni componente icona, è il path svg che disegna il simbolo.

È quindi sufficiente un solo componente Icona a cui passo dall’esterno (come prop) un name che identifica l’icona:

<Icon name="USER" />

Potrei definire il componente Icon come segue nel file Icon.js:

import React from "react";
import PropTypes from "prop-types";

// Oggetto con tutti i path delle icone
const ICONS = {
  USER: 'M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z',
  LIKE: 'M23,10C23,8.89 22.1,8 21,8H14.68L15.64,3.43C15.66,3.33 15.67,3.22 15.67,3.11C15.67,2.7 15.5,2.32 15.23,2.05L14.17,1L7.59,7.58C7.22,7.95 7,8.45 7,9V19A2,2 0 0,0 9,21H18C18.83,21 19.54,20.5 19.84,19.78L22.86,12.73C22.95,12.5 23,12.26 23,12V10M1,21H5V9H1V21Z',
  ARROW: 'M16,13V21H8V13H2L12,3L22,13H16M7,11H10V19H14V11H17L12,6L7,11Z',
    ...,
    ...,
}

const Icon = ({name, color, size}) => {

  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
    >
      <path 
        fill={color}
        d={icons[name]} 
      />
	</svg>
  )
}

Icon.PropTypes = {
  name: PropTypes.string.isRequired,
  color: PropTypes.string,
  size: PropTypes.number,
}

Icon.defaultProps = {
  size: 24,
  color: '#000',
}

export default Icon

Un oggetto contiene tutti i path delle icone. Le chiavi identificano il tipo di icona, il valore rappresenta il rispettivo path SVG.

Passando la chiave identificativa dall’esterno, vado a pescare il corrispondente path tra quelli disponibili e lo renderizzo.

Se volessi aggiungere una nuova icona, dovrei solo inserire un nuovo path, tra quelli già presenti.

L’idea regge. Procedo quindi a ripulire un po’ il codice.

Miglioramenti per la gestione e l’uso

Quando il numero di icone cresce ci sono alcuni sconvenienti di gestione e di utilizzo:

  • L’estensione in altezza dell’oggetto ICONS cresce al crescere delle icone inserite. Questo rende poco agevole accedere alla definizione del componente Icon senza l’onere di scrollare la finestra dell’editor fino in fondo.

  • Chi utilizza il componente deve ricordare il nome identificativo di ogni icona. Se ha poca memoria, dovrebbe di volta in volta consultare la documentazione (se esiste!) o accedere al codice sorgente dove le icone stesse son definite (incubo!).

File dedicato icone

Per risolvere il primo problema, basta un attimo. Sposto la costante ICONS in un file separato che chiamo icons.js:

// File icons.js dedicato alla definizione delle icone
export default {
  USER: 'M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z',
  LIKE: 'M23,10C23,8.89 22.1,8 21,8H14.68L15.64,3.43C15.66,3.33 15.67,3.22 15.67,3.11C15.67,2.7 15.5,2.32 15.23,2.05L14.17,1L7.59,7.58C7.22,7.95 7,8.45 7,9V19A2,2 0 0,0 9,21H18C18.83,21 19.54,20.5 19.84,19.78L22.86,12.73C22.95,12.5 23,12.26 23,12V10M1,21H5V9H1V21Z',
  ARROW: 'M16,13V21H8V13H2L12,3L22,13H16M7,11H10V19H14V11H17L12,6L7,11Z',
    ...,
    ...,
}

Importo la costante nella definizione del componente Icon (file Icon.js):

import React from "react";
import icons from "./icons";

// Oggetto con tutti i path delle icone
export const ICONS = icons

const Icon = ({name, color, size}) => {
  // ...
}

// ...

export default Icon

Così facendo il file Icon.js rimane slegato da logiche specifiche (la rappresentazione delle icone) e può essere facilmente riutilizzato in altri progetti.

Suggerimento icone

Il secondo punto in esame necessita più sforzo. Aiuterei sicuramente l’utente nel ricordare il nome dell’icona, fornendogli contestualmente dei suggerimenti.

A questo fine, modifico la definizione del componente Icon.

Piuttosto che passare il nome dell’icona, passo direttamente il path che la disegna. Non preoccuparti, a breve ne capirai il motivo.

const Icon = ({path, color, size}) => {

  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
    >
      <path 
        fill={color}
        d={path} // Passo direttamente il path a parametro, piuttosto che selezionarlo da ICONS
      />
	</svg>
  )
}

Ho sostituito il parametro name con il parametro path.

L’uso del componente Icon è ora il seguente:

<Icon path={SVG_PATH_ICON} />

Così, quando voglio utilizzare un’icona passo il suo path come costante (prendendolo dal file dei path). Ad esempio, in un caso specifico avrò:

// Importo il file contenente tutte le icone
import Icon, {ICONS} from './icons'

// Utilizzo il componente passando il path dell'icona USER 
<Icon path={ICONS.USER} />

In questo modo l’editor fornisce i suggerimenti in automatico. Carino, no? Secondo problema risolto!

suggerimenti constanti icone nell'editor

Orientamento dell’icona

Ci sono alcune icone usate nelle interfacce che cambiano solo l’orientamento. È il caso ad esempio delle icone “Frecce”. Lo stesso simbolo è direzionato verso l’alto o il basso, a sinistra o a destra.

Quando devo implementare questa tipologia di icone, ho davanti due scenari:

  • Utilizzo un SVG dedicato per ognuna
  • Aggiungo la possibilità di ruotare il simbolo orientandolo in una precisa direzione (alto, basso, sinistra o destra)

In genere prediligo la secondo strada per le seguenti ragioni:

  • NON appesantisco la libreria aggiungendo del codice che potrebbe essere evitato;
  • EVITO di allungare la lista di nomi identificativi dell’icona (ARROW_TOP, ARROW_BOTTOM, ARROW_LEFT, ARROW_RIGHT, basta solo ARROW);
  • Se volessi aggiornare il simbolo, dovrei aggiornarne uno piuttosto che quattro
  • Infine, perchè è necessario un minimo sforzo per implementare la rotazione!

Aggiungo per l’appunto una prop direction e un tag graphic <g> a contorno del path dell’SVG:

// ...

const Icon = ({ path, color, size, direction }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24">
      <g transform={`rotate(${direction}, 12, 12)`}>
        <path fill={color} d={path} />
      </g>
    </svg>
  );
};

Icon.PropTypes = {
  path: PropTypes.string.isRequired,
  color: PropTypes.string,
  size: PropTypes.number,
  direction: PropTypes.number, // Aggiunta typing nelle proptype
};

Icon.defaultProps = {
  size: 24,
  color: "#000",
  direction: 0 // Valore di default per la direction (orientamento verso l'alto)
};

// ...

Al tag <g /> aggiungo l’attributo trasform per applicare una trasformazione mediante rotazione.

<g transform={`rotate(${direction}, 12, 12)`}>

Nota che il primo parametro della funzione rotate è utilizzato per definire l’angolo di rotazione, mentre i restanti due ..,12, 12 fissano il punto perno sul quale avviene la rotazione.

Poiché il viewbox (viewBox="0 0 24 24") delle mie icone è un quadrato 24x24 pixel (i valori 0 0 indicano i valori cartesiani x e y da cui inizia il viewbox), dovrò puntare al centro di esso, ovvero al metà del valore 24, appunto 12!

Questo per darti una regola qualora avessi di fronte per il tuo set di icone un viewbox differente.

Al parametro direzione occorre passare i valori 0, 90, 180, -90 per (rispettivamente) orientare l’icona verso l’alto, a destra, in basso e a sinistra.

Nota: Nel codice in alto ho messo come default il valore 0, orientamento verso l’alto (caso comune)

Per evitare che l’utente debba ricordare a memoria tali valori per orientare l’icona, rendo la prop direction “user-friendly”.

Definisco delle costanti in un oggetto DIRECTIONS all’interno del componente Icon:

export const DIRECTIONS = {
  TOP: 0,
  RIGHT: 90,
  BOTTOM: 180,
  LEFT: -90
};

Esporto la costante in modo da renderla disponibile all’utente dall’esterno (come fatto per ICONS).

In questo modo applico facilmente la rotazione desiderata:

// Importo componente e costanti direzioni
import Icon, {ICONS, DIRECTIONS} from './Icon'

// Imposto icona e direzione
<Icon path={ICONS.USER} direction={DIRECTIONS.LEFT} />

Ecco come si presenta l’intero file Icon.js dopo le ultime modifiche:

import React from "react";
import icons from "./icons";
import PropTypes from "prop-types";

export const DIRECTIONS = {
  TOP: 0,
  RIGHT: 90,
  BOTTOM: 180,
  LEFT: -90
};

export const ICONS = icons;

const Icon = ({ path, color, size, direction }) => {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24">
      <g transform={`rotate(${direction}, 12, 12)`}>
        <path fill={color} d={path} />
      </g>
    </svg>
  );
};

Icon.propTypes = {
  name: PropTypes.string.isRequired,
  color: PropTypes.string,
  size: PropTypes.number,
  direction: PropTypes.number
};

Icon.defaultProps = {
  size: 24,
  color: "currentColor",
  direction: DIRECTIONS.TOP
};

export default Icon;

Ottimizzazione sul colore predefinito

Se dai uno sguardo alle defaultProps dell’ultimo snippet di codice di Icon.js, ho sostituito l’iniziale valore di default della prop color da #000 a currentColor.

In tal modo, quando non è passata la prop color sul componente Icon, l’attributo fill del path SVG prenderà come colore, il valore della proprietà CSS color del primo elemento DOM superiore all’icona (sia nel caso in cui sia esplicitamente dichiarata, sia nel caso in cui la si erediti a cascata dall’alto).

Esempio pratico:

<div style={{color: 'red'}}>
  <div>
    <Icon path={ICONS.USER} />
    Andrea
  </div>
</div>

Non c’è la prop color dichiarata sull’Icon. Il fill prenderà il valore corrente della proprietà CSS color (da qui il nome currentColor) del <div /> ad esso superiore (che nello specifico caso è ereditata dal div più in alto dove esplicitamente la dichiarata {{color: 'red'}}.

Questa piccola accortezza evita (nella maggior parte dei casi) di impostare il colore direttamente come proprietà del componente Icon quando il colore dell’icona deve essere uguale al colore del contesto in cui è situata. Ad esempio, nel caso in cui l’icona ha colore uguale al testo che affianca.

Conclusioni

Esistono altri modi per costruire con React una comoda libreria di icone. Ogni approccio ha pro e contro. Nel mio lavoro quotidiano ne ho provati diversi, preferendo (ad oggi) quello appena descritto. Ne apprezzo la semplicità d’implementazione, la buona flessibilità d’uso e la facilità di aggiornamento delle icone.

Ti condivido l’intero esempio su codesandbox, così da giocarci un po’:

Edit 32n3wxnz3p

Alla prossima!

Pillole di React: Render Prop Pattern

16 novembre 2018
React.js
Render Rrop Rattern
Advanced Pattern
Tutorial
L'autore
Foto di Andrea Mangano

Andrea Mangano

Frontend Developer @ iBuildings

Da più di otto anni, costruisco interfacce utente per siti e applicazioni web commerciali, con constante curiosità ed entusiasmo per le nuove tecnologie.

Professionalità, tenacia e propensione al rischio sono i miei tratti distintivi.

Seguimi su twitter: @andreaman87