React Grundlagen

Nils Hartmann | @nilshartmann | nils@nilshartmann.net

Vorbereitung

git clone https://github.com/nilshartmann/react-training

cd react-training

npm install

Slides: react-training/2019_react_grundlagen.html

oder: https://nils.buzz/react-grundlagen

Das Beispiel-Projekt

Workspace

  • 2019_react_grundlagen.html Slides im Root-Verzeichnis
  • code/workspace: Verzeichnis für Eure Übungen
    👉 Empfehlung: dieses Verzeichnis in VS Code/IDE öffnen
  • code/material: Code als Ausgangspunkt für Übungen
  • code/schritte: Fertige Stände nach den einzelnen Teilen

Nils Hartmann

https://nilshartmann.net / @nilshartmann

Freiberuflicher Entwickler, Architekt, Trainer aus Hamburg

Java

JavaScript, TypeScript

React

Single-Page-Anwendungen

GraphQL


https://reactbuch.de

Inhalt

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

React

ist sehr stabil (16.x seit September 2017!)

  • Trotzdem grundlegend neue (zusätzliche) APIs

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 als ES6 Klasse implementiert
    • Hooks API seit React 16.8 (Februar 2019)
    • Klassen noch sehr weit verbreitet in bestehendem Code
  • Keine Templatesprache (stattdessen JavaScript)
    • Templates können HTML-artige Syntax enthalten (JSX)

Eine erste Komponente: Hello, World!

Demo

  • Beispiel Schritt-für-Schritt (code/workspace-live-coding)

Hello World React


import React from "react";

export default function HelloMessage(props) {
  const [name, setName] = React.useState(props.initialGreeting || "");

  return (
    <div>
    <input onChange={event => setName(event.target.value)} value={name} />

    <p>{name}, World</p>
    </div>
  );
}
          

Aufruf

index.html


<html>
  <body>
    
</body> <script src="dist/main.js"></script> </html>

main.js


import React from 'react';
import ReactDOM from 'react-dom';

import HelloMessage from './HelloMessage';

const mountNode = document.getElementById('mount');
ReactDOM.render(<HelloMessage initialGreeting="Hello"/>, mountNode);

Run

Hintergrund: Tooling

Webpack und Babel

Webpack Development Server

  • Zum Ausführen der Anwendung wird ein Webserver benötigt
  • Für die Entwicklung Webpack Dev Server:
    • ...führt Webpack auf Basis der Projekt Webpack Konfiguration aus
    • ...erzeugt Ausgabe in Memory (Performance)
    • ...Hot Reloading: Automatische Aktualisierung nach Code Änderung

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 PROJEKTNAME --typescript

React Devtools

React Developer Tools für Chrome und Firefox

Untersuchen der React Anwendung zur Laufzeit

Übung 1: React Hello-World

Mach dich mit den Werkzeugen vertraut und schreib deine erste React-Komponente

Das Beispiel-Projekt

Workspace

  • 2019_react_grundlagen.html Slides im Root-Verzeichnis
  • code/workspace: Verzeichnis für Eure Übungen
    👉 Empfehlung: dieses Verzeichnis in VS Code/IDE öffnen
  • code/material: Code als Ausgangspunkt für Übungen
  • code/schritte: Fertige Stände nach den einzelnen Teilen

Schritt #1: Tool-Chain starten (falls noch nicht gemacht)

  1. cd code/workspace
  2. npm start
  3. Öffne http://localhost:8080 im Browser
  4. Wenn auf der Seite 'Hello, World' steht, ist alles gut

Schritt #2: Deine erste React-Komponente

  • Implementiere die erste Version der GreetingDetail-Komponente:
    • Füge ein Eingabefeld für den Namen hinzu
    • Der eingegeben Name soll in der Zeile darunter ausgegeben werden
    • Optional: wenn kein Name eingegeben ist (Eingabe leer), soll stattdessen eine Meldung ausgegeben werden (z.B. "Bitte Name eingeben")
  • Als Hilfe kannst Du dir den Code auf den vergangenen Folien anschauen

Teil II

React-Komponenten (Grundlagen)

Ziel-Anwendung

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 (Expressions) enthalten, eingeschlossen in {}:
    
    const title = 'Hello, World';
    <h1>{title.toUpperCase()}</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 GreetingDetail(props) {
  const [greeting, setGreeting] = React.useState(props.initialGreeting);

  return <input onChange={e => setGreeting(e.target.value) value={greeting} />;
}
                  
  • Aufruf des Setters löst erneutes rendern der gesamten Komponente aus
  • Es können mehrere States erzeugt werden, durch Verwendung mehrerer useState-Aufrufe
  • Zustand ist eines der zentralen Konzepte von React

Hintergrund: React Hooks API

Mit React Hooks kann sich eine Komponente in Zustand und Lebenszyklus "einhaken"
  • Vorgestellt Ende 2018, eingeführt mit React 16.8 (Feb 2019)
  • "Normale" Funktionen, müssen aber mit use beginnen (useState, useEffect, ...)
  • Es können eigene Hooks gebaut werden ("custom hooks")
  • Importieren und verwenden von Hooks
    
                    import React from "react";
    
                    function GreetingDetail(props) {
                      const [greeting, setGreeting] = React.useState(props.initialGreeting);
                      // ...
                    }
                                      
    
                                          import React, { useState } from "react";
                          
                                          function GreetingDetail(props) {
                                            const [greeting, setGreeting] = useState(props.initialGreeting);
                                            // ...
                                          }
                                                            
    • (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 GreetingDetail(props) {
                const [greeting, setGreeting] = React.useState(props.initialGreeting);
                const [name, setName] = React.useState(props.initialName);
                // ...
              }
                                

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

React Hooks

Beispiele für unerlaubte Verwendung #1

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

                          // VERBOTEN:
                          function GreetingDetail(props) {
                            const [greeting, setGreeting] = React.useState(props.initialGreeting);
                            if (greeting === null) {
                              return 

Please enter greeting first

; } // ... }

React Hooks

Beispiele für unerlaubte Verwendung #2

              // VERBOTEN 
              function GreetingDetail(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 GreetingDetail(props) {
                  // wäre erlaubt, wenn initState 'useInitState' hieße
                  const [greeting, setGreeting] = initState();
                  // ...
                }
              

Render Zyklus

Virtual DOM

"Rendern" hat leider doppelte Bedeutung!

Übung 2: Detail-Ansicht für unsere Gruß-Anwendung

Erzeuge eine Komponente mit der man einen Namen und eine Grußformel eingeben kann

Schritte

Erweitere deine erste React-Komponente im workspace-Ordner

  1. du brauchst ein zweites Eingabefeld, das greeting heißt
  2. Erweitere deine Komponente so, dass man von außen Properties übergeben kann, die den Zustand der beiden Eingabefelder initialisieren (initialGreeting und initialName)
  3. Füge einen "Clear"-Button hinzu, der den Inhalt beider Eingabefelder löscht (onClick-Event)
  4. Optional: kannst Du den "Clear"-Button disablen, solange beide Eingabefelder leer sind?

Teil III

React-Komponenten: Details, Hierarchien und Anwendungen

Ziel-Anwendung

Themen

  • Darstellung von Listen
  • Architektur-Idee: Controller-Komponente und View-Komponente
  • Durchreichen von Zustand und Callbacks
  • Komponenten als Klassen

Listen

JSX bietet nichts für Listen

Ausgabe typischerweise über Array.map()

Elemente einer Liste brauchen einen eindeutigen Key


const greetings = [
  { id: 0, name: 'Klaus', greeting: 'Hallo' },
  { id: 1, name: 'Susi', greeting: 'Moin' }
];

const GreetingsTable(props) => (
    <table>
      {props.greetings.map(greeting =>
        <tr key={greeting.id}>
            <td>{greeting.name}</td>
            <td>{greeting.greeting}</td>
        </tr>
      )}
    </table>
);

Teil III (b)

Anwendungen und Komponentenhierarchien

Problem: Kommunikation zwischen Komponenten

#1: Welche Komponente soll angezeigt werden (Master oder Detail?)

Problem: Kommunikation zwischen Komponenten

#2: Wo wird der State (Greetings) verwaltet?

Problem #1

Welche Komponente soll angezeigt werden?

Master oder Detail?

Schritt-für-Schritt in code/workspace-live-coding

Controller Komponente #1

Verwaltet den Zustand (u.a. welche Komponente sichtbar ist)

Controller Komponente #2

Rendering der Children


function GreetingController(props) {
  const [mode, setMode] = React.useState(MODE_MASTER);

  if (mode === 'MASTER') {
    return <GreetingMaster />;
  } 

  return <GreetingDetail />;
}            
            

Controller Komponente

Wie wird zwischen den Komponenten kommuniziert?

Beispiel: Child-Komponente will Parent Informationen übermitteln

Callback-Funktionen als Properties #1

Controller Komponente

Kommunikation mit Children: Callback-Funktionen als Properties #2


function GreetingController(props) {
  const [mode, setMode] = React.useState('MASTER');

  if (mode === 'MASTER') {
    return <GreetingMaster onAdd={() => setMode('DETAIL')} />;
  } 

  return <GreetingDetail onSave={() => setMode('MASTER')} />;
}      

  

  function GreetingMaster(props) {
      return (
        // Tabelle mit Greetings ...
        <button onClick={props.onAdd}>Add</button>
      )
  }            
              

Problem #2

Wo wird der State (Greetings) verwaltet?

Wie kommen neue Greetings (GreetingDetail) in die Liste (GreetingMaster)?

Controller Komponente

Verwaltet den "globalen" State

State wird als Property an Children übergeben

Controller Komponente

State wird als Property an Children übergeben #2


function GreetingController(props) {
  const [mode, setMode] = React.useState('MASTER');
  const [greetings, setGreetings] = React.useState(props.initialGreetings);

  if (mode === 'MASTER') {
        return <GreetingMaster
          greetings={greetings}
          onAdd={() => setMode('DETAIL')}
        />
  }
  // ...
}
  

function GreetingMaster(props) {
    return (
      <table>
        { props.greetings.map(g => <tr>...</tr>) }
      </table>
      <button onClick={props.onAdd}>Add</button>
    )
}            
                

Controller Komponente

Verwaltet den "globalen" State

Neues Greeting wird per Callback-Funktion zurück gegeben

Controller Komponente

Neues Greeting wird per Callback-Funktion übergeben #2


  function GreetingController(props) {
    const [mode, setMode] = React.useState('MASTER');
    const [greetings, setGreetings] = React.useState(props.initialGreetings);

    if (mode === 'MASTER') {
      // ...
    }

    return <GreetingDetail 
      onSave={newGreeting => {
        setGreetings: [...greetings, newGreeting];
        setMode('MASTER');
      }} 
    />;
  }      
    

  function GreetingDetail(props) {
    const [name, setName] = React.useState();
    const [greeting, setGreeting] = React.useState();
      return (
        <input name="name" . . . />
        <input name="greeting" . . . />
        <button onClick={() => props.onSave({name: name, greeting: greeting})}>
          Add
        </button>
      )
  }            
                  

Komponenten in Hierarchien

Beispiel: Unsere Anwendung (Zusammenfassung)

Smart und Dumb Components

(Alternativ: Container und Presentation Components)

Übung 3: Eine komplette Anwendung zusammen setzen

Füge deinen bestehenden Detail-View GreetingDetail und einen Master-View über eine Controller-Komponente zusammen

Schritte

  1. kopiere das Material aus code/material/2-hierarchy in deinen src-Ordner (oder dein fertiges GreetingDetail verwenden)
  2. erweitere den GreetingController, so dass dein GreetingDetail angezeigt wird, wenn der Benutzer den Add-Button klickt. Dort gibt es bereits einen Kommentar (TODO), der weitere Details enthält
  3. im GreetingDetail brauchst du einen neuen Knopf, der mit dem neuen Gruß den Callback aufruft

GreetingMaster

GreetingDetail

Exkurs: Komponenten als Klassen

State als this.state und this.setState()

Properties als this.props


      import React from "react";
      
      export default class GreetingDetail extends React.Component {
      
        constructor(props) {
          super(props);
      
          this.state = {
            name: props.initialName,
            greeting: props.initialGreeting
          };
        }
      
        render() {
          return (
            <div>
              <input 
                value={this.state.name} 
                onChange={event => this.setState({name: event.target.value})}
              />
              <input 
                value={this.state.greeting} 
                onChange={event => this.setState({greeting: event.target.value})}
              />
            </div>
          );
        }
      }

Teil IV

Daten lesen und schreiben vom Server

Ziel-Anwendung

Herausforderungen

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

Server-Calls

Beispiel: fetch

Daten lesen per GET


          // Für GET Zugriff reicht es, die URL anzugeben:
          try {
            const response = await fetch('http://localhost:7000/api/greetings')
            const json = await response.json();
            // ...
          } (catch ex) {
            console.error('request failed', ex)
          }
          

// Alternative mit Promise:

fetch('http://localhost:7000/api/greetings')
  .then(response => response.json())
  .then(json => /* ... */)
  .catch(ex => console.error('request failed', ex));

Beispiel #2: fetch

Daten lesen per POST

fetch erwartet als zweiten Parameter ein Objekt mit Konfigurationsparametern, u.a:

  • method: gibt die HTTP Methode an ( PUT, POST, DELETE, ...)
  • headers: Objekt mit HTTP Headern für den Request
  • body: Der Request-Payload (als String)

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

fetch im Detail


  try {
    // 1. fetch gibt ein Promise zurück, dass mit dem
    // Response-Objekt aufgelöst wird, wenn die Antwort vom Server
    // kommt
    const response = await fetch('http://localhost:7000/greetings');
  
    // 2. das Response Objekt enthält eine json() Funktion,
    // die das geparse JSON aus der Antwort zurückliefert
    const greetings = await response.json();
  
    // WAS MACHEN WIR MIT DER ANTWORT?
    // ???
  } catch (err) {
    // 4. Falls etwas schief geht, Fehler loggen
    console.error('request failed', err);
  }
  
  

Fetch API Doku

Wann werden Daten gelesen und geschrieben?

Wann müssen die Daten in der Greeting-Anwendung gelesen werden?

Initiales Laden von Daten

Schritt-für-Schritt (code/workspace-live-coding, vorher schritte/2-hierarchy reinkopieren)

useEffekt-Hook

Der useEffekt-Hook erlaubt es Seiteneffekte zu definierten Zeitpunkten auszuführen
  • Zeitpunkte: nach initialem Rendern der Komponente, nach Rendern durch Aktualisierung und nach dem Entfernen aus dem DOM

            function GreetingController(props) {
              React.useEffect( 
                () => console.log("Ich werde nach JEDEM Rendern ausgeführt")
              );
            }
          
2. Parameter (Array) gibt an, wann der Effekt ausgeführt werden soll:

              function GreetingController(props) {
                React.useEffect( 
                  () => console.log("Ich werde nur nach 1. Rendern ausgeführt"),
                  []
                );
              }
            
Nur nach 1. Rendern ausführen, und wenn sich das greetingId-Prop ändert:

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

useEffect Hook

Zwei Parameter:

  • Callback-Funktion, die aufgerufen wenn entsprechendes Ereignis eintritt (z.B. initiales Rendern abgeschlossen)
  • Ein Array mit Abhängigkeiten:
    • Wenn kein Array angegeben wird, wird der Effekt nach jedem Rendern ausgeführt (achtung! Endlosschleife möglich)
    • Wenn ein leeres Array angegeben wird, wird der Effekt nur nach dem 1. Rendern ausgeführt
    • Wenn Werte angegeben werden, wird der Effekt ausgeführt, wenn sich mind 1 Wert verändert hat

Um auf das Entfernen der Komponente aus dem DOM zu reagieren (z.B Resourcen freigeben), kann die Callback-Funktion eine weitere Funktion zurückliefern, die dann ausgeführt wird:


                  function GreetingController(props) {
                    React.useEffect(
                      () => {
                        console.log("greetingId hat sich geändert"); 
                        return () => console.log("Ich bin entfernt worden") 
                      },
                      [props.greetingId])
                    );
                  }
                

Initiales Laden von Daten

useEffect und (useState) werden verwendet um Daten nach dem 1. Rendern zu laden:


                function GreetingController() {
                  const [mode, setMode] = React.useState(MODE_MASTER);
                  const [greetings, setGreetings] = React.useState([]);
                
                  React.useEffect(() => {
                    async function loadGreetings() {
                      let greetings = null;
                      try {
                        const response = await fetch(BACKEND_URL);
                        greetings = await response.json();
                      } catch (err) {
                        console.error("LOADING GREETINGS FAILED:", err);
                        return;
                      }
                      setGreetings(greetings);
                    }
                
                    loadGreetings();
                  }, []);

                  // ...
                }
                
                

Wann werden Daten gelesen und geschrieben?

Wann müssen die Daten in der Greeting-Anwendung gespeichert werden?

Speichern von Daten

Im Event-Handler als Folge einer Benutzerinteraktion

Im Event-Handler sind wir nicht in der Render-Phase!


function GreetingController(props) {
  async function addGreeting(greetingToBeAdded) {
    let newGreeting;
    try {
      const response = await fetch(BACKEND_URL, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json"
        },
        body: JSON.stringify(greetingToBeAdded)
      });
      if (response.status !== 201) {
        throw new Error("Invalid status code: " + response.status);
      }
      newGreeting = await response.json();
    } catch (err) { ... }
    // use updater function (in setGreetings) to make sure
    // we get the latest 'greetings' value from state
    setGreetings(currentGreetings => [...currentGreetings, newGreeting]);
    setMode(MODE_MASTER);
  }
  return
    ...
      <GreetingDetail onSave={newGreeting => addGreeting(newGreeting)} />
    ...
}
                

Übung: Laden und Speichern der Daten von/auf einem Server

Entwickle auf Basis von fetch eine Version des GreetingControllers, der die Daten auf dem Server laden und dort wieder speichern kann

Der Server ist bereits fertig. Zum Starten:


                cd react-training/code/server
                npm start
              

Zum Testen: http://localhost:7000/greetings

Schritte

  1. Kopiere code/material/3-remote/GreetingController.js in deinen Arbeitsbereich
  2. Die Serverzugriffe sollen in loadGreetings und saveGreeting erfolgen
    • Dort sind bereits entsprechende TODOs für dich eingetragen
    • Die URL des Backends steht in der Konstante BACKEND_URL.
      const BACKEND_URL = "http://localhost:7000/greetings";

Teil V

React mit TypeScript entwickeln

Ziel-Anwendung

Themen

Hintergrund: 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'.
              

Klassen


  class Sayer {
    what: string; // Typ-Angabe für Felder ist erforderlich
  
    constructor(what: string) { // Typ-Angabe für Parameter ist erforderlich
      this.what = what;
    }
  
    // Angabe des Return-Types optional
    sayIt(): string {
      return `Saying: ${this.what}`;
    }
  }
  
  

Klassen (Sichtbarkeiten)


  class Sayer {
    // Erlaubte Sichtbarkeiten: private | protected | public
    private what: string; 
  
    constructor(what: string) { 
      this.what = what;
    }
  
    sayIt(): string {
      return `Saying: ${this.what}`;
    }
  }
  
  const sayer = new Sayer("Susi");
  sayer.what = ""; // ERROR: Property 'greeting' is private
  
  

Klassen (Parameter Properties)


  class Sayer {
    // identisch zu vorherigem Beispiel
    constructor(private what: string) {
    }
  
    sayIt(): string {
      return `Saying: ${this.what}`;
    }
  }
  

Klassen (readonly Felder)


  class Sayer {
  
    readonly what: string; 
  
    // Alternativ:
    constructor(readonly public what: string) {
    }
  
    setWhat(newWhat: string) {
      this.what = newWhat; // ERR Cannot assign to 'what' 
                           // because it is a read-only property.
    }
  }
  

Generics

Klassen können ähnlich wie in Java mit Generics parametrisiert werden:


class Store<T> {
  private value: T;

  setValue(t: T) { this.value = t};
  getValue() { return this.value  ; }
}

const s = new Store<string>();
s.set("Klaus"); // ok
s.set(123); // err

const x:number = s.get(); // err
const y = s.get(); //OK (y ist string)
const z:string = s.get(); OK;

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'.
            }
                           

Typ Kompatibilität

Im Gegensatz zu Java/C# ("nominal typing") sind bei TypeScript zwei Typen kompatibel, sofern sie dieselbe Struktur haben ("structural typing")

  interface Book {
    title: string
  }
  
  interface Movie {
    title: string
  }
  
  const book:Book = { title: "Das Kapital" };
  const movie:Movie = book; // OK, obwohl Book !== Movie
                 

Type Aliase

Mit type können Aliase für Typen definiert werden.

Mittlerweile sehr ähnlich zu Interfaces und fast können sowohl interface als auch type verwendet werden


  type Book = {
    title: string
  }
  
  type Movie = {
    title: string
  }
  
  const book:Book = { title: "Das Kapital" };
  const movie:Movie = { title: "Der weiße Hai" };

  type MODE_DETAIL = "MODE_DETAIL";
  let s:MODE_DETAIL = "MODE_DETAIL";
  s = "MODE_MASTER" // ERROR


                             

Generics

Generische Typen verwenden


  type Person = { name: string };
  type Movie =  { title: string };
  
  let persons:Array<Person> = [];
  let movies:Array<Movie> = [];
  
  persons.push({name: 'Klaus'});
  movies.push({title: 'Batman'});
  
  persons.push({title: 'Casablanca'}) // error ('title' not in Person)
  
  persons = movies; // error
  
        

Methoden

Methoden können beschrieben werden


type AnEvent = { type: string; payload: any }
type ListenerFunction = (e: AnEvent) => void;

class EventEmitter {
  // addEventListener erwartet EINEN Parameter:
  // eine Funktion, die ein 'AnEvent' entgegennimmer
  addEventListener(listenerFn: ListenerFunction) {

    listenerFn(null); // err
    listenerFn({ type: "GREET", payload: "Hello"}); // OK
  }
}    


const c = new EventEmitter();
c.addEventListener( (e) => console.log(e)); // OK
c.addEventListener( (e,p: string) => console.log(e)); 
  // ERR: Argument of type '(e: any, p: string) => void' is not 
  //      assignable to parameter of type '(e: AnEvent) => void'.

          

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
    
          

Intersection Types

Types können kombiniert werden. Sie enthalten dann alle Properties aus allen Types:


type Person = { name: string };
type Birthday = { dateOfBirth: number };

type PersonWithBirthday = Person & Birthday;

const x:PersonWithBirthday = {
  name: "Klaus",
  dateOfBirth: 1976
} // OK

const x:PersonWithBirthday = {
  name: "Klaus"
} // ERR: Missing Property 'dateOfBirth'
      
            

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

Mapped Types

Bestehende Typen können auf neue Typen mit neuen Eigenschaften gemappt werden:

              
type Person = { name: string; lastname: string};

const p:Person = {
    name: "Klaus",
    lastname: "mueller"
}

p.lastname = "Meier"; // OK

// ReadonlyPerson ist ein "mapped type"
type ReadonlyPerson = Readonly<Person>;

const p2:ReadonlyPerson = {
    name: "Klaus",
    lastname: "mueller"
}

p2.name = "Karl"; // Cannot assign to 'name' because it is a read-only property.

React Anwendungen mit TypeScript

State und Properties von Komponenten werden mit Typen beschrieben

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

👨‍💻Zeigen in workspace-typescript

Typsicherheit in Funktionskomponenten


    type GreetingMasterProps = {
        greetings: Greeting[]
        onAdd: () => void
    };
            

    function GreetingMaster(props: GreetingMasterProps) {
      props.greetings.length // OK
      
      props.greeting // compile ERROR: Property 'greeting' does not exist on type 'GreetingMasterProps'.
      props.onAdd("huhu"); // compile ERROR: Expected 0 arguments, but got 1.
    }
    
            

    // Mit Destructuring
    function GreetingMaster({greetings, onAdd}: GreetingMasterProps) => {
      // ...
    }
    
            

Typsichere Verwendung von Komponenten

Code Completion

Unbekanntes Property

Falsche Verwendung eines Properties

Typsicherheit in useState

Der Typ des States bei useState kann abgeleitet


type GreetingDetailProps = { intitialGreeting: string };
function GreetingDetailProps(props: GreetingDetailProps) {
  const [greeting, setGreeting] = React.useState(props.initialGreeting);

  // greeting ist string, weil initialGreeting ein string ist
  setGreeting("huhu"); // OK
  setGreeting(666); // ERROR (falscher Typ)
  setGreeting(null); // ERROR (falscher Typ)
}
            

Oder explizit angeben


type MODE_STATE = "MODE_MASTER" | "MODE_DETAIL";

function GreetingController() {
  const [mode, setMode] = React.useState<MODE_STATE>("MODE_MASTER");

  // Mode ist entweder String "MODE_MASTER" oder "MODE_DETAIL"
  // setMode akzeptiert nur Strings "MODE_MASTER" oder "MODE_DETAIL" 

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

Getypte React Klassen-Komponenten

React.Component ist eine generische Klasse, die einen Typ für Properties und State erwartet


      type Greeting = {name: string; greeting: string};
      
      type GreetingDetailProps = {
          greeting?: Greeting;
          onSave: (newGreeting: NewGreeting) => void;
      }
      
      type GreetingDetailState = {
          name: string;
          greeting: string;
      }
                  

      
      class GreetingDetail
        extends React.Component<GreetingDetailProps, GreetingDetailState> {
        // ...
      }
                  

Typsicherheit in React Klassen-Komponenten

Properties und State sind typsicher


  constructor(props: Props) {
      super(props);
  
      this.state = { name: '', greeting: ''} // OK
  
      // ERROR: Object literal may only specify known properties,
      // and 'aha' does not exist in type 'Readonly<State>'
      this.state = {name: '', greeting: '', aha: 10};
  
      // ERROR: Cannot assign to 'greeting' because
      // it is a constant or a read-only property.
      this.state.greeting = 'no way';
  }
  
  render() {
    // ERROR: Property 'nothere' does not exist on type...
    return <div>{this.props.nothere}</iv>;
  }
                 

React Events in TypeScript

Generisches React Event: React.SyntheticEvent

Wird parametrisiert mit dem Element, auf dem es definiert ist, z.B. HTMLInputElement

TypeScript weiß dann, wie das Event aussieht


function GreetingDetail(props) {
  const [greeting, setGreeting] = React.useState("");

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

  return <input onChange={handleChange} value={greeting} />
}              
          

target vs currentTarget

useRef

useRef liefert Container für beliebigen Typ zurück.
Der gehaltene Wert kann immer entweder vom vorgegebenen Typ sein (z.B. ein HTML Element) oder null




  function GreetingDetail(props) {
    const inputRef = React.useRef<HTMLInputElement>(null);
    // inputRef.current kann entweder HTMLInputElement oder null ein

    function reset() {
      inputRef.current.focus(); // ERR: current kann null sein
      
      if (inputRef.current) {
        inputRef.current.focus(); // OK
      }
    }
  }
            

Übung: Typsichere React-Komponenten

Vervollständige die Typ-Informationen der Anwendung

WORKSPACE:

Bitte arbeite in dem neuen Workspace code/workspace-typescript, der die Anwendung aus dem letzten Schritt enthält, aber in TypeScript implementiert

VORBEREITUNG:

  1. Beende deinen laufenden Webpack Dev Server! (Strg/Ctrl/Cmd+C)
  2. Starte den neuen Dev Server mit TypeScript (statt Babel) in code/workspace-typescript:
    npm start

Aufgabe 1: Typ-Informationen ergänzen

  1. Ergänze die Typ-Informationen in useInput (Vorher umbenennen useApi.js in useApi.tsx. Sobald Du die Datei umbenennst, sollten Compile-Fehler angezeigt werden)
  2. Ergänze die Typ-Informationen in GreetingDetail (Vorher umbenennen GreetingDetail.js in GreetingDetail.tsx. Sobald Du die Datei umbenennst, sollten Compile-Fehler angezeigt werden)

Aufgabe 2: Neuen Mode im GreetingController einführen

  1. Nach dem Speichern eines neuen Greetings soll ein neuer "Feedback-View" mit einer Meldung erscheinen (statt Master), in der steht "Gruß wurde gespeichert" (o.ä.).
  2. Zusätzlich zu der Meldung soll der View einen "OK"-Button anzeigen.
  3. Wenn der OK-Button geklickt wird, soll wieder zum Master gewechselt werden:
    MASTER --onAdd--> DETAIL --onSave--> FEEDBACK --onOk--> MASTER

Teil VI

Zustands-Management mit Redux

Eine typische React-Anwendung in Komponenten

Frage an Euch: was könnte es für Probleme geben?

Problem 1: Über Komponenten verteilter Zustand

Wo muss ich nach Fehlern suchen? Wo ist die Logik?

Problem 2: Getrennte Komponentenhierarchien

Geht entweder nicht oder "Gott-Komponente" entsteht

Problem 3: Gemeinsamer ("globaler") Zustand

Wie kommt Zustand von ganz oben nach ganz unten?

Problem 4: UI und Logik vermischt

Wiederverwendung? React-unabhängigkeit? Testbarkeit?

External Statemanagement

Habt ihr Ideen?

Wie können wir Zustand und/oder Logik aus den Komponenten befreien?

Redux

External Statemanagement

Wiederholung

Render Cycle in Pure React

Redux extrahiert die Verantwortlichkeiten

Demo: Redux & Redux Devtools

Ziel-Anwendung

(npm start in code/schritte/redux/7-redux-complete-app)

Redux im Code

(npm start in code/workspace-live-coding-redux)

Strukturierter Überblick über alle Redux Teile

Event-Handlers werden Action-Creators


export function setFilter(filter) {
  return {
    type: SET_FILTER,
    filter
  };
}
  • Action-Creators erzeugen Action-Objekte
  • Actions sind Kommando-artige Strukturen von Dingen, die die Applikation tun soll
  • Über die dispatch-Methode des Stores werden sie an alle Reducer weiter gegeben
  • Actions bestehen aus einem Typen und einer beliebigen Nutzlast (payload)

Action-Creators machen Server-Calls


export const loadGreeting = greetingId => dispatch => {
fetch(BACKEND_URL+'/'+greetingId)
    .then(response => response.json())
    .then(greetings => dispatch({
        type: SET_GREETINGS,
        greetings
    });
};
  • Die Action wird nicht direkt zurück geliefert, sondern erst später dispatched
  • wir bekommen die dispatch Methode als Parameter, mit der wir später die Action dispatchen

Action-Creators sind die einzigen Teile einer Redux-Anwendung, die asynchrone Operationen ausführen dürfen

Ein einziger Store hält den kompletten Zustand


import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { rootReducer } from './reducers';

// http://redux.js.org/docs/api/createStore.html
const store = createStore(
rootReducer // reducer
);
ReactDOM.render(
<Provider store={store}>
    <GreetingController />
</Provider>,
mountNode
);
    
  • Zentraler Teil der Anwendung
  • Liefert die bereits bekannte dispatch-Methode
  • Der Store wird allen Componenten über die Wrapper-Komponenten Provider zur Verfügung gestellt

Middleware zwischen Dispatch und Store


import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(
  rootReducer, // reducer
  applyMiddleware(thunk) // middleware as enhancer
);
    

Reducers enthalten die Kern-(Business)-Logik


import {combineReducers} from 'redux';

// http://redux.js.org/docs/api/combineReducers.html
export const rootReducer = combineReducers({
greetings, // updates greeting partial state
filter,
mode
});
    
  • Reducer sind pure Funktionen, die den alten Zustand und eine Action bekommen und einen neuen Zustand erzeugen
  • Oft bearbeitet ein Reducer nur einen Teil des Zustands

Teil-Reducer


const mode = (state = MODE_MASTER, action) => {
switch (action.type) {
    case SET_MODE:
        return action.mode;
    default:
        return state;
}
};
    
  • Initialisiert seinen Teilzustand (oft mit einem Default-Parameter)
  • Ändert niemals zustand direkt
  • Sondern liefert einen neuen Zustand (manchmal teilweise als Kopie)

useSelector: Auf den globalen Zustand zugreifen


import { useSelector } from 'react-redux';

function GreetingController() {
  const mode = useSelector(state => state.mode);
  ...
}

useDispatch: Actions auslösen


      import { useDispatch } from 'react-redux';
      
      import * as actions from './actions';
      
      function GreetingController() {
        const dispatch = useDispatch();

        async function addGreeting(greetingToBeAdded) {
          await dispatch(actions.saveGreetingToServer(greetingToBeAdded));
        }
        ...
      }
    

Zusammenfassung Redux-Architektur

Lokaler State ist weiterhin erlaubt!

Es gibt unterschiedliche Arten von "State"

Geschafft 😊

Vielen Dank für Eure Teilnahme!

Kontakt:

Mail: nils@nilshartmann.net

Web: https://nilshartmann.net

Twitter: @nilshartmann