c't webdev Köln

React

Eine praktische Einführung mit Hooks und TypeScript

Nils Hartmann | @nilshartmann

Installationsanleitung für die Übungen

👉https://nils.buzz/ct_webdev

(wenn noch Zeit ist, sonst später)

Nils Hartmann

https://nilshartmann.net / Twitter:  @nilshartmann

Freiberuflicher Software-Entwickler, Berater und Trainer aus Hamburg

Java

JavaScript, TypeScript

React

Single-Page-Applications

GraphQL

Schulungen und Workshops


https://reactbuch.de

Vorbereitung / Einrichtung für die Übungen 1/2

Schritt 1: Klonen und installieren

  1. Repository klonen:
    git clone https://github.com/nilshartmann/react-training/
    Die Slides findest Du dann hier: react-training/2020_ct_webdev.html
  2. Abhängigkeiten für den JavaScript-Workspace installieren:
    cd react-training/blog-example/workspace
    npm install
    npm start

Vorbereitung / Einrichtung für die Übungen 2/2

Schritt 2: Starten

  1. Starten des Backends (Achtung: Port 7000 muss frei sein!):
    cd react-training/blog-example/backend-rest
    npm start
  2. Starten des JavaScript-Workspaces (Achtung: Port 3000 muss frei sein):
    cd react-training/blog-example/workspace
    npm start

Agenda

Jederzeit: Fragen und Diskussionen!

Beispiel-Anwendung

Teil 1

React Einführung

React

https://reactjs.org

React

  • Minimales API
  • Minimales Feature Set
    • Ihr könnt/müsst viele Entscheidungen selber treffen
  • Bewusste Verstöße gegen Best-Practices

Komponenten in React

Zentrales Konzept in React: Komponenten

Komponenten in React

Unser Beispiel in Komponenten

React Komponenten

  • bestehen aus Logik und UI
  • keine Templatesprache (?)
  • werden deklarativ beschrieben
  • werden immer komplett gerendert (kein 2-Wege-Data-Binding)
  • werden zu ganzen Anwendungen aggregiert

React Komponenten

  • Werden als Funktion mit Hooks oder ES6 Klasse implementiert
    • Hooks seit React 16.8 (Februar 2019)
    • Klassen (noch?) sehr weit verbreitet in bestehendem Code

    Mehr Hintergründe zu Hooks: Ein Jahr React Hooks-API (heise Developer)

  • Keine Templatesprache
    • Wir schreiben unsere UI komplett in JavaScript
    • React bringt eine Spracherweiterung für JS mit, die es uns erlaubt HTML-artigen Code in JavaScript zu schreiben (JSX)
    • (Ist das nicht ein Template?)

Unsere erste Komponente: Hello, World!

  • Schritt-für-Schritt (blog-example/workspace)

Hello World React

PostEditor.js

  import React from "react";
  
  export default function PostEditor(props) {
    const [title, setTitle] = React.useState("");
  
    return (
      <div>
        <label>
          Title
          <input onChange={event => setTitle(event.target.value)} value={title} />
        </label>
      </div>
    );
  }
            

Einbinden der Anwendung in die HTML-Seite

index.html


  <html>
    <-- ... -->
    <body>
      
</body> </html>

index.js


  import React from 'react';
  import ReactDOM from 'react-dom';
  
  import PostEditor from './PostEditor';
  
  ReactDOM.render(<PostEditor />, 
    document.getElementById('root')
  );
  

React Devtools

React Developer Tools für Chrome und Firefox

Untersuchen einer laufenden React Anwendung

create-react-app

User Guide

Bootstrap von neuen React Anwendung

Fertige Konfiguration von React und Webpack mit TypeScript, Sass, Linter

Beispiel: npx create-react-app --template typescript

Der Trainingsworkspace

  • 2020_ct_webdev.html: Die Slides
  • Der Code ist im blog-example Verzeichnis
  • blog-example/workspace: Verzeichnis für Eure Übungen
    👉 Empfehlung: nur diesen Ordner in IDE/Editor öffnen
  • blog-example/material: Code für einige der Übungen
  • blog-example/steps: Fertiger Source-Code nach jeder Übung

Übung #1: Hello-World

Mit Tools, Workspace und React vertraut machen

Schritt #1: Tool Chain starten

  1. cd blog-example/workspace
  2. npm install
  3. npm start
  4. Der Browser sollte automatisch http://localhost:3000 öffnen
  5. Wenn Du "Hello, World" im Browser siehst, ist alles gut!

Schritt #2: Deine erste React-Komponente

Der erste kleine Schritt für die PostEditor-Komponente
  • Ersetze den "statischen" Code in in PostEditor.js mit dem React Code aus den vorherigen Slides.
  • Die Komponente soll PostEditor heißen
  • Sie sollte einen Zustand/State (title) und ein Eingabefeld dafür haben

Teil II

React Komponenten

React Dokumentation

https://reactjs.org/docs/hello-world.html

React: JSX

  • Wird wie HTML hingeschrieben, inkl. Attribute:
    
    <div><input type="text"/></div>
                        
  • Achtung! class-Attribut heißt className:
    
                            <h1 className="title">...</h1>
                        
  • Attribute, die keine Strings sind, müssen in {} eingeschlossen werden:
    
    <Counter label="Count" count={7} showValues={true} />
                        
  • Kann JavaScript Ausdrücke enthalten, eingeschlossen in {}:
    
    <h1>{title ? title.toUpperCase() : "New document"}</h1>
                        
  • CSS-Eigenschaften werden als Objekt übergeben in Camel-Case-Notation:
    
    const styles = { marginLeft: '10px', border: '1px solid red' };
    <h1 style={styles}>...</h1>
                        

React: JSX #2

  • Fragmente (rendern selber kein Element in den DOM, nur ihre Kind-Elemente):
    
    function Choice() { 
      return <>
        <li>Yes</li> 
        <li>No</li>
      </>              
    }  
                        
  • null, false oder boolean, um nichts zu rendern:
    
    function ErrorMessage(props) {
      if (!props.msg) {
        return null; // oder false oder true
      }
    
      return 

    Fehler: {props.msg}

    ; }
  • Kommentare
    
      function MyComponent() {
        return 
    { /* hier ist javascript, deswegen block-kommentare erlaubt */ }
    ; }

React: Properties und Zustand

  • Properties werden der Komponente von außen übergeben (und nicht verändert)
  • Zustand (State) ist eine innere Eigenschaft der Komponente (die verändert werden kann)

Properties ("Props") einer Komponente

  • sind Objekte mit Key-Value-Paaren
  • werden als 1. Methoden-Parameter an Komponente übergeben
  • dürfen nicht verändert werden

            function Header(props) {
                return (
                  <h1 style={{color: props.titleColor}}>{props.title}</h1>
                );
              }
            }
            

// Mit Destructuring
function Header({titleColor, title}) {
    return (
      <h1 style={{color: titleColor}}>{title}</h1>
    );
  }
}
                

Zustand einer Komponente: useState-Hook

  • Beispiel: Inhalt eines Eingabefelds, Daten vom Server, Menu offen oder zu
  • Werte üblicherweise immutable
  • Arbeiten mit Zustand über useState-Hook
  • useState liefert Array mit zwei Werten zurück: aktuellen Zustand, und setter-Funktion um Zustand zu verändern

function HelloWorld(props) {
  const [title, setTitle] = React.useState(props.initialTitle);

  return <input onChange={e => setTitle(e.target.value) value={title} />;
}
                  
  • Aufruf des Setters löst erneutes rendern der gesamten Komponente aus
  • 👆Es können mehrere States erzeugt werden, durch Verwendung mehrerer useState-Aufrufe

React Hooks

Mit React Hooks kann sich eine Komponente in Zustand und Lebenszyklus "einhaken"
  • Hooks sind "normale" Funktionen, müssen aber mit use beginnen (useState, useEffect, ...)
  • Beispiel: Importieren und verwenden von Hooks
    
                    import React from "react";
    
                    function HelloWorld(props) {
                      const [title, setTitle] = React.useState(props.initialTitle);
                      // ...
                    }
                                      
    
                                          import React, { useState } from "react";
                          
                                          function HelloWorld(props) {
                                            const [title, setTitle] = useState(props.initialTitle);
                                            // ...
                                          }
                                                            
    • (React muss immer importiert werden, wenn JSX verwendet wird!)

React Hooks

Es gibt einige Regeln zu beachten, bei der Verwendung von Hooks 👆

(https://reactjs.org/docs/hooks-rules.html)

Einschränkungen:

  • Hooks können nur in Funktionskomponenten (und anderen Hooks) aufgerufen werden
  • Hooks müssen immer in derselben Reihenfolge und auf Top-Level-Ebene verwendet werden
    • Verboten z.B. in Schleifen, if-Abfragen oder in anderen Funktionen
  • Es gibt ein ESLint Plug-in zur korrekten Verwendung der Hooks

Der Hooks-Mechanismus basiert intern darauf, dass React sich die Reihenfolge der useXyz-Aufrufe merkt!

React Hooks

Beispiele für korrekte und unerlaubte Verwendung

              // ERLAUBT:
              function HelloWorld(props) {
                const [greeting, setGreeting] = React.useState(props.initialGreeting);
                const [name, setName] = React.useState(props.initialName);
                // ...
              }
                                

                                    // ERLAUBT:
                                    function HelloWorld(props) {
                                      const [greeting, setGreeting] = React.useState(props.initialGreeting);
                                      const uppercaseGreeting = greeting.toUpperCase(); 
                                      const [name, setName] = React.useState(props.initialName);
                                      // ...
                                    }
                                                      

                                    // VERBOTEN:
                                    function HelloWorld(props) {
                                      const [greeting, setGreeting] = React.useState(props.initialGreeting);
                                      if (greeting !== null) {
                                        const [name, setName] = React.useState(props.initialName);
                                      }
                                      // ...
                                    }
                                                      

                          // VERBOTEN:
                          function HelloWorld(props) {
                            const [title, setTitle] = React.useState(props.initialTitle);
                            if (title === null) {
                              return 

Please enter title first

; } const [body, setBody] = React.useState(""); // ... }

React Hooks

Beispiele für korrekte und unerlaubte Verwendung #2

              // VERBOTEN 
              function HelloWorld(props) {
                function initState() {
                  return React.useState(props.initialGreeting);
                }
                const [greeting, setGreeting] = initState();
              }
            

                // VERBOTEN (initState ist 'normale' Funktion)
                function initState() {
                  return React.useState(props.initialGreeting);
                }

                function HelloWorld(props) {
                  // wäre erlaubt, wenn initState 'useInitState' hieße
                  const [greeting, setGreeting] = initState();
                }
              

Render Zyklus

Virtual DOM

"Rendern" hat leider doppelte Bedeutung!

Übung 2: Post-Editor für unsere Anwendung

Erweitere deine Komponente um neue Features

Schritte

  1. Zusätzlich zum "title", sollte es einen weiteren Zustand und eine Textarea geben: body.
  2. Füge einen "Clear"-Button hinzu, der beide Eingabefeld leert. Das Property auf dem Button ist onClick.
    • Optional: Der "clear"-Button sollte disabled sein, solange beide Felder leer sind. Um ein button-Element zu disablen, kannst Du das disabled Property auf true setzen.

Teil III

React: Hierarchien und Anwendungen

blog-example/steps/3-hierarchy

Listen

JSX hat keine eigenen Konstrukte für Listen

Üblicherweise verwendet man Array.map() um eine Liste von Objekten in eine Liste von JSX Elementen zu überführen

Jedes JSX Element in der Liste benötigt einen List-weit eindeutigen key


const posts = [
  { id: 0, title: 'Hello World', body: 'Lorem ipsum' },
  { id: 1, title: 'React in a Nutshell', body: 'Lets get started with React' }
];

function PostList(props) {
  return props.posts.map(post => (
    <div key={post.id}>
        <h1>{post.title}</h1>
        <p>{post.body}</p>
    </div>
  ))
}

Komponenten-Hierarchien

👉 Wir bauen eine neue Ansicht: Blog-List

Wie kommuniziert unsere Anwendung?

Wir haben zwei Views: Blog-List und Post-Editor

Welche ist sichtbar?

Wie fließen die Daten zwischen den beiden Komponenten?

Datenfluss in React-Anwendungen

  1. In React Anwendungen, Komponenten werden in Hierarchien zu Anwendungen aggregiert. Eine "Anwendung" ist nichts weiter als eine Sammlung von Komponenten
  2. Innerhalb der Hierarchie wird immer nur in eine Richtung kommuniziert: Eltern-Komponenten geben properties an ihre Kinder
  3. Mit den Properties können Daten (Blog Post, angemeldeter Benutzer, ...) von "oben" nach "unten" gereicht werden. Das kann über mehr als eine Hierarchie-Ebene passieren.

    (Eine Komponente kann die Properties oder einen Teil davon ihrerseits weiter nach "unten" reichen)

  4. Mit Properties können außerdem callback-Funktionen nach unten gereicht werden.
  5. Eine Kind-Komponente kann die übergebene Funktion aufrufen und ein "Event" (mit oder ohne Daten) an ihre Eltern-Komponente schicken.
    Dieses Verfahren haben wir bereits beim onChange-Property am input-Feld gesehen!

Smart und Dumb-Komponenten #1

Zur Erinnerung: in React bauen wir Komponenten. Komponenten bestehen aus Logik, Zustand und UI (HTML-Elemente und Styling)

Ein bekanntes Muster ist, die Komponenten in zwei Arten aufzuteilen: Smart (oder Controller)- und Dumb oder (Presentation-)-Komponenten

Technisch sind die Komponenten identisch, also "normale" React-Komponenten

Nur ihre Aufgabe ist anders definiert...

Smart und Dumb-Komponenten #2

Smart-Komponenten enthalten Logik und Zustand

Dumb-Komponenten sind nur zur Darstellung der Daten

Smart-Komponenten reichen Zustand in die Dumb-Komponenten. Diese zeigen den Zustand an

Smart-Komponenten reichen Callback-Funktion als Event-Handler an die Dumb-Komponenten

Wenn in Dumb-Komponenten ein Ereignis eintritt (z.B. Button-Click oder Texteingabe), wird eine Callback-Funktion aufgerufen

Die Callback-Funktion wird dann in der Smart-Komponente aufgerufen und die Verarbeitung ausgeführt

Die Smart-Komponente setzt ihren Zustand neu, und rendert sich und ihre Kinder (die Dumb-Komponenten) neu
Der "Gesamt-Zustand" der Anwendung bleibt somit immer konsistent!

Beispiel

Unsere Smart-Komponente hält eine Liste von Blog-Posts und steuert, welche Ansicht aktiv ist

Die Smart-Komponente gibt die Liste der Blog-Posts an die BlogList zum Anzeigen

Die Smart-Komponente gibt jeweils eine Callback-Funktion an die BlogList und die PostEditor


function App() {
  const [posts, setPosts] = React.useState([]);
  const [view, setView] = React.useState("list");

  function addPost(newPost) {
    // Neuen Post hinzufügen
    setPosts([...posts, newPost]);

    // Wieder Liste anzeigen
    setView("list");
  }

  if (view === "list") {
    return <BlogList posts={posts} onAdd={() => setView("PostEditor")} />
  }

  return <PostEditor onAdd={addPost} />

}            

Beispiel #2

Die BlogList zeigt die übergebene Liste nur an und informiert die App, wenn auf den "Add"-Button gedrückt wurde

Die App kann dann die andere Komponente (PostEditor) anzeigen


  function BlogList(props) {

    return <div>
      // ... Liste anzeigen ...
      <button onClick={props.onAdd}>Add Blog Post</button>
    </div>;
  }            
  

Beispiel #3

Die PostEditor erfasst einen neuen BlogPost und übergibt diesen der Callback-Funktion, so dass die App-Komponente ihn in die Liste der BlogPosts (State) einfügen kann


  function PostEditor(props) {
    const [title, setTitle] = React.useState("");
    const [body, setBody] = React.useState("");

    function addPost() {
      const newPost = {
        title, body
      }

      // App-Komponente informieren
      props.onAdd(newPost);
    }

    return <div>
      // ... Formular rendern ...
      <button onClick={addPost}>Save Post</button>
    </div>;
  }            
  

Übung #3: Baue eine komplette "Anwendung"

Integriere deine bestehende PostEditor-Komponente und die neue PostList Komponente über die App-Komponente

Schritte

  1. kopiere das Material aus blog-example/material/3-hierarchy/src in deinen source Ordner (Du kannst deinen eigenen PostEditor verwenden oder den aus material/3-hierarchy)
  2. Erweitere die App Komponente, so dass sie den PostEditor anzeigt, wenn der User auf den Add Button klickt.

    - In App.js stehen TODOs mit weiteren Infos

  3. ImPostEditor benötigst Du einen Save Button, der die übergebene Callback-Funktion aufruft, die von der App als Property (onSave) übergeben wird.
    Siehe TODO in blog-example/material/3-hierarchy/src/PostEditor.js

Teil IV

Server Zugriffe

Lesen und Schreiben von Daten von einem Backend (REST/HTTP)

blog-example/steps/4-remote

Herausforderungen

  1. Wie machen wir das Laden und Speichern technisch?
  2. Wo steht der Code zum initialen Laden der Blog Posts? (beim Start der Anwendung)
  3. Wo speichern wir?
  4. Wie funktioniert asynchrone Verarbeitung in React?

Server-Calls

Beispiel: fetch

Lesen von Daten mit HTTP GET


// wenn keine weiteren Parameter gesetzt sind, wird ein GET Request ausgeführt            
fetch('http://localhost:7000/posts')
  .then(response => response.json())
  .then(json => /* ... */)
  .catch(ex => console.error('request failed', ex));

          // Oder mit async/await: 
          try {
            const response = await fetch('http://localhost:7000/posts')
            const json = await response.json();
            // ...
          } (catch ex) {
            console.error('request failed', ex)
          }
          

Beispiel #2: fetch

Schreiben von Daten mit HTTP POST

fetch erwartet als 2. Parameter ein Konfigurationsobjekt:

  • method: HTTP Methode (PUT, POST, DELETE, ...)
  • headers: HTTP Header für den Request (z.B. Authorization)
  • body: Der Payload (als)

Der Returnwert ist derselbe wie bei Get


const response = await fetch(url, {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(payload)
})
// ... 
    

Verkettete Ausführung


// Ein Promise (z.B. als Rückgabewert aus einer Funktion)
const promise = ...;
  
// 'then' gibt IMMER ein Promise zurück
const promise2 = promise.then(name => `Hello, ${name}`);
promise2.then(greeting => console.log(greeting));
// Ausgabe nach einer Sekunde: "Hello, Klaus"
  

Fehler unterbrechen die Promise-Kette

Mit catch() kann man den Fehler fangen und darauf reagieren

Catch im Fehlerfall


  const promise = new Promise( /* wie gesehen */ )
    .then(name => {throw new Error("Unexpected Error") })
    .then(greeting => console.log(greeting));
    .catch(error => console.error(`Greeting failed: ${error}`))
  
    // Output: Greeting failed: Unexpected error
    

fetch: Details


  try {
    // 1. fetch returns a Promise, that will be resolved with a
    // Response object when the answer from the server comes in
    const response = await fetch('http://localhost:7000/posts');
  
    // 2. the Response object contains "meta data" about the Response
    // (for ex. http status code) and functions the read the payload,
    // for example from JSON:
    const posts = await response.json();
  
    // btw: What do we do with the answer here in our React application?
    // ???
  } catch (err) {
    // 4. In case something goes wrong, log error
    console.error('request failed', err);
  }
  
  

Fetch API Doku

Wann laden wir die initialen Daten für unsere Anwendung (Blog Posts)?

Laden (und speichern) von Daten

👉Schritt-für-Schritt

steps/3-hierarchy

Fetch-on-Render

Wir können den Server-Aufruf beim Rendern der Komponente triggern

Bis die Daten verfügbar sind (während des laufenden Server Requests) zeigen wir einen Loading Indicator

Seiteneffekte

Server-Aufrufe sind Seiteneffekte (andere Beispiele: DOM manipulieren, WebSocket öffnen)

Seiteneffekte sind in der Renderphase einer Komponente verboten!

useEffekt-Hook

Mit useEffekt kann eine Funktion registriert werden, die nach dem Rendern der Komponente ausgeführt wird


            function App(props) {
              React.useEffect( 
                () => console.log("I will run after EACH render")
              );
            }
          
2. Parameter (array) gibt an, wenn die Funktion ausgeführt werden soll ("dependencies")

              function App(props) {
                React.useEffect( 
                  () => console.log("I will run only once after 1st rendering"),
                  []
                );
              }
            
Nur nach dem 1. Rendern und immer wenn sich die postId ändert:

                function App(props) {
                  React.useEffect(
                    () => console.log("..."), 
                    [props.postId]) 
                  );
                }
              

Beispiel: initiales Laden von Daten

useEffect und (useState) werden verwendet um die initialen Daten zu laden


                function App() {
                  const [posts, setPosts] = React.useState([]);
                
                  React.useEffect(() => {
                    fetch("http://localhost:7000/posts")
                      .then(response => response.json())
                      .then(json => setPosts(json));
                  }, []);

                  return {posts.map(p => (
                    <Post key={p.id} post={p} />
                  ))}
                }
                
                

Speichern von Daten

Als Folge einer Benutzerinteraktion:

In einem Event-Handler können Seiteneffekte verwendet werden!


function App(props) {
  // Laden der Daten, wie zuvor gesehen
  React.useEffect( ... );

  function savePost(post) {
    fetch("http://localhost:7000/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(post)
    })
      .then(response => response.json())
      .then(newPost => setPosts([newPost, ...posts]));
  }


  return
    ...
      <PostEditor onSave={newPost => savePost(newPost)} />
    ...
}
                

Übung: Laden und Speichern von Daten auf einem Server

Implementiere die nächste Version der App-Komponente, die in der Lage ist, die Blog Posts mit fetch zu laden und zu speichern.

Das backend ist bereits fertig. Ihr könnt es starten mit:


                  cd react-training/blog-example/backend-rest
                  npm start
                

Der Server läuft auf Port 7000

Ihr könnt das Backend mit folgender ULR testen (Browser, wget, curl, ...): http://localhost:7000/posts

Schritte

  1. Kopiere dir die App.js-Datei aus blog-example/material/4-remote/App.js in deinen src-Folder.
  2. Darin sind TODOs enthalten, die dir helfen, den Code zum Laden und Speichern hinzufügen.

Teil V

React Anwendungen mit TypeScript

Example: code/blog-example/steps/6-typescript

TypeScript

  1. Einführung in TypeScript
  2. React mit TypeScript

Einführung: Die Sprache TypeScript

TypeScript is a superset of JavaScript that compiles to plain JavaScript ( http://www.typescriptlang.org/)

  • Erweitert JavaScript um ein Typen System
  • Jeder gültige JavaScript Code ist auch gültiger TypeScript Code
  • Mittels des TypeScript Compilers wird aus TS Code JavaScript Code

TypeScript Grundlagen

Typ-Angaben werden hinter einen Bezeichner geschrieben


  // Variablen können Typ-Informationen bekommen
  let foo: string;
  foo = 'yo';
  // Error: number: This type is incompatible with string
  foo = 10;
                 

  // Funktionen
  function sayIt(what: string): string {
    return `Saying: ${what}`;
  }
  
  sayIt('Klaus'); // ok
  sayIt(10); // error
  

  // Arrow Funktionen
  const sayIt = (what: string): string => `Saying: ${what}`;
  
  sayIt('Moin');
  sayIt(123); // Error: Argument of type '123' is not assignable
              // to parameter of type 'string'.
  

Eingebaute Typen


  // string
  let city: string = 'Hamburg';
  
  // boolean
  let isDone: boolean = false;
  
  // number
  let theAnswer: number = 42;
  
  // array (note the [])
  let cities: string[] = ['Hamburg', 'Barcelona'];
  // alternative:
  let languages: Array<string> = ['JavaScript', 'TypeScript'];
  
  // any
  let theUnknown: any = 'Who cares';
  theUnknown = 666; // ok
  theUnknown = true; // ok
  let a: number = theUnknown; // ok
  
  // void
  function log(s: string): void { /* ... */ }
              

Typen können abgeleitet (inferred) werden


  let city = 'Hamburg'; // city ist ein String
  
  city = 42;
  // Fehler: [ts] Type '42' is not assignable to type 'string'.
  

  // Explizite Angabe eines Types (parameter)
  // und abgeleiteter Typ (Return Type der Funktion)
  
  function sayIt(what: string) {
    return `Saying: ${what}`;
  }
  
  const said: string = sayIt('Hello TypeScript'); // ok
  const saidItWrong: number = sayIt('Hello TypeScript'); // error!
  
  
  

Type Check ausschalten

Mit @ts-ignore (als Kommentar) kann wird die Überprüfung der nächsten Zeile ausgeschaltet:


  let city:string = "Hamburg";
  
  city = 20259; // error: [ts] Type '20259' is not assignable to type 'string'.
  
  // @ts-ignore
  city = 20259; // ok                
  

Nützlich in corner cases, die nur schwer mit TypeScript abbildbar sind oder bei Migration

null und undefined

null muss explizit zugelassen werden (strictNullChecks):


  let city:string = null; //Type 'null' is not assignable to type 'string'.
  
  let optionalCity:string|null = null; // OK
              

undefined muss ebenfalls explizit zugelassen werden:


    let city:string = undefined; //Type 'undefined' is not assignable to type 'string'.
    
    let optionalCity:string|undefined = undefined; // OK
    let optionalCity:string|undefined|null = null; // OK
                

Optionale Parameter können mit ? gekennzeichnet werden (erlauben dann auch undefined)


  function greet(name: string, greeting?: string) {
    console.log(`${greeting || 'Hello'}, {name}`);
  }
  
  greet('Susi', 'Moin')// Moin, Susi
  
  // 2. Parameter ist optional:
  greet('Klaus'); // Hello, Klaus
  
  greet('Peter', null); // Argument of type 'null' is not assignable
                        // to parameter of type 'string | undefined'.
              

Eigene Typen

Mit interface und typekönnen eigene Typen (Objekt-Strukturen) definiert werden:


  // Komplexer Typ
  interface Person {
    name: string; // Pflicht
    livesIn?: string; // Optional
  }

  // Alternativ (interface und type fast synonym)
  type Person = { name: string; livesIn?: string; }
  
  const susi: Person = { // OK
    name: 'Klaus',
    livesIn: 'Hamburg'
  };
  const klaus: Person = { // OK (livesIn ist optional)
    name: 'Klaus'
  }
  
  const helmut: Person = {} // Error: Property 'name' is missing
  
  const lukas: Person = {
    name: 'Lukas',
    profession: 'Lokführer'
  } // Error: 'profession' does not exist in type 'Person'.
                 

Eigene Typen II

Eigene Objekt-Typen können sowohl "Attribute" als auch Funktionen enthalten:


            // Komplexer Typ
            type Person {
              name: string; // Pflicht
              greet(greeting: string): string;
            }

            const p:Person = {
              name: "Klaus",
              greet(greeting: string) { 
                return `${greeting}, ${this.name}`
              }
            }
            p.greet("Hello"); // OK
            p.greet(123); // ERR: Argument of type '123' is not 
                          // assignable to parameter of type 'string'.

            const wrong:Person = {
              name: "Susi", // OK
              greet(greeting: number) { return "hello" } 
                // ERR: Type '(greeting: number) => string' is not assignable to 
                //      type '(greeting: string) => string'.
                //      Types of parameters 'greeting' and 'greeting' are incompatible.
                //      Type 'string' is not assignable to type 'number'.
            }
                           

Union Types

Variablen, Parameter etc. können mehr als einen Typ annehmen:


type Person = { name: string };
type Movie = { title: string };

function printNameOrTitle(obj: Person | Movie) { 

  console.log(obj.title); // ERR: Property 'title' does not 
                          // exist on type 'Person | Movie'
 
  if ("title" in obj) {
    // obj ist Movie hier, title ist definiert
    console.log(obj.title);
  } else {
    // obj ist Person hier: name ist definiert
    console.log(obj.name);
  }
}

printNameOrTitle({name: "Klaus"}); //OK
printNameOrTitle({title: "Pulp Fiction"}); //OK
printNameOrTitle({label: "Save"}); // ERR
    
          

String Literal Types

Mit einem String Literal Type kann genau festgelegt werden, welche Ausprägungen ein String annehmen kann. Dadurch sind Enum-ähnliche Konstrukte möglich.



type MODE = "MASTER" | "DETAIL" | "ERROR";

const m:MODE = "MASTER"; // OK
const n:MODE = "FEHLER"; // ERR: Type '"FEHLER"' 
                         // is not assignable to type 'MODE'.

function getView(m: MODE) {
  if (m === "NOT_FOUND") {
     // ERR: This condition will always return 'false' since the 
     // types 'MODE' and '"NOT_FOUND"' have no overlap.       
  } else if (m === "DETAIL") {
      // OK
  }
}
            

React Anwendungen mit TypeScript

State und Properties einer Komponente können mit TypeScript Typen beschrieben weden

Achtung! TypeScript-Dateien, die JSX-Code enthalten, müssen mit .tsx enden!

👉 Lasst uns ausprobieren, wie das funktioniert! (workspace-typescript)

Typsicherheit in Funktionskomponenten


type PostListProps = {
  posts: BlogPost[];
  onAddPost(): void;
};
            

    function BlogList(props: PostListProps) {
      props.posts.length // OK
      
      props.post // compile ERROR: Property 'post' does not exist on type 'PostListProps'.
      props.onAddPost("huhu"); // compile ERROR: Expected 0 arguments, but got 1.
    }
    
            

    // Mit Destructuring
    function BlogList({posts, onAddPost}: GreetingMasterProps) => {
      // ...
    }
    
            

Typ-sichere Verwendung von Komponenten

Code Completion

Unbekanntes Property

Fehlerhafte Verwendung eines Properties

Typ-sicherheit in useState

Der Typ von useState kann grundsätzlich von TypeScript hergeleitet werden


type PostEditorProps = {  onSavePost(post: NewBlogPost): void; };
function PostEdior(props: PostEditorProps) {
  const [title, setTitle] = React.useState("");

  // greeting is string, because initial value is a string
  setGreeting("huhu"); // OK
  setGreeting(666); // ERROR (wrong Type)
  setGreeting(null); // ERROR (wrong Type)
}
            

Du kannst alternativ den Typen auch explizit setzen

Zum Beispiel notwendig, wenn der State mehr als einen Typen aufnehmen kann


type VIEW = "LIST" | "ADD";

function App() {
  const [view, setView] = React.useState<VIEW>("LIST");

  // Mode ist either string "MODE_MASTER" or "MODE_DETAIL"
  // setMode only accepts the string "MODE_MASTER" and "MODE_DETAIL" 

  setMode("NOT_FOUND"); // compile error
  setMode(null); // compile error
}
                            

React Events in TypeScript

Events in React sind Instanzen von React.SyntheticEvent, die die nativen DOM Events wrappen (und so "ähnlich" aussehen)

Der Typ-Parameter für ein Event muss auf den Typ des HTML Elements gesetzt werden, dass das Event auslöst

TypeScript kennt dann die Eigenschaften des Events bei der Verarbeitung


function PostEditor(props) {
  const [title, setTitle] = React.useState("");

  function handleChange(e: React.SyntheticEvent<HTMLInputElement>) {
    setTitle(e.currentTaget.value);
  }

  return <input onChange={handleChange} value={title} />
}              
          
target vs currentTarget

Übung: Typ-sichere React Komponenten

Füge die fehlenden Typ-Informationen zu der App-Anwendung hinzu

WORKSPACE:

Bitte benutze den Workspace blog-example/workspace-typescript.
Dieser enthält die letzte Version unserer Anwendung, ist aber mit TypeScript konfiguriert.

VORBEREITUNG:

  1. Stop deinen laufenden "npm run"-Frontend-Prozess (ctrl+c)
  2. Führe npm install in blog-example/workspace-typescript aus
  3. Führe npm start in blog-example/workspace-typescript aus

Typ Informationen hinzufügen

  1. Workspace: blog-example/workspace-typescript aus
  2. Füge die fehlenden Typ-Informationen in PostList.js und PostEditor.js hinzu
  3. Bevor die Dateien anpasst, ändere die Dateiendung zu .tsx and starte "npm run" ggf. neu (manchmal verhakt sich Webpack beim Umbennen von Dateien).
  4. Weitere Informationen kannst Du in den Dateien finden.

Geschafft! 😊

Vielen Dank für Eure Teilnahme!

Viel Spaß und Erfolg mit React!

Wenn ihr noch Fragen habt, könnt ihr mich erreichen:

Mail: nils@nilshartmann.net

Web: https://nilshartmann.net

Twitter: @nilshartmann