Travailler avec React Hooks et TypeScript

Publié: 2022-03-11

Les crochets ont été introduits dans React en février 2019 pour améliorer la lisibilité du code. Nous avons déjà discuté des hooks React dans des articles précédents, mais cette fois nous examinons comment les hooks fonctionnent avec TypeScript.

Avant les crochets, les composants React avaient deux saveurs :

  • Classes qui gèrent un état
  • Fonctions entièrement définies par leurs props

Une utilisation naturelle de ceux-ci était de construire des composants de conteneur complexes avec des classes et des composants de présentation simples avec des fonctions pures.

Que sont les crochets React ?

Les composants du conteneur gèrent la gestion de l'état et des requêtes vers le serveur, qui sera ensuite appelé dans cet article effets secondaires . L'état sera propagé aux enfants du conteneur via les accessoires.

Que sont les crochets React ?

Mais à mesure que le code grandit, les composants fonctionnels ont tendance à se transformer en composants de conteneur.

La mise à niveau d'un composant fonctionnel vers un composant plus intelligent n'est pas si pénible, mais c'est une tâche longue et désagréable. De plus, distinguer très strictement les présentateurs et les contenants n'est plus le bienvenu.

Les crochets peuvent faire les deux, de sorte que le code résultant est plus uniforme et présente presque tous les avantages. Voici un exemple d'ajout d'un état local à un petit composant qui gère une signature de citation.

 // 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 </> }

Il y a un gros bonus à cela : le codage avec TypeScript était génial avec Angular mais gonflé avec React. Cependant, coder des crochets React avec TypeScript est une expérience agréable.

TypeScript avec l'ancien React

TypeScript a été conçu par Microsoft et a suivi la voie angulaire lorsque React a développé Flow , qui perd maintenant du terrain. L'écriture de classes React avec un TypeScript naïf était assez pénible car les développeurs de React devaient taper à la fois les props et state même si de nombreuses clés étaient les mêmes.

Voici un objet de domaine simple. Nous créons une application de devis , avec un type de Quotation , géré dans certains composants crud avec état et accessoires. Le Quotation peut être créé, et son statut associé peut passer à signé ou non.

 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> { // ... }

Mais imaginez que la QuotationPage demande maintenant un identifiant au serveur : par exemple, la 678ème cotation faite par l'entreprise. Eh bien, cela signifie que le QuotationProps ne connaît pas ce nombre important - il n'enveloppe pas exactement un Quotation . Nous devons déclarer beaucoup plus de code dans l'interface QuotationProps :

 interface QuotationProps{ // ... all the attributes of Quotation but id title:string; lines:QuotationLine[] price: number }

Nous copions tous les attributs sauf l'id dans un nouveau type. Hum. Cela me fait penser à l'ancien Java, qui impliquait d'écrire un tas de DTO. Pour surmonter cela, nous augmenterons nos connaissances TypeScript pour contourner la douleur.

Avantages de TypeScript avec crochets

En utilisant des crochets, nous pourrons nous débarrasser de l'ancienne interface QuotationState. Pour ce faire, nous allons diviser QuotationState en deux parties différentes de l'état.

 interface QuotationProps{ quotation:Quotation; } function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }

En divisant l'état, nous n'avons pas à créer de nouvelles interfaces. Les types d'état locaux sont souvent déduits par les valeurs d'état par défaut.

Les composants avec crochets sont tous des fonctions. Ainsi, nous pouvons écrire le même composant renvoyant le type FC<P> défini dans la bibliothèque React. La fonction déclare explicitement son type de retour, en définissant le type d'accessoires.

 const QuotationPage : FC<QuotationProps> = ({quotation}) => { const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); }

Évidemment, utiliser TypeScript avec des crochets React est plus facile que de l'utiliser avec des classes React. Et parce que le typage fort est une sécurité précieuse pour la sécurité du code, vous devriez envisager d'utiliser TypeScript si votre nouveau projet utilise des crochets. Vous devez absolument utiliser des crochets si vous voulez du TypeScript.

Il existe de nombreuses raisons pour lesquelles vous pourriez éviter TypeScript, en utilisant React ou non. Mais si vous choisissez de l'utiliser, vous devez également utiliser des crochets.

Caractéristiques spécifiques de TypeScript adaptées aux crochets

Dans l'exemple TypeScript des hooks React précédent, j'ai toujours l'attribut number dans QuotationProps, mais il n'y a encore aucune idée de ce qu'est réellement ce nombre.

TypeScript nous donne une longue liste de types d'utilitaires, et trois d'entre eux nous aideront avec React en réduisant le bruit de nombreuses descriptions d'interface.

  • Partial<T> : toutes les sous-clés de T
  • Omit<T, 'x'> : toutes les clés de T sauf la clé x
  • Pick<T, 'x', 'y', 'z'> : Exactement les touches x, y, z de T

Caractéristiques spécifiques de TypeScript adaptées aux crochets

Dans notre cas, nous voudrions Omit<Quotation, 'id'> pour omettre l'id de la citation. Nous pouvons créer un nouveau type à la volée avec le mot-clé type .

Partial<T> et Omit<T> n'existent pas dans la plupart des langages typés tels que Java, mais aident beaucoup pour les exemples avec Forms dans le développement frontal. Cela simplifie la tâche de dactylographie.

 type QuotationProps= Omit<Quotation, id>; function QuotationPage({quotation}:QuotationProps){ const [quotation, setQuotation] = useState(quotation); const [signed, setSigned] = useState(false); // ... }

Nous avons maintenant un devis sans identifiant. Donc, peut-être pourrions-nous concevoir un Quotation et PersistedQuotation extends Quotation . Aussi, nous résoudrons facilement certains problèmes récurrents if ou undefined . Devrions-nous encore appeler la citation variable, même si ce n'est pas l'objet complet ? Cela dépasse le cadre de cet article, mais nous le mentionnerons plus tard de toute façon.

Cependant, maintenant nous sommes sûrs que nous ne diffuserons pas un objet que nous pensions avoir un number . L'utilisation de Partial<T> n'apporte pas toutes ces garanties, alors utilisez-le avec discrétion.

Pick<T, 'x'|'y'> est une autre façon de déclarer un type à la volée sans avoir à déclarer une nouvelle interface. S'il s'agit d'un composant, modifiez simplement le titre du devis :

 type QuoteEditFormProps= Pick<Quotation, 'id'|'title'>

Ou juste:

 function QuotationNameEditor({id, title}:Pick<Quotation, 'id'|'title'>){ ...}

Ne me jugez pas, je suis un grand fan du Domain Driven Design. Je ne suis pas paresseux au point de ne pas vouloir écrire deux lignes de plus pour une nouvelle interface. J'utilise des interfaces pour décrire précisément les noms de domaine, et ces fonctions utilitaires pour l'exactitude du code local, en évitant le bruit. Le lecteur saura que Quotation est l'interface canonique.

Autres avantages des crochets React

L'équipe React a toujours considéré et traité React comme un cadre fonctionnel. Ils ont utilisé des classes pour qu'un composant puisse gérer son propre état, et maintenant des crochets comme technique permettant à une fonction de garder une trace de l'état du composant.

 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> ); }

Voici un cas où l'utilisation de Partial est sûre et un bon choix.

Bien qu'une fonction puisse être exécutée plusieurs fois, le crochet useReducer associé ne sera créé qu'une seule fois.

En extrayant naturellement la fonction de réduction du composant, le code peut être divisé en plusieurs fonctions indépendantes au lieu de plusieurs fonctions à l'intérieur d'une classe, toutes liées à l'état à l'intérieur de la classe.

C'est clairement mieux pour la testabilité - certaines fonctions traitent de JSX, d'autres de comportement, d'autres de logique métier, etc.

Vous n'avez (presque) plus besoin de composants d'ordre supérieur. Le modèle d'accessoires de rendu est plus facile à écrire avec des fonctions.

Ainsi, la lecture du code est plus facile. Votre code n'est pas un flux de classes/fonctions/modèles mais un flux de fonctions. Cependant, comme vos fonctions ne sont pas attachées à un objet, il peut être difficile de nommer toutes ces fonctions.

TypeScript est toujours JavaScript

JavaScript est amusant car vous pouvez déchirer votre code dans n'importe quelle direction. Avec TypeScript, vous pouvez toujours utiliser keyof pour jouer avec les touches des objets. Vous pouvez utiliser des unions de type pour créer quelque chose d'illisible et de non maintenable - non, je n'aime pas ça. Vous pouvez utiliser un alias de type pour prétendre qu'une chaîne est un UUID.

Mais vous pouvez le faire avec une sécurité nulle. Assurez-vous que votre tsconfig.json a l'option "strict":true . Vérifiez-le avant le début du projet, ou vous devrez refactoriser presque chaque ligne !

Il y a des débats sur le niveau de frappe que vous mettez dans votre code. Vous pouvez tout taper ou laisser le compilateur déduire les types. Cela dépend de la configuration du linter et des choix de l'équipe.

De plus, vous pouvez toujours faire des erreurs d'exécution ! TypeScript est plus simple que Java et évite les problèmes de covariance/contravariance avec les génériques.

Dans cet exemple Animal/Chat, nous avons une liste Animal identique à la liste Chat. Malheureusement, c'est un contrat en première ligne, pas en seconde. Ensuite, nous ajoutons un canard à la liste des animaux, donc la liste des chats est fausse.

 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 n'a qu'une approche bivariante pour les génériques qui est simple et facilite l'adoption par les développeurs JavaScript. Si vous nommez correctement vos variables, vous ajouterez rarement un duck à un listOfCats .

En outre, il existe une proposition pour ajouter des contrats d'entrée et de sortie pour la covariance et la contravariance.

Conclusion

Je suis récemment revenu à Kotlin, qui est un bon langage typé, et c'était compliqué de taper correctement des génériques complexes. Vous n'avez pas cette complexité générique avec TypeScript en raison de la simplicité du typage de canard et de l'approche bivariante, mais vous avez d'autres problèmes. Vous n'avez peut-être pas rencontré beaucoup de problèmes avec Angular, conçu avec et pour TypeScript, mais vous en avez eu avec les classes React.

TypeScript a probablement été le grand gagnant de 2019. Il a gagné React mais est également en train de conquérir le monde du back-end avec Node.js et sa capacité à taper d'anciennes bibliothèques avec des fichiers de déclaration assez simples. C'est enterrer Flow, même si certains vont jusqu'au bout avec ReasonML.

Maintenant, je pense que hooks + TypeScript est plus agréable et productif qu'Angular. Je n'aurais pas pensé ça il y a six mois.