Arbeiten mit React Hooks und TypeScript

Veröffentlicht: 2022-03-11

Hooks wurden im Februar 2019 in React eingeführt, um die Lesbarkeit des Codes zu verbessern. Wir haben React-Hooks bereits in früheren Artikeln besprochen, aber dieses Mal untersuchen wir, wie Hooks mit TypeScript funktionieren.

Vor Hooks hatten React-Komponenten zwei Varianten:

  • Klassen , die einen Zustand behandeln
  • Funktionen , die vollständig durch ihre Props definiert sind

Eine natürliche Verwendung davon bestand darin, komplexe Containerkomponenten mit Klassen und einfache Präsentationskomponenten mit reinen Funktionen zu erstellen.

Was sind Reaktionshaken?

Container-Komponenten übernehmen die Zustandsverwaltung und Anfragen an den Server, die dann in diesem Artikel Nebenwirkungen genannt werden. Der Zustand wird über die Requisiten an die untergeordneten Container weitergegeben.

Was sind Reaktionshaken?

Aber wenn der Code wächst, neigen funktionale Komponenten dazu, in Containerkomponenten umgewandelt zu werden.

Das Aufrüsten einer funktionellen Komponente in eine intelligentere ist nicht so schmerzhaft, aber es ist eine zeitaufwändige und unangenehme Aufgabe. Außerdem ist eine strikte Unterscheidung von Presentern und Containern nicht mehr erwünscht.

Hooks können beides, daher ist der resultierende Code einheitlicher und hat fast alle Vorteile. Hier ist ein Beispiel für das Hinzufügen eines lokalen Zustands zu einer kleinen Komponente, die eine Zitatsignatur verarbeitet.

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

Das hat einen großen Bonus – das Codieren mit TypeScript war großartig mit Angular, aber aufgebläht mit React. Das Codieren von React-Hooks mit TypeScript ist jedoch eine angenehme Erfahrung.

TypeScript mit Old React

TypeScript wurde von Microsoft entworfen und folgte dem Weg von Angular, als React Flow entwickelte, das jetzt an Zugkraft verliert. Das Schreiben von React-Klassen mit naivem TypeScript war ziemlich schmerzhaft, da React-Entwickler sowohl props als auch state eingeben mussten, obwohl viele Schlüssel gleich waren.

Hier ist ein einfaches Domänenobjekt. Wir erstellen eine Angebots-App mit einem Quotation , die in einigen groben Komponenten mit Status und Requisiten verwaltet wird. Das Quotation kann erstellt werden, und sein zugehöriger Status kann sich in signiert oder nicht ändern.

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

Aber stellen Sie sich vor, die QuotationPage fragt den Server jetzt nach einer ID : zum Beispiel das 678. Angebot des Unternehmens. Nun, das bedeutet, dass QuotationProps diese wichtige Zahl nicht kennt – es umschließt ein Zitat nicht exakt . Wir müssen viel mehr Code in der QuotationProps-Schnittstelle deklarieren:

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

Wir kopieren alle Attribute außer der ID in einen neuen Typ. Hm. Das erinnert mich an das alte Java, das das Schreiben einer Reihe von DTOs beinhaltete. Um dies zu überwinden, werden wir unser TypeScript-Wissen erweitern, um den Schmerz zu umgehen.

Vorteile von TypeScript mit Hooks

Durch die Verwendung von Hooks können wir die vorherige QuotationState-Schnittstelle loswerden. Dazu teilen wir QuotationState in zwei verschiedene Teile des Zustands auf.

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

Durch die Aufteilung des Zustands müssen wir keine neuen Schnittstellen erstellen. Lokale Zustandstypen werden häufig von den Standardzustandswerten abgeleitet.

Komponenten mit Haken sind alle Funktionen. Wir können also dieselbe Komponente schreiben, die den FC<P> -Typ zurückgibt, der in der React-Bibliothek definiert ist. Die Funktion deklariert explizit ihren Rückgabetyp und legt den Requisitentyp fest.

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

Offensichtlich ist die Verwendung von TypeScript mit React-Hooks einfacher als die Verwendung mit React-Klassen. Und da starke Typisierung eine wertvolle Sicherheit für die Codesicherheit ist, sollten Sie die Verwendung von TypeScript in Betracht ziehen, wenn Ihr neues Projekt Hooks verwendet. Sie sollten auf jeden Fall Hooks verwenden, wenn Sie etwas TypeScript wollen.

Es gibt zahlreiche Gründe, warum Sie TypeScript vermeiden könnten, ob Sie React verwenden oder nicht. Aber wenn Sie sich dafür entscheiden, sollten Sie auf jeden Fall auch Haken verwenden.

Spezifische Merkmale von TypeScript Geeignet für Hooks

Im vorherigen TypeScript-Beispiel für React-Hooks habe ich immer noch das Zahlenattribut in den QuotationProps, aber es gibt noch keinen Hinweis darauf, was diese Zahl tatsächlich ist.

TypeScript gibt uns eine lange Liste von Utility-Typen, und drei davon werden uns bei React helfen, indem sie das Rauschen vieler Schnittstellenbeschreibungen reduzieren.

  • Partial<T> : beliebige Unterschlüssel von T
  • Omit<T, 'x'> : alle Schlüssel von T außer dem Schlüssel x
  • Pick<T, 'x', 'y', 'z'> : Genau x, y, z Schlüssel von T

Spezifische Merkmale von TypeScript Geeignet für Hooks

In unserem Fall möchten wir, dass Omit<Quotation, 'id'> die ID des Zitats weglässt. Mit dem Schlüsselwort type können wir spontan einen neuen Typ erstellen.

Partial<T> und Omit<T> gibt es in den meisten typisierten Sprachen wie Java nicht, hilft aber sehr bei Beispielen mit Forms in der Front-End-Entwicklung. Es vereinfacht die Last des Tippens.

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

Jetzt haben wir ein Angebot ohne ID. Also könnten wir vielleicht ein Quotation entwerfen und PersistedQuotation extends Quotation . Außerdem lösen wir problemlos einige wiederkehrende if oder undefined Probleme. Sollen wir die Variable trotzdem Zitat nennen, obwohl es nicht das vollständige Objekt ist? Das würde den Rahmen dieses Artikels sprengen, aber wir werden es später trotzdem erwähnen.

Jetzt sind wir uns jedoch sicher, dass wir ein Objekt, von dem wir dachten, es hätte eine number , nicht verbreiten werden. Die Verwendung von Partial<T> bietet nicht alle diese Garantien, verwenden Sie sie also mit Diskretion.

Pick<T, 'x'|'y'> ist eine weitere Möglichkeit, einen Typ spontan zu deklarieren, ohne eine neue Schnittstelle deklarieren zu müssen. Wenn es sich um eine Komponente handelt, bearbeiten Sie einfach den Angebotstitel:

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

Oder nur:

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

Verurteilen Sie mich nicht, ich bin ein großer Fan von Domain Driven Design. Ich bin nicht so faul, dass ich keine zwei Zeilen mehr für eine neue Schnittstelle schreiben möchte. Ich verwende Schnittstellen, um die Domänennamen genau zu beschreiben, und diese Hilfsfunktionen für die Korrektheit des lokalen Codes, um Rauschen zu vermeiden. Der Leser wird wissen, dass Quotation die kanonische Schnittstelle ist.

Weitere Vorteile von Reaktionshaken

Das React-Team hat React immer als funktionales Framework betrachtet und behandelt. Sie verwendeten Klassen, damit eine Komponente ihren eigenen Zustand handhaben konnte, und jetzt Hooks als eine Technik, die es einer Funktion ermöglicht, den Zustand der Komponente zu verfolgen.

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

Hier ist ein Fall, in dem die Verwendung von Partial sicher und eine gute Wahl ist.

Obwohl eine Funktion mehrmals ausgeführt werden kann, wird der zugehörige useReducer Hook nur einmal erstellt.

Durch das natürliche Extrahieren der Reduzierfunktion aus der Komponente kann der Code in mehrere unabhängige Funktionen anstatt in mehrere Funktionen innerhalb einer Klasse unterteilt werden, die alle mit dem Zustand innerhalb der Klasse verknüpft sind.

Dies ist eindeutig besser für die Testbarkeit – einige Funktionen befassen sich mit JSX, andere mit Verhalten, andere mit Geschäftslogik und so weiter.

Sie benötigen (fast) keine Higher Order Components mehr. Das Render-Requisiten-Muster ist einfacher mit Funktionen zu schreiben.

Das Lesen des Codes ist also einfacher. Ihr Code ist kein Fluss von Klassen/Funktionen/Mustern, sondern ein Fluss von Funktionen. Da Ihre Funktionen jedoch keinem Objekt zugeordnet sind, kann es schwierig sein, alle diese Funktionen zu benennen.

TypeScript ist immer noch JavaScript

JavaScript macht Spaß, weil Sie Ihren Code in jede Richtung reißen können. Mit TypeScript können Sie immer noch keyof verwenden, um mit den Tasten von Objekten zu spielen. Sie können Type Unions verwenden, um etwas Unlesbares und Unwartbares zu erstellen - nein, das mag ich nicht. Sie können Typalias verwenden, um vorzugeben, dass eine Zeichenfolge eine UUID ist.

Aber Sie können es mit null Sicherheit tun. Stellen Sie sicher, dass Ihre tsconfig.json die Option "strict":true hat. Überprüfen Sie es vor dem Start des Projekts, oder Sie müssen fast jede Zeile umgestalten!

Es gibt Debatten über die Ebene der Eingabe, die Sie in Ihren Code einfügen. Sie können alles eingeben oder den Compiler die Typen ableiten lassen. Dies hängt von der Linter-Konfiguration und der Teamauswahl ab.

Außerdem können Sie immer noch Laufzeitfehler machen! TypeScript ist einfacher als Java und vermeidet Kovarianz-/Kontravarianzprobleme mit Generics.

In diesem Tier/Katze-Beispiel haben wir eine Tierliste, die mit der Katzenliste identisch ist. Leider ist es ein Vertrag in erster Linie, nicht in zweiter Linie. Dann fügen wir der Tierliste eine Ente hinzu, sodass die Liste der Katze falsch ist.

 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 hat nur einen bivarianten Ansatz für Generika, der einfach ist und die Akzeptanz von JavaScript-Entwicklern unterstützt. Wenn Sie Ihre Variablen richtig benennen, werden Sie selten eine duck zu einer listOfCats .

Außerdem gibt es einen Vorschlag zum Hinzufügen von In- und Out-Kontrakten für Kovarianz und Kontravarianz.

Fazit

Ich bin kürzlich auf Kotlin zurückgekommen, das eine gute, getippte Sprache ist, und es war kompliziert, komplexe Generika korrekt einzugeben. Sie haben diese generische Komplexität mit TypeScript aufgrund der Einfachheit der Ententypisierung und des bivarianten Ansatzes nicht, aber Sie haben andere Probleme. Vielleicht sind Sie mit Angular, das mit und für TypeScript entwickelt wurde, nicht auf viele Probleme gestoßen, aber Sie hatten sie mit React-Klassen.

TypeScript war wahrscheinlich der große Gewinner von 2019. Es hat React gewonnen, erobert aber mit Node.js und seiner Fähigkeit, alte Bibliotheken mit recht einfachen Deklarationsdateien zu typisieren, auch die Backend-Welt. Es begräbt Flow, obwohl einige mit ReasonML den ganzen Weg gehen.

Jetzt habe ich das Gefühl, dass hooks+TypeScript angenehmer und produktiver ist als Angular. Das hätte ich vor sechs Monaten nicht gedacht.