Lavorare con React Hooks e TypeScript
Pubblicato: 2022-03-11Gli hook sono stati introdotti in React nel febbraio 2019 per migliorare la leggibilità del codice. Abbiamo già discusso degli hook React in articoli precedenti, ma questa volta esamineremo come funzionano gli hook con TypeScript.
Prima degli hook, i componenti React avevano due gusti:
- Classi che gestiscono uno stato
- Funzioni che sono completamente definite dai loro oggetti di scena
Un uso naturale di questi era quello di costruire componenti contenitori complessi con classi e componenti di presentazione semplici con funzioni pure.
Cosa sono i React Hook?
I componenti del contenitore gestiscono la gestione dello stato e le richieste al server, che verranno quindi chiamate in questo articolo effetti collaterali . Lo stato verrà propagato ai bambini del contenitore attraverso gli oggetti di scena.
Ma man mano che il codice cresce, i componenti funzionali tendono a trasformarsi in componenti contenitore.
Aggiornare un componente funzionale in uno più intelligente non è così doloroso, ma è un compito che richiede tempo e spiacevole. Inoltre, non è più gradito distinguere molto rigorosamente presentatori e contenitori.
Hooks può fare entrambe le cose, quindi il codice risultante è più uniforme e ha quasi tutti i vantaggi. Ecco un esempio di aggiunta di uno stato locale a un piccolo componente che gestisce una firma di citazione.
// put signature in local state and toggle signature when signed changes function QuotationSignature({quotation}) { const [signed, setSigned] = useState(quotation.signed); useEffect(() => { fetchPost(`quotation/${quotation.number}/sign`) }, [signed]); // effect will be fired when signed changes return <> <input type="checkbox" checked={signed} onChange={() => {setSigned(!signed)}}/> Signature </> }
C'è un grande vantaggio in questo: la codifica con TypeScript è stata fantastica con Angular ma gonfia con React. Tuttavia, codificare gli hook React con TypeScript è un'esperienza piacevole.
TypeScript con Old React
TypeScript è stato progettato da Microsoft e ha seguito il percorso angolare quando React ha sviluppato Flow , che ora sta perdendo trazione. Scrivere classi React con TypeScript ingenuo è stato piuttosto doloroso perché gli sviluppatori di React hanno dovuto digitare sia props
che state
anche se molte chiavi erano le stesse.
Ecco un semplice oggetto di dominio. Realizziamo un'app di preventivo , con un tipo Quotation
, gestita in alcuni componenti crud con stato e props. Il Quotation
può essere creato e il suo stato associato può cambiare in firmato o meno.
interface Quotation{ id: number title:string; lines:QuotationLine[] price: number } interface QuotationState{ readonly quotation:Quotation; signed: boolean } interface QuotationProps{ quotation:Quotation; } class QuotationPage extends Component<QuotationProps, QuotationState> { // ... }
Ma immagina che QuotationPage ora chiederà al server un id : ad esempio, la 678a citazione fatta dall'azienda. Bene, ciò significa che QuotationProps non conosce quel numero importante, non sta racchiudendo esattamente una citazione . Dobbiamo dichiarare molto più codice nell'interfaccia QuotationProps:
interface QuotationProps{ // ... all the attributes of Quotation but id title:string; lines:QuotationLine[] price: number }
Copiamo tutti gli attributi tranne l'id in un nuovo tipo. Hm. Questo mi fa pensare al vecchio Java, che prevedeva la scrittura di un sacco di DTO. Per superare questo, aumenteremo la nostra conoscenza di TypeScript per aggirare il dolore.
Vantaggi di TypeScript con Hooks
Usando gli hook, saremo in grado di sbarazzarci della precedente interfaccia di QuotationState. Per fare ciò, divideremo QuotationState in due diverse parti dello stato.
interface QuotationProps{ quotation:Quotation; } function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
Dividendo lo stato, non dobbiamo creare nuove interfacce. I tipi di stato locale vengono spesso dedotti dai valori di stato predefiniti.
I componenti con ganci sono tutte funzioni. Quindi, possiamo scrivere lo stesso componente che restituisce il tipo FC<P>
definito nella libreria React. La funzione dichiara esplicitamente il suo tipo restituito, impostandolo lungo il tipo props.
const QuotationPage : FC<QuotationProps> = ({quotation}) => { const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }
Evidentemente, usare TypeScript con gli hook React è più facile che usarlo con le classi React. E poiché la digitazione forte è una sicurezza preziosa per la sicurezza del codice, dovresti prendere in considerazione l'utilizzo di TypeScript se il tuo nuovo progetto utilizza hook. Dovresti assolutamente usare gli hook se vuoi un po' di TypeScript.
Ci sono numerosi motivi per cui potresti evitare TypeScript, usando React o meno. Ma se scegli di usarlo, dovresti assolutamente usare anche i ganci.
Caratteristiche specifiche di TypeScript adatto per Hooks
Nel precedente esempio React hooks TypeScript, ho ancora l'attributo number in QuotationProps, ma non ho ancora idea di quale sia effettivamente quel numero.
TypeScript ci fornisce un lungo elenco di tipi di utilità e tre di questi ci aiuteranno con React riducendo il rumore di molte descrizioni dell'interfaccia.
-
Partial<T>
: qualsiasi sottochiave di T -
Omit<T, 'x'>
: tutte le chiavi di T tranne la chiavex
-
Pick<T, 'x', 'y', 'z'>
: Esattamentex, y, z
chiavi daT
Nel nostro caso, vorremmo Omit<Quotation, 'id'>
omettesse l'id della citazione. Possiamo creare un nuovo tipo al volo con la parola chiave type
.
Partial<T>
e Omit<T>
non esistono nella maggior parte dei linguaggi tipizzati come Java, ma aiutano molto per esempi con Forms nello sviluppo front-end. Semplifica l'onere della digitazione.
type QuotationProps= Omit<Quotation, id>; function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); // ... }
Ora abbiamo una citazione senza ID. Quindi, forse potremmo progettare una Quotation
e PersistedQuotation
extends
la Quotation
. Inoltre, risolveremo facilmente alcuni problemi ricorrenti if
undefined
. Dovremmo ancora chiamare la virgoletta variabile, anche se non è l'oggetto completo? Questo va oltre lo scopo di questo articolo, ma lo menzioneremo comunque in seguito.

Tuttavia, ora siamo sicuri che non diffonderemo un oggetto che pensavamo avesse un number
. L'utilizzo di Partial<T>
non comporta tutte queste garanzie, quindi utilizzalo con discrezione.
Pick<T, 'x'|'y'>
è un altro modo per dichiarare un tipo al volo senza l'onere di dover dichiarare una nuova interfaccia. Se è un componente, modifica semplicemente il titolo dell'offerta:
type QuoteEditFormProps= Pick<Quotation, 'id'|'title'>
O semplicemente:
function QuotationNameEditor({id, title}:Pick<Quotation, 'id'|'title'>){ ...}
Non giudicarmi, sono un grande fan del Domain Driven Design. Non sono pigro al punto da non voler scrivere altre due righe per una nuova interfaccia. Uso le interfacce per descrivere con precisione i nomi di dominio e queste funzioni di utilità per la correttezza del codice locale, evitando il rumore. Il lettore saprà che Quotation
è l'interfaccia canonica.
Altri vantaggi dei ganci React
Il team di React ha sempre considerato e trattato React come una struttura funzionale. Hanno usato le classi in modo che un componente potesse gestire il proprio stato e ora si agganciano come tecnica che consente a una funzione di tenere traccia dello stato del componente.
interface Place{ city:string, country:string } const initialState:Place = { city: 'Rosebud', country: 'USA' }; function reducer(state:Place, action):Partial<Place> { switch (action.type) { case 'city': return { city: action.payload }; case 'country': return { country: action.payload }; } } function PlaceForm() { const [state, dispatch] = useReducer(reducer, initialState); return ( <form> <input type="text" name="city" onChange={(event) => { dispatch({ type: 'city',payload: event.target.value}) }} value={state.city} /> <input type="text" name="country" onChange={(event) => { dispatch({type: 'country', payload: event.target.value }) }} value={state.country} /> </form> ); }
Ecco un caso in cui l'uso di Partial
è sicuro e una buona scelta.
Sebbene una funzione possa essere eseguita più volte, l'hook useReducer
associato verrà creato una sola volta.
Estraendo naturalmente la funzione riduttore dal componente, il codice può essere suddiviso in più funzioni indipendenti anziché in più funzioni all'interno di una classe, tutte legate allo stato all'interno della classe.
Questo è chiaramente migliore per la verificabilità: alcune funzioni riguardano JSX, altre il comportamento, altre la logica aziendale e così via.
Non hai più (quasi) bisogno di componenti di ordine superiore. Il pattern Render props è più facile da scrivere con le funzioni.
Quindi, leggere il codice è più facile. Il tuo codice non è un flusso di classi/funzioni/pattern ma un flusso di funzioni. Tuttavia, poiché le tue funzioni non sono associate a un oggetto, potrebbe essere difficile assegnare un nome a tutte queste funzioni.
TypeScript è ancora JavaScript
JavaScript è divertente perché puoi strappare il tuo codice in qualsiasi direzione. Con TypeScript, puoi ancora usare keyof
per giocare con le chiavi degli oggetti. Puoi usare le unioni di tipo per creare qualcosa di illeggibile e non mantenibile - no, non mi piacciono. Puoi usare l'alias di tipo per fingere che una stringa sia un UUID.
Ma puoi farlo con sicurezza nulla. Assicurati che il tuo tsconfig.json
abbia l'opzione "strict":true
. Controllalo prima dell'inizio del progetto, o dovrai rifattorizzare quasi ogni riga!
Ci sono dibattiti sul livello di digitazione che inserisci nel tuo codice. Puoi digitare tutto o lasciare che il compilatore deduca i tipi. Dipende dalla configurazione della linter e dalle scelte della squadra.
Inoltre, puoi ancora fare errori di runtime! TypeScript è più semplice di Java ed evita problemi di covarianza/controvarianza con Generics.
In questo esempio di Animal/Cat, abbiamo un elenco di animali identico all'elenco di gatti. Sfortunatamente, è un contratto in prima linea, non nella seconda. Quindi, aggiungiamo un'anatra all'elenco degli animali, quindi l'elenco di Cat è falso.
interface Animal {} interface Cat extends Animal { meow: () => string; } const duck = {age: 7}; const felix = { age: 12, meow: () => "Meow" }; const listOfAnimals: Animal[] = [duck]; const listOfCats: Cat[] = [felix]; function MyApp() { const [cats , setCats] = useState<Cat[]>(listOfCats); // Here the thing: listOfCats is declared as a Animal[] const [animals , setAnimals] = useState<Animal[]>(listOfCats) const [animal , setAnimal] = useState(duck) return <div onClick={()=>{ animals.unshift(animal) // we set as first cat a duck ! setAnimals([...animals]) // dirty forceUpdate } }> The first cat says {cats[0].meow()}</div>; }
TypeScript ha solo un approccio bivariante per i generici che è semplice e aiuta l'adozione da parte degli sviluppatori JavaScript. Se nomi le tue variabili correttamente, raramente aggiungerai duck
a listOfCats
.
Inoltre, esiste una proposta per l'aggiunta e l'eliminazione di contratti per covarianza e controvarianza.
Conclusione
Di recente sono tornato a Kotlin, che è un buon linguaggio digitato ed è stato complicato digitare correttamente i generici complessi. Non hai quella complessità generica con TypeScript a causa della semplicità della digitazione anatra e dell'approccio bivariante, ma hai altri problemi. Forse non hai riscontrato molti problemi con Angular, progettato con e per TypeScript, ma li hai avuti con le classi React.
TypeScript è stato probabilmente il grande vincitore del 2019. Ha guadagnato React ma sta anche conquistando il mondo del back-end con Node.js e la sua capacità di digitare vecchie librerie con file di dichiarazione abbastanza semplici. Sta seppellendo Flow, anche se alcuni vanno fino in fondo con ReasonML.
Ora, sento che hooks+TypeScript è più piacevole e produttivo di Angular. Non l'avrei pensato sei mesi fa.