React Training

Nils Hartmann | @nilshartmann

Vorbereitung

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

oder: git pull

Slides

Lokal: 2020_1211_react.html

oder online: https://react.schule/2020_1211_react

Nils Hartmann

https://nilshartmann.net / @nilshartmann

Freiberuflicher Software-Entwickler, Berater und Trainer aus Hamburg

Java

JavaScript, TypeScript

React

Single-Page-Applications

GraphQL

Schulungen und Workshops

Alle Dienstleistungen auch online!


https://reactbuch.de

Zeitplan

Dienstag

  • 9:00 bis 12:30 Uhr
  • Mittagspause
  • 13:30 bis 17:00 Uhr

Mittwoch

  • 9:00 bis 12:30 Uhr
  • Mittagspause
  • 13:30 bis 17:00 Uhr

Kürze Pausen zwischendurch

Agenda

Jederzeit: Fragen und Diskussionen!

Fragen per Audio oder per Chat

Teil 1

React Einführung

Beispiel-Anwendung

React

https://reactjs.org

React

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?) verbreitet in bestehendem Code
  • 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)

Unsere erste React-Komponente

Sehen wir uns die wichtigsten Konzepte einer React-Komponente im Code an

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

Hello World React

PostEditor.js

  import React from "react";
  
  export default function PostEditor() {
    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 my_new_app_name

Der Trainingsworkspace

  • 2020_1211_react.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
  • blog-example/steps/XX/build/index.html: Die fertige Anwendung nach einem Schritt zum Öffnen im Browser

Ü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 PostEditor.js
  • Die Komponente soll PostEditor heißen
  • Sie sollte einen Zustand/State (title) und ein Eingabefeld dafür haben
  • Als Hilfe kannst Du den Code aus den vorherigen Slides ansehen.
  • Optional: mache ein paar Experimente mit der Komponente, zum Beispiel:
    • Kannst Du unter dem Eingabefeld den gerade eingegebenen Text ausgeben lassen, so dass dieser immer dem aktuellen Inhalt des Eingabefeldes entspricht?
  • Eine mögliche Lösung für diese Aufgabe findest Du in blog-example/steps/1-hello-world

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 pures JavaScript 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 */ }
    ; }

Im Detail: Zustand einer Komponente und der 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() {
  const [title, setTitle] = React.useState("");

  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
  • Zustand ist eines der zentralen Konzepte von React

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 * as React from "react";
    
                    function HelloWorld() {
                      const [title, setTitle] = React.useState("");
                      // ...
                    }
                                      
    
                                          import React, { useState } from "react";
                          
                                          function HelloWorld() {
                                            const [title, setTitle] = useState("");
                                            // ...
                                          }
                                                            
    • (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() {
                const [greeting, setGreeting] = React.useState("");
                const [name, setName] = React.useState("");
                // ...
              }
                                

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

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

React Hooks

Beispiele für korrekte und unerlaubte Verwendung #2

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

Please enter greeting first

; } const [name, setName] = React.useState(""); // ... }

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

useState: Details

Wird verwendet, um den internen Zustand (State) einer Komponente zu verwalten

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

                  // Aufruf von setTitle führt dazu, dass Komponente neu gerendert wird
                  // d.h. diese Funktion (PostEditor) wird erneut ausgeführt
                }
              

React: Properties

  • Mit Properties können einer Komponente von außen Werte und Eigenschaften übergeben werden
    
                function PostEditor() {
                    // type, onChange und value sind Properties!
    
                    return (
                      <input type="text" onChange={() => setTitle("...")} value={title}</h1>
                    );
                  }
                }
                
  • Properties können innerhalb der Komponente nicht verändert werden
  • Zum Vergleich: Zustand (State) ist eine innere Eigenschaft der Komponente (die verändert werden kann)

Properties ("Props") einer Komponente

  • Komponenten können beliebig eigene Properties definieren
  • Bei der Angabe der Properties bei der Verwendung einer Komponente werden die Properties wie von HTML gewohnt in der Notiation name=value hingeschrieben

<PostEditor initialTitle="Hello" initialBody="World" />
 
  • ...alle angegebenen Properties werden "gesammelt" und in einem Objekt im 1. Methoden-Parameter an Komponente übergeben

            function PostEditor(props) {
                // props: { initialTitle: "Hello", initialBody: "World" }
                const [title, setTitle] = React.useState(props.title);
                const [body, setBody] = React.useState(props.body);

                return ...;
              }
            }
            

// Mit Destructuring
function PostEditor({initialTitle, initialBody}) {
   const [title, setTitle] = React.useState(title);
   const [body, setBody] = React.useState(body);

   return ...;
  }
}
                

Properties: Typen

  • Properties können alle bekannten JavaScript-Typen aufnehmen und sind grundsätzlich optional

function UserDetails(props) {
   return <div>
     Address: {props.address.street} {props.address.city}
     Member since: {props.memberSince} years
     {props.premium ? "Premium" : "Regular"} membership
     </div>
  }
}
                

// Verwendung
const address = { street: "Highway", city: "Hamburg" };                   

<UserDetails address={address} memberSince={2} />
  

// oder:
<UserDetails address={{ street: "Highway", city: "Hamburg" }} memberSince={2} />

                

Render Zyklus

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

Erweitere deine Komponente um neue Features und Properties

Schritte 1/2

  1. Zusätzlich zum "title", sollte es einen weiteren Zustand und eine Textarea geben: body. Ein textarea-Element hat dieselbe API wie ein input-Feld.
  2. Füge einen "Clear"-Button hinzu, der beide Eingabefeld leert. Das Property auf dem Button ist onClick.
    • 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.

Schritte 2/2

  1. Passe die Signatur der PostEditor-Funktion an, so dass die Komponente Properties für die initiale Belegung der beiden Eingabefelder (title und body) entgegen nimmt. Die Properties sollen initialTitle bzw. initialBody heißen.
  2. Modifiziere die useState-Aufrufe, so dass sie als initial Werte die entsprechenden Werte aus den Properties übernehmen (statt wie bisher ein Leerstring)
  3. Die Komponente sollte weiterhin auch funktionieren, wenn der Aufrufer keine Properties übergibt.
  4. Übergib bei der Verwendung von PostEditor in index.js Initial-Werte für die beiden Properties
  5. Eine mögliche Lösung für diese Aufgabe findest Du in blog-example/steps/2-editor-mit-props

Virtual DOM

"Rendern" hat leider doppelte Bedeutung!

Virtual DOM

Beispiel: blog-example/steps/2b-rendering

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 => (
    <article key={post.id}>
        <h1>{post.title}</h1>
        <p>{post.body}</p>
    </article>
  ))
}

Komponenten-Hierarchien

Wie kommuniziert unsere Anwendung?

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

Welche ist sichtbar?

Wie fließen die Daten zwischen den beiden Komponenten?

👉 Wir bauen eine Anwendung: mit Blog-List und Post-Editor

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: 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
  4. Eine mögliche Lösung für diese Aufgabe findest Du in blog-example/steps/3-hierarchy

React: Komponenten als ES6 Klassen

React: Rendering

Jede React Komponenten-Klasse braucht eine render-Methode:

  • wird aufgerufen beim ersten Rendering und wenn sich der Zustand ändert
  • analog zur Komponente als Funktion

Properties einer Komponente

  • Properties werden über den Konstruktor in die Komponente hineingereicht
  • Zugriff über this.props

              class Header extends React.Component {
                constructor(props) {
                  super(props);
                }
  
                render() {
                  return (
                    <h1 style={{color: this.props.titleColor}}>{this.props.title}</h1>
                  );
                }
              }
              
  • Zum Vergleich: Komponente als Funktion

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

Zustand einer Komponente

  • Initialisieren im Konstruktor mit this.state={}
  • Zustand lesen über this.state
  • Zustand setzen über this.setState()
    • Achtung: kein "reiner" Setter
    • Führt alten und neuen Zustand zusammen
    • Wird asynchron ausgeführt!
    • Löst erneutes rendern der gesamten Komponente aus

Beispiel: State in Komponente als Klasse


      import React from "react";
      
      export default class PostEditor extends React.Component {
      
        constructor(props) {
          super(props);
      
          this.state = {
            title: props.initialTitle,
            body: props.initialBody
          };
        }
      
        render() {
          return (
            <div>
              <input 
                value={this.state.title} 
                onChange=
                  {event => this.setState({title: event.target.value})}
              />
              <input 
                    value={this.state.body} 
                    onChange=
                      {event => this.setState({body: event.target.value})}
              />
            </div>
          );
        }
      }

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)


// JavaScript Promise API:

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

Hintergrund: Promises


// Ein ausgedachtes Promise, dass "irgendwann" den String "Klaus" zurückgibt
const promise = ...;
  
// 'then' gibt IMMER ein Promise zurück
const promise2 = promise.then(name => `Hello, ${name}`);
promise2.then(greeting => console.log(greeting));
// Ausgabe "irgendwann": "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
    

Beispiel: fetch

Lesen von Daten (mit HTTP GET)


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

async / await

"Wrapper" um Promises

Erlaubt es, asynchronen Code "linear" hinzuschreiben

  • Seit ECMASCript 2017 Standard

async / await

await wartet bis ein Promise aufgelöst wird


    function greet(name) { return new Promise(...); }
    
    // await can only be used in 'async'-functions:
    async function sayWhat(name) {
      const greeting = await greet(name);
    
      // greeting is a string!
      console.log(greeting); // => Hello, ...
    }
                      

async / await

async machen eine Funktion zu einer "async function"

Eine async Funktion gibt   immer ein Promise zurück


  async function greet(name) {
    return `Hello, ${name}`;
  }
  
  const greeting = greet('Klaus');
  
  // greeting is a Promise!
  console.log(greeting instanceof Promise); // => true
  console.log(greeting) // => Promise {<resolved>: "Hello, Klaus"}
                  

async Funktion als Fat Arrow Funktionen:


  const greet = async (name) => `Hello, ${name}`;                
    
  const greeting = greet('Klaus');
    
  // greeting is a promise!
  console.log(greeting instanceof Promise); // => true
  console.log(greeting) // => Promise {<resolved>: "Hello, Klaus"}
                    

async / await

Anstadt then oder catch-Ketten kannst Du einfach mehrere await Statements verwenden und try/catch für die Fehlerbehandlung:


  // Return promises
  async function loadUser(username) { . . . };
  async function loadProfileDate(userId) { . . .};
  
  async function loadData() {
    try {
      const user = await loadUser('klaus');
      const profile = await loadProfileData(user.userId);
    } catch (e) {
      console.error("Something failed")
    }
  }
                      

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 String!)

Der Returnwert ist derselbe wie bei GET


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

fetch: Zusammenfassung


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

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

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) {
  // load, as seen
  React.useEffect( ... );

  function addPost(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
    ...
      <AddPost onAdd={newPost => addPost(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 blog-example/material/4-remote/App.js in deinen src-Folder und füge dort den Code zum Laden und Speichern hinzu.
  2. In App.js sind TODOs enthalten.
  3. Füge einen Loading Indicator hinzu
    • Wenn Du ?slow an die URLs im fetch-Aufruf hängst, wird der Server-Call künstlich verlangsamt
    • Füge einen neuen Zustand in der App-Komponente hinzu, der festhält, ob ein Server-Call gerade läuft
    • Gib während des laufenden Server-Calls eine Meldung aus ("Bitte warten Sie")
  4. Mögliche Lösungen für diese Aufgabe findest Du in blog-example/steps/4-remote und blog-example/step/4-remote-with-loading

Komplexer Zustand

Wenn eine Komponente mehrere Zustände hält, die nicht unabhängig sind, kannst Du auch ein Objekt als Zustand verwende:


function App() {
  const [fetchState, setFetchState] = React.useState({
    loading: false,
    posts: []
  });

  ...;
}              
            

Wenn Du den Zustand änderst, musst Du ein neues Objekt erzeugen! Das bestehende Objekt darf nicht verändert werden


    function App() {
      const [fetchState, setFetchState] ...;

      // OK:
      setFetchState({
        loading: true
      })

      // FALSCH:
      fetchState.loading = true;
      setFechtState(fetchState)
    }              
        
  

Custom Hooks

Eigene Hooks können für wiederkehrende Aufgaben genutzt werden

Beispiele: Laden von Daten, Arbeiten mit spezialisiertem State, ...

Custom Hooks

  • ...müssen mit usebeginnen
    • (ansonsten kann der Linter nicht auf mögliche Fehler prüfen!)
  • ...dürfen nur aus anderen (Custom) Hooks und Komponenten aufgerufen werden
  • ...erwarten "normale" Funktionsparameter (nicht zwingend ein Objekt wie bei Komponenten)
  • ...dürfen andere (Custom) Hooks verwenden
  • ...beliebiger Rückgabewert möglich (alle Typen erlaubt, auch JSX!)

Beispiel: Ein Custom Hook zum Laden von Daten


import React from "react";

export default function useApi(url, initialValue) {
  const [isLoading, setLoading] = React.useState(false);
  const [data, setData] = React.useState(initialValue);

  React.useEffect(
    () => {
      async function loadData() {
        try {
          setLoading(true);
          setData(initialValue);
          const response = await fetch(url);
          const json = await response.json();
          setData(json);
        } catch (err) {
          console.error("LOADING DATA FAILED:", err);
          setData(initialValue);
        } finally {
          setLoading(false);
        }
      }
      loadData();
    },
    // Make sure, this Hook only runs (after initial) when URL changes
    [url]
  );

  return [data, setData, isLoading];
}

Teil V

React Anwendungen mit TypeScript

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

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!

Empfehlung: bei typecasts "as" Notation verwenden (x as string vs. <string>x), gibt sonst manchmal Probleme mit den spitzen Klammern.

👉 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

In TypeScript gibt es für die konkreten Events eigene Typen (z.B. ChangeEvent, MouseEvent)

Die Event-Typen erwarten einen Typ-Parameter, der auf den Typ des HTML Elements gesetzt werden muss, das 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.ChangeEvent<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

Übung: Typ Informationen hinzufügen

Füge die fehlenden Typ-Informationen in PostList.js und PostEditor.js hinzu

  1. Benenne PostList.js in PostList.tsx um und starte npm start neu
  2. Stelle PostList.tsx auf TypeScript um
  3. Benenne PostEditor.js in PostEditor.tsx um und starte npm start neu
  4. Stelle PostEditor.tsx auf TypeScript um
  5. Weitere Informationen kannst Du in den Dateien finden.
  6. Eine mögliche Lösung für diese Aufgabe findest Du in blog-example/steps/5-typescript

Part VI

Testing React Applications

Exkurs: Material UI

Beispiel: steps/6-tests-with-material-ui

Material UI

https://material-ui.com/

  • React-Komponenten, mit denen Googles Material Design realisiert werden kann
  • Viele Basis-Komponenten wie Button, Text-Felder, Typographie etc
  • Die Komponenten kümmern sich um das korrekte CSS samt Fonts und Icons, so wie es das Material Design vorsieht

function PostList(props) {
  return <>
      <Fab color="primary" aria-label="Add Post" onClick={props.onAddPost}>
        <AddIcon />
      </Fab>
      {posts.map(p => (
        <Card key={p.id}>
            <Typography variant="overline">
              {formattedDate(p.date)}
            </Typography>
            <Typography variant="h2">
              {p.title}
            </Typography>
            <Typography variant="body1">
              {p.body}
            </Typography>
        </Card>
      ))}
    </>
}

          

Styling

  • Grundsätzlich sind die Material UI Komponenten "fertig" gestyled
  • Man kann das CSS aber überschreiben, in dem man das class-Property angibt und einen CSS-Klassennamen übergibt
  • Um gezielter einzelne Teile einer Komponente zu verändern, kann man eine Reihe von Komponente-spezifischen CSS-Klassen mit dem classes-Property angeben:
    
    <Input classes={{
      focused: "my-focus-class",
      disabled: "my-disabled-class"
    }} ... />                
                  
  • In diesem Fall müsst ihr dafür sorgen, dass my-focus-class und my-disabled-class in Eurem Code irgendwo definiert sind, z.B. in einem externen CSS-Stylesheet
  • Wichtig! Die Keys im classes-Objekt sind nicht die tatsächlichen CSS-Klassen, die Material-UI verwenden, sondern logische Namen. Diese sind jeweils in der Komponenten API beschrieben

Styling #2: CSS-in-JS

  • Für Material UI spielt es grundsätzlich keine Rolle, wo Euer eigenes CSS herkommt; ihr müsst die Klassennamen nur mit className oder classes angeben.
  • Eine Idee für CSS mit React wird CSS-in-JS genannt. Dabei schreibt ihr den CSS-Code direkt in Eurer JavaScript-Anwendung
  • Das ist nicht Material UI-spezifisch, wird aber von Material UI untersützt
  • Material UI verwendet dazu eine eigene Implementierung, bekannt für diesen Ansatz ist auch Styled Components
  • Aus Euren in JavaScript geschrieben Styles erzeugt die Bibliothek style-Elemente. Die Styles werden also nicht "inline" auf die Elemente angewandt

Styling #3: makeStyles und useStyles-Hook

  • In Material UI könnt ihr Euch eine Hook-Funktion erzeugen lassen, die in Eurer Komponente Referenzen auf den CSS-Code hält
  • Diese Hook-Funktion wird mit der Funktion makeStyles erzeugt. makeStyles übergebt ihr ein Objekt mit CSS-Klassen und deren Eigenschaften
  • Das Besondere daran ist, dass ihr in den CSS-Eigenschaften JavaScript-Code verwenden könnt (z.B. für Variablen oder Berechnungen)

// Eine neue Klasse "MessageBox" definieren
const useStyles = makeStyles({
  MessageBox: { 
    // "reguläre" CSS-Eigenschaft
    marginBottom: "2rem",

    // Wert für CSS basierend auf Komponeten-Properties,
    backgroundColor => props.warning ? "orange" : "gren"
  }
});


function MessageBox(props) {
  const styles = useStyles(props); // Referenz auf die Styles

  // Klasse verwenden (muss keine Material UI-Komponente sein!)
  return <div className={styles.MessageBox}>...f</div>
}
                  

Styling #4: Styled Components-Stil

  • Durch die Bibliothek Styled Components ist ein etwas anderer CSS-in-JS-Stil bekannt geworden
  • Eine ähnliche API bietet auch Material UI als Alternative zur Hook API an.
  • Bei dieser Variante erzeugt man eine neue, gestylte, Komponente aus einer bestehenden.
  • Das eigenet sich besonders für wiederverwendbare Basis-Komponenten

export const LargeInput = styled(Input)({
  fontSize: props => props.xxl ? "2rem" : "1rem"
})
          

// Verwendung wie eine "normale" Komponente
<LargeInput xxl="true" />            
                      

Styling: Zusammenfassung

  • Material UI ist "fertig" gestyled
  • Ihr könnt das Aussehen jeder Komponente mit spezifischen CSS-Klassen verändern
  • Wo das CSS herkommt bleibt Euch überlassen: "klassisches" externes CSS (ggf. mit SASS/LESS) oder CSS-in-JS
  • für CSS-in-JS bringt Material UI eine eigene Bibliothek mit
  • (Diese Bibliothek könnt ihr auch ohne MAterial UI verwenden, falls das relevant ist)

Theming

  • Mit CSS könnt ihr das Aussehen einzelner Material UI Komponenten beeinflussen
  • Mit Theming könnt ihr das "generelle" Design anpassen (Schriftarten, Farben, Abstände etc)
  • Um ein Theme zu erstellen, wird die createMuiTheme-Funktion verwendet
  • Diese Funktion erlaubt es, beliebige Werte aus dem Default-Theme zu überschreiben und anzupassen
  • Das erzeugte Theme wird dann mittels eines ThemeProviders in die Anwendung gereicht:
    
    const blogTheme = createMuiTheme({ ... });
    
    return <App>
      <ThemeProvider theme={blogTheme}>
        ...alle Komponenten hierunter verwenden automatisch das eingestellte Theme...
      </ThemeProvider>
    </App>;  
    }  
    
  • Die Material UI Komponenten verwenden das eingestellt Theme automatisch
  • Eigene Komponenten können mit dem useTheme darauf zugreifen und die Informationen auswerten:
    
    function BlogList() {
       const theme = useTheme();
    
       return <article><h1 styles={{font: theme.typography.fontWeightBold}}>...</h1></article>; 
    }  
    

Fragen?

Habt ihr noch Fragen, zur Material UI Bibliothek, die wir gemeinsam besprechen sollen?

Testen

What do we test?

  • UI-independent logic (for example services, backend calls)
  • Rendering (is the list of blog post correctly rendered? is the markup as we want it?)
  • Interactions (are my event handlers working and correctly set?)
  • Behaviour in the browser (z.B. correct rendering, Browser-spezific JavaScript, Working with browser artefacts, such as history, Title, Scrollbars etc)

What do we test?

  • UI-independent logic (for example services, backend calls)
  • Rendering (is the list of blog post correctly rendered? is the markup as we want it?)
  • Interactions (are my event handlers working and correctly set?)
  • Behaviour in the browser (z.B. correct rendering, Browser-spezific JavaScript, Working with browser artefacts, such as history, Title, Scrollbars etc)

Jest

"Delightful JavaScript Testing" ( https://jestjs.io/)

Complete Test solution for React (and others):

  • Test Runner
  • Specs, Assertions, Mocks
  • Code Coverage
  • "Snapshot testing"

Jest

Example: A simple testcase

// sum.js (or sum.ts)
export const sum = (a,b) => a+b;
        
// sum.test.js
import {sum} from '../sum.js';

test('sum of 2 and 2 is 4', () => {
  expect(sum(2, 2)).toBe(4);
});

test('sum of 2 and 2 is not 3', () => {
  expect(sum(2, 2)).not.toBe(3);
});
            

Jest

Testcases

test or it

test('it should work', () => {
  expect(...).toBe(...);
});
it('it should also work', () => {
  expect(...).toBe(...);
});

Jest

Expectations and Matchers

expect() returns an Expectation obects, that contains several Matcher functions:

expect(actual).toXyz(expected);
// for example:
expect("Hello Jest").toBe("Hello Jest"); // => ok
        

Some Matchers

// Compare identity
expect(actual).toBe(expected);
// Compare value:
expect(actual).toEqual(expected);
// true / false / null:
expect(actual).toBeTruthy();
expect(actual).toBeFalsy();
expect(actual).toBeNull();
// Length (Array oder String)
expect(actual).toHaveLength(123);
        

https://jestjs.io/docs/en/expect

Jest

Mock Functions

  • jest.fn() creates a mock function
    // Returns undefined when executed
    const aMockFn = jest.fn();
    
    aMockFn("huhu"); // => undefined
    
    expect(aMockFn).toHaveBeenCalled());
    expect(aMockFn.toHaveBeenCalledWith("huhu"));
                          
  • Implementation of the mock function can be passed as parameter:
    
            const aMockFn = jest.fn( param => `Hello, ${param}` );
            
            console.log(aMockFn('World'));
            // => Hello, World
                      

Testing React Components

Rendering-only

React Test Renderer

https://www.npmjs.com/package/react-test-renderer

Renders React Components into JSON Objects (without DOM):

import renderer from 'react-test-renderer';

const component = renderer.create(
  <PostList posts={somePosts} />
);

console.log(component.toJSON());
            

        { type: 'div',
          props: {},
          children:  [
            { type: 'button', props: {}, children: [Object] },
            { type: 'article', props: [Object], children: [Object] },
            { type: 'article', props: [Object], children: [Object] }
          ]
        }
            

"Snapshot Testing" with Jest

expect(obj).toMatchSnapshot() compares a JSON-Object with a stored object in a file:

  • At first execution: Snapshot-File will be created ( Example)
    • Snapshot files will be stored in Git
  • In all following test executions: new snapshot will be created and compared against saved snapshot.
  • When Snapshots differ:
    • Error including diff ( Example)
    • In "watch mode" the snapshot can be updated

Snapshot Testing with Jest and React

import renderer from 'react-test-renderer';
        
test('it should render correctly', () => {

  const somePosts = [ . . . ];

  const component = renderer.create(
    <PostList posts={somePosts} />
  );

  expect(component.toJSON()).toMatchSnapshot();
});
            

Testing React Components #2

Interactions

react-testing-library

"Simple and complete React DOM testing utilities that encourage good testing practices."

https://github.com/testing-library/react-testing-library)

Philosophy: tests are written from a user perspective. To find your elements you want to test, you use properties the user also sees (like labels, aria roles, etc)

Should make your app better (enforce accessibility) and your tests more stable (don't rely on internal DOM structure etc)

The same tests can be executed against the "plain CSS version" and "Material UI version" of the Blog Example App!

👉 workspace-typescript

react-testing-library

Example


            import { render, screen } from "@testing-library/react";
            import userEvent from "@testing-library/user-event";

            it("invokes callback on button click", () => {
              const onAddPostFn = jest.fn();
            
              // Render
              render(<PostList onAddPost={onAddPostFn} posts={mockPosts} />);
            
              // search the button
              const buttonElement = screen.getByRole("button", { name: "Add Post" });

              // "click" the button
              userEvent.click(buttonElement);
            
              // make sure it has been invoked
              expect(onAddPostFn).toHaveBeenCalled();
            });


            

render-Function

You can use the render-Function from testing-library/react to render your component in your test (no browser needed)

Just as in real world you can specify properties etc (might be testdata and/or mocks)

The render function returns a set of query-functions that can be used to search for elements within the rendered components (deprecated!, see next slide)


            import { render } from "@testing-library/react";

              // Render a single component
              render(<PostList onAddPost={onAddPostFn} posts={mockPosts} />);
          

Remember when using Components, that needs a "Context"!



              // Render with surrounding Redux Provider (or Router, your own Context etc.)
              render(
                <Provider>
                  <PostList onAddPost={onAddPostFn} posts={mockPosts} />
                </Provider>
              );
            

query Functions

To verify and interact with the elements from your component you need to find them 🤓

On the global screen object there are many functions defined, that allows you searching for an element

The query functions have different suffixes, that describe what they're searching for (a label, a aria-role, ...)

The query functions have different prefixes (getBy, queryBy etc) that describes their outcome (i.e. wether they fail if they don't find the object or if they just return nothing)

More on this: React Testing Library Cheatsheet


  // EXAMPLES: 

  // search the button (assume there is exactly one, fail otherwise)
  const buttonElement = screen.getByRole("button", { name: "Add Post" });

  // search the button (return null or ONE button, throws Exception otherwise)
  const buttonElement = screen.queryByRole("button", { name: "Add Post" });

  // search the button (return all buttons or an empty array)  
  const buttonElement = screen.queryAllByRole("button", { name: "Add Post" });

Verify you components

After quering a component you can verify their attributes etc to make sure they're correct

The library jest-dom adds DOM-specific matchers for jest.


  import { screen } from "@testing-library/react";

  const buttonElement = screen.queryByRole("button", { name: "Add Post" });

  // assert presence of the element (getBy query would maybe make more sense)
  expect(buttonElement).toBeInTheDocument();

  // assert value of input field
  expect(titleInput).toHaveValue("Moin moin");

  // assert enablement of a button
  expect(okButton).toBeDisabled();

All matchers are described on the GitHub page (Link see above)

Interaction

You can interact with the elements from a component by sending events, as the browser would do

There's a special library user-event, that helps you constructing this events.

(In the next release of react-testing-library it will be included)


            import userEvent from "@testing-library/user-event";
            // type somthing into an input field
            // (with another parameter you can configure how fast typing speed should be etc)
            userEvent.type(titleInput, "New Title");

            // click a button
            userEvent.click(clearButton);
          

All events can be found on the GitHub page (Link see above)

Note that you don't have to test if an element exists before sending an event; the test will fail with a readable error message, if the element is not available

Testing with fetch

Challenges:

  • Asynchronous code (not limited to fetch)
  • Server mocking

Dealing with asynchronous code

Testing async code

Example: The App components loads data, than it displays the post list. Loading of the data is async code


            it("should render posts read from backend", () => {
              render(<App />);
            
              // imagine there is an article with title "One"
              const articleOne = screen.findByRole("heading", { name: "One" });

              expect(articleOne).toBeInTheDocument(); // THIS WON'T WORK!
            });
          

Good news: we can write an async test and waitfor the element until it's loaded


            // not the async function here:
            it("should render posts read from backend", async () => {
              render(<App />);
            
              // note the await statement here:
              const articleOne = await screen.findByRole("heading", { name: "One" });

              expect(articleOne).toBeInTheDocument(); // YIPPIE, WORKS
            });
          

Testing fetch

As seen, we're able to test async code

Do we want to have fetch calls in our code?

Propably not, because we don't want to depend on a running (and working) server in test

We want to "mock out" fetch

Mocking fetch #1

We can mock whole modules in jest

We could move our fetch code to an own module an mock that


            // api.ts (simplified!)
            export function readPosts() {
              return fetch("http://localhost:7000/posts").then(response => response.json());
            }
          

            // App.tsx:

            import { readPosts } from "./api";
            function App() {
              // ...
              React.useEffect(() => {
                // use readPosts here:
                readPosts()
                  .then(json => {
                    setLoading(false);
                    setPosts(json);
                  })
                  .catch(err => console.error("Loading data failed: " + err));
              }, []);
            }
          

Mocking a Module

Several ways, this is the simplest one(?)


            // App.test.tsx

            const mockPosts = [
              { id: "1", title: "One Fetch Mock", body: "Lorem ipsum" },
              { id: "2", title: "Second Post Fetch Mock", body: "Some more content" }
            ];

            jest.mock("../api", () => ({
              readPosts: () => Promise.resolve(mockPosts)
            }));

            it("should render posts read from backend", async () => {
              render(<App />);
            
              // we still need to await, but we don't care about
              // running server anymore
              const articleOne = await screen.findByRole("heading", { name: "One" });
              expect(articleOne).toBeInTheDocument();

              expect(screen.getByText("Second Post")).toBeInTheDocument();
            });
          

Example: blog-example/steps/6a-testing-with-api-module/src/__tests__/App_module_mock.test.tsx

Option #2: Mocking fetch

There are several libs that allows you to mock fetch

For me jest-fetch-mock works best


            // App.test.tsx
            it("should render posts read from backend", async () => {
              // set the expected response (there are more ways to customize the fetch mock behaviour)
              fetchMock.mockResponse(JSON.stringify(mockPosts));

              render(<App />);
            
              // we're still async, but no "real" fetch anymore
              const articleOne = await screen.findByRole("heading", { name: "One Fetch Mock" });

              expect(articleOne).toBeInTheDocument();
            });

          

Example: blog-example/steps/6a-testing-with-api-module/src/__tests__/App_jest_mock.test.tsx

Option #3: Mock Service Worker (MSW)

MSW

Mocks real backend calls using a Service Worker

fetch-Calls are executed but responded by the service worker mock


            // PostList.test.js
            import { rest } from 'msw'
            import { setupServer } from 'msw/node'
            
            const server = setupServer(
              rest.get('/posts', (req, res, ctx) => {
                return res(ctx.json( mockPosts ))
              }),
            )

            test("it should render blog list", async () => {
              render(<PostList />);
              // ...
            })
          

Übung: Ein Test für den PostEditor

Schritte:

  1. Arbeite in blog-example/workspace-typescript
  2. Starte in einem neuen Terminal npm test (der Prozess läuft "ewig")
  3. Es sollte einen fehlerhaften Test geben: PostEditor.test.tsx.
  4. Warum? Weil der noch leer ist 😬!
  5. Implementiere den fehlenden Test. In der DateiPostEditor.test.tsx stehen TODOs dafür

Hinweis: wenn Du die Datei speicherst, werden die Tests automatisch neu ausgeführt (solange Du npm test noch laufen hast), ansonsten kannst Du "a" auf der Kommandozeile drücken, zum erneuten Ausführen der Tests.

  • Eine mögliche Lösung für diese Aufgabe findest Du in blog-example/steps/6-tests

Browser Tests

Often used:

Testcafe

  • Tests werden in JavaScript bzw. TypeScript geschrieben
  • Out-of-the-box-Support für TypeScript
  • Eigene Test API (baut nicht auf bekannten APIs wie Jest auf 😢)
  • Es gibt eine testing-library API für Testcafe
  • Support für diverse Browser, einschliesslich BrowserStack
  • Tests lassen sich auf Remote-Browsern ausführen, zum Beispiel auf dem Smartphone
  • Tests lassen sich "headless" im CI-Build (Jenkins, Docker etc) ausführen
  • Ausführen: testcafe firefox,chrome,safari test-cafe-tests/*.ts

Code Beispiel

👉 Beispiel: steps/5-tests-with-material-ui/testcafe/app-test.ts


            import { Selector } from "testcafe";

            fixture`App Browser Test`.page`http://localhost:3000`;
            
            test("Entering new Blog Post", async t => {
              const titleField = Selector("#titleField");
              const bodyField = Selector("#bodyField");
              const saveButton = Selector("#saveButton");
            
              await t.typeText(titleField, "Learning React");
              await t.expect(titleField.value).eql("Learning React");

              await t.click(saveButton);
            });
                        
          

Test Cafe tests

  • Mit der test-Funktion werden Tests geschrieben (nicht verwechseln mit Jest...)
  • Eine Test-Funktion muss eine async-Funktion sein, denn alle Interaktionen mit dem Browser erfolgen asynchron
  • Der Test-Funktion wird ein Controller-Objekt übergeben...

Controller-Objekt

  • Das Test-Controller-Objekt ist die zentrale API von TestCafe, um mit dem Browser zu interagieren
  • Tests bekommen das Objekt automatisch als Parameter übergeben
  • Außerhalb einer Test-Funktion kann das Objekt aus dem testcafe-Modul importiert werden: import { t } from "testcafe"
  • Das Controller-Objekt kann den Browser steuern (z.B. click) oder auch Assertions ausführen (expect)
  • Über das Controller-Objekt gibt es auch Zugriff auf native Browser-Elemente wie die Browser Console

Selektoren

  • Um mit einem Element auf einer Seite zu interagieren, muss ein Selektor geschrieben werden
  • Es gibt mehrere Arten von Selektoren, der klassische ist ein CSS-Selektor
  • In vielen Stellen der API kann entweder direkt ein Selektor als String geschrieben werden, oder es wird eine Selector-Instanz erzeugt:
    
        const titleField = Selector("#titleField");
        const thirdArticle = Selector("article").nth(2);    
        const thirdArticle = Selector(".btn").withText("Save");    
      
  • Für React-Anwendung gibt es spezialisierte React Selektoren, die es erlauben, nach Komponenten zu suchen:
    
                    import { ReactSelector } from 'testcafe-react-selectors';
                                
                    const buttons = ReactSelector('PostEditor Button');
                                  
  • Damit kann dann auf Komponenten-Informationen zugegriffen werden, z.B. auf Properties und State einer Komponente
  • "Gefahr": testet man noch Black-Box? 🤔

Tests

  • Auf dem Test-Controller ist die expect-Funktion definiert, die einen Wert nimmt und ein Assertion-Objekt zurückliefert
  • Auf dem Assertion-Objekt sind Prüf-Funktionen definiert, z.B. eql, notEql
    
    await t.expect(titleField.exists).ok();
    await t.expect(titleField.value).eql("Hello World");
    await t.expect("article".count).gt(3);              
                
  • Achtung! Die Selektoren werden erst ausgewertet, wenn sie verwendet werden, nicht bei ihrer Deklaration!
    
    // Hier wird der Selektor nur deklariert
    const inputField = Selector("input").nth(2);
    
    // Hier wird nun der Selektor ausgeführt
    await t.expect(inputField.getAttribute("disabled")).notOk();
                  
  • Aus diesem Grund muss auf die expect-Aufrufe mit await gewartet werden
  • Bei der Suche nach Elementen verwendet TestCafe "intelligente" Timeouts, um z.B. mit Asynchronität (Blog-Posts laden!) umzugehen

Interaktion

  • Auf dem Test-Controller sind Funktionen zur Interaktion mit Elementen definiert
  • Auch diese Funktionen erwarten einen Selektor, um ein Element eindeutig auszuwählen
  • Beispiel:
    
                    await t.typeText(titleField, "Learning Testcafe");
                    await t.click(saveButton);
                  
  • Diese Aufrufe können auch verkettet werden:
    
        await t.typeText(titleField, "Learning Testcafe")
              .expect(saveButton.getAttribute("disabled").notOk()
              .click(saveButton);
    

Selektoren mit testcafe-testing-library

Beispiel: steps/6-tests-with-material-ui/testcafe/app-test-rtl.ts

  • Die Query-Funktionen aus der dom-testing-library lassen sich mit dem Modul testcafe-testing-library auch mit Testcafe nutzen!
  • Es werden nicht alle Query-Funktionen unterstützt, bzw. sie unterscheiden sich semantisch etwas vom Testen in JSDom (siehe Doku)
  • Damit kann man auch mit dem Material-UI-DOM gut z.B. über aria-Attribute suchen
    
    // Exakt derselbe Code wie beim Test mit react-testing-library!
    const clearButton = screen.getByRole("button", { name: "Clear" });
    const saveButton = screen.getByRole("button", { name: "Save Post" });
    
    const titleField = screen.getByLabelText("Title");
    const bodyField = screen.getByLabelText("Body");                
    
    
    // Exakt derselbe Code aus dem Test-Cafe Beispiel (nur andere Selektoren...)
    await t.typeText("#bodyField", "Lorem ipsum tralala");
    await t.typeText(titleField, "Learning Testcafe");
    await t.typeText(bodyField,  "Testcafe - Browser testing tool");
    
    await t.expect(clearButton.hasAttribute("disabled")).notOk();
    await t.expect(saveButton.hasAttribute("disabled")).notOk();
    await t.click(saveButton);
                  

Arbeiten mit TestCafe

Um die Entwicklung von Tests zu vereinfachen und Fehler zu finden, gibt es unter anderem folgende Features:

  • Bei einem fehlgeschlagenden Test kann TestCafe automatisch einen Screenshot erzeugen oder per API einen Screenshot erzeugen:
    
      await t.typeText(titleField, "Hello")
        .screenshot()
        .click(saveButton);
    
  • Man kann grundsätzlich Testcafe im Debug-Modus starten, oder per API an einer beliebigen Stelle den Debugger starten:
    
    await t.debug();                
                  
  • Im Live-Modus bleibt der Test-Browser durchgehend gestartet, Tests werden automatisch bei Datei-Änderungen ausgeführt

Teil VIII

Zustands-Management mit Context API, Redux und MobX

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?

Context API

Beispiele:

  • Blog App mit Hooks: blog-example/steps/30-complete-app-with-hooks
  • Context API "Hello World" (Counter Example): advanced/steps/4a-context-hello-world

Context Api Live Introduction

(advanced/exercise/4_context)

Context...

erlaubt das Durchreichen von Informationen ohne explizites angeben als Properties

  • funktioniert nur innerhalb einer Hierarchie-Ebene
  • es können beliebg viele (fachliche) Context definiert werden
  • besteht aus Provider und Consumer
  • Doku

Context Factory

erzeugt ein Objekt, mit zwei Komponenten

  • Provider, stellt Objekt mit Key-Value-Paaren zur Verfügung (der Context-"Value")
  • Consumer wird in eigener Komponente verwendet, um auf einen Context zuzugreifen ("versteckt" durch useContext Hook)

                      import react from "React";

                      const AuthContext = React.createContext();

                      // erzeugt:
                      // AuthContext.Provider 
                      // AuthContext.Consumer
                              

Context Provider

Eine React-Komponente, die einen Context zur Verfüng stellt

  • wird innerhalb einer eigenen Komponente in render eingebunden
  • Nimmt ein Objekt ("Context") mit beliebigen Werten entgegen

                      const AuthContext = React.createContext();

                      function AuthProvider() {
                          const [ currentUser, setCurrentUser ] = React.useState(null);
                          const contextValue = {
                            // the current theme
                            currentUser,
                      
                            // function to set new theme
                            login: name => setCurrentUser(name),
                            logout: () => setCurrentUser(null)
                          };
                      
                          return <AuthContext.Provider value={contextValue}>
                            {props.children}
                          </AuthContext.Provider>;
                        }
                      }                    

useContext-Hook

Zugriff auf die Werte aus dem Context

In allen Komponenten unterhalb der Provider Komponente, kann mit useContext auf den Kontext zugegriffen werden


function UserBadge() {
  const { currentUser } = React.useContext(AuthContext);

  return currentUser  ? <h1>Welcome, {currentUser}<h1> : null;
}            
          

Aufrufen einer Funktion aus dem Context

Ändert im Context den Zustand der Provider-Komponente

Alle Konsumer werden neu gerendert


function UserBadge() {
  const { currentUser, logout } = React.useContext(AuthContext);

  return currentUser  ? 
    <><h1>Welcome, {currentUser}<h1><button onClick={logut}>Logout</button></> 
    : null;
}                
          

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

Hello World-Redux (PostEditor mit Redux)

  • Live Coding/Übung: advanced/workspace-redux
  • Lösung: advanced/steps/5a-redux-hello-world

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 loadPost = postId => dispatch => {
fetch(BACKEND_URL+'/'+postId)
    .then(response => response.json())
    .then(post => dispatch({
        type: ADD_POST,
        post
    });
};
  • 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}>
    <App />
</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({
posts, // Blog Posts
auth   // Aktueller Benutzer
});
    
  • 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 auth = (state = {}, action) => {
switch (action.type) {
    case "LOGIN":
        return { currentUser: action.username };
    case "LOGOUT":
        return {};
    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 App() {
  const currentUser = useSelector(state => state.auth.currentUser);
  ...
}

useDispatch: Actions auslösen


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

        async function login(newUser) {
          await dispatch(actions.login(newUser));
        }
        ...
      }
    

Zusammenfassung Redux-Architektur

Redux Toolkit

"The official, opinionated, batteries-included toolset for efficient Redux development"

Local state is still "allowed"!

There are a lot of places where you can hold state

  • Server and client
  • Component State
  • Redux
  • URL, Session or Local Storage

Redux vs Context

Redux hat Dev Tools, Time Travelling, Middlewares, globale Actions und Reducers/State

Redux ist sehr optimiert für Performance (häufige Updates)

Context von der API her einfacher (aber auch nicht so mächtig)

Redux lässt feingranularere Auswahl aus dem globalen Zustand zu (verhindert unnötige Renderings)

Redux vs Context

Wie entscheiden wir uns?

Entscheidung 1: Mischform: Redux und Context? Oder: Redux oder Context

Entscheidung 2: Was kommt wohin?

Auth-State (eingeloggter Benutzer)

Api-State: Netzwerk Request(s) laufen gerade

Draft-Post: editierters, neues, Post

MobX

External Statemanagement

MobX

  • https://mobx.js.org/README.html
  • Claim: Simple, scalable state management.
  • Aktuelle Version und Library für React: mobx-react-lite
  • Achtung #1: mobx-react braucht ihr nicht (wenn ihr Hooks API verwendet)
  • Achtung #2: Doku teilweise veraltet, die nicht direkt auf mobx.js.org liegt!
  • Basiert auf der Idee von "observable State", der von Euren Komponenten "observiert" wird

MobX Schritt-für-Schritt

Hands-On 👉 advanced/exercices/7a-mobx

Fertig 👉 advanced/steps/7a-mobx

MobX, Beispiel #1

  • Der Zustand in MobX wird entweder in JavaScript-Objekten oder ES6-Klassen gehalten
  • Ihr könnt beides verwenden, ES6-Klassen weiter verbreitet und in der Doku auch empfohlen
  • Eine Klasse müsst ihr zum "Observer" machen

class BlogAppState = {
  blogPosts:Array<BlogPost> = [];

  constructor() {
    // Diese Instanz "observieren", MobX verfolgt jetzt alle Änderungen
    makeAutoObservable(this);
  }
}        

// Unser globaler Zustand
const blogAppState = new blogAppState();  
        
  • Eine Komponente kann zum Observer werden, und rendert sich dann bei allen Änderungen an der beobachteten Klasse:

const PostList = observer(function PostList() {
  return <div>
    {blogAppState.blogPosts.map( p => /* wie gewohnt */ )}
  </div>  
})
        

MobX: Actions

  • Der Zustand in MobX wird über Actions verändert. Dabei handelt es sich um Methoden an einer Klasse.
  • Wenn der Zustand außerhalb einer Action verändert wird, gibt es einen Fehler. Auf diese Weise erzwingt MobX eine gewisse Architektur
  • Die Action-Methoden werden per Konvention von makeAutoObservable erkannt, oder ihr gebt sie explizit an
  • Das Laden von Daten könnte z.B. eine Action sein:

class BlogAppState = {
  blogPosts:Array<BlogPost> = [];
            
  constructor() { makeAutoObservable(this); }

  async loadPosts() {
    const await response = fetch("...");
    const posts = await response.json();
    this.blogPosts = posts;
  }
}
          

// Auslösen:
function App() {
  React.useEffect(  () => { blogState.loadPosts() }, []);

  // ...
}            
          

Berechnete Werte: Computed Properties

  • MobX kann berechnete Werte cachen. Berechnete Werte basieren auf observierten Daten.
  • Beispiel: Auf Basis der Blog-Post-Liste (beobachtet) und eines Sortierkriteriums (beobachtet) wird eine sortierte Blog-Post-Liste erzeugt. Die sortierte Liste ist ein "berechneter" Wert.
  • Der Wert wird dann nur neuberechnet, wenn sich einer der darin verwendeten Werte ändert (Liste oder Kriterium).
  • Ein Computed Property ist in der Regel eine getter-Methode an einer Klasse

Berechnete Werte #2

Beispiel


class BlogAppState = {
  blogPosts:Array<BlogPost> = [];
  orderBy:string = "";
  currentUser: string = "";
            
  constructor() { makeAutoObservable(this); }

  get orderedPosts loadPosts() {
    const posts = [...blogPosts];
    posts.sort(orderBy); // vereinfacht

    return posts;
  }
}              
            

blogAppState.orderedPosts; // Liste wird initial berechnet
blogAppState.orderedPosts; // Liste wird NICHT neu berechnet (Cache!)

blogAppState.orderBy = "date";              
blogAppState.orderedPosts; // Liste wird neu berechnet

blogAppState.currentUser = "Klaus";
blogAppState.orderedPosts; // Liste wird NICHT berechnet
            

Observed Components

  • Eine Komponente, die mit dem State arbeitet, muss ein Observer werden
  • Dazu wird sie der observe-Funktion übergeben, die eine eine observed Komponente zurückliefert
  • Das ist für den Aufrufer vollständig transparent
  • Die observed Komponente wird immer dann neu gerendert, wenn sich einer der Werte verändert, auf die sie zugreift
  • Die observed Komponente rendert sich daher auch nicht neu, wenn ihre Parent-Komponente sich neu rendert
  • Ausnahme: die Parent-Kompoenten übergibt veränderte Properties

            const PostList = observer(function PostList() {
              return <div>
                {blogAppState.blogPosts.map( p => /* wie gewohnt */ )}
              </div>  
            })  
          

Bereitstellen des Zustands

  • MobX ist es egal, wo der Zustand herkommt; es ist ein beliebiges Observable
  • Typischerweise wird der Zustand in React-Anwendungen über genau einen Context in die Anwendung gereicht
  • Das Context-Objekt wird sich nie ändern, da sich nur die Werte in der Klasse verändern.
  • Deswegen wird eine konsumierende Komponente auch nie neu gerendert, weil sich der Context nicht ändert
  • Deswegen muss die Komponente ein Observer sein; wenn sich ein Wert aus dem Context ändert, rendert MobX neu

Bereitstellen des Zustands - Beispiel


const context = {
  blog: new BlogAppState(),
  auth: new AuthenticationState()
}
function StoreProvider({ children }) {
  return (
    <StoreContext.Provider value={context}>
      {children}
    </StoreContext.Provider>
  );
}

export function useStore() {
  return React.useContext(StoreContext);
}
          

const PostList = observe(function PostList() {
  const { blog } = useStore();

  // PostList wird nur neu gerendert, wenn sich BlogPost-Liste ändert
  return blog.posts.map(<BlogPost id={...});
})           
          

MobX vs. Redux

  • Redux eher funktionale Programmierung, MobX eher OO
  • Viel Freiheiten mit MobX...
  • ...aber auch viel "Magie"
  • In großen Modellen manchmal schwer zu verstehen, was eigentlich wo und wie "observed" wird
  • Observer-Komponenten eher untypisch für React (noch)
  • Developer Tooling nicht so gut wie Redux
  • Doku, StackOverflow etc. deutlich weniger umfänglich als Redux
  • 👉 Beide Bibliotheken gute Wahl und können verwendet werden
  • 👉 "Für Redux ist noch niemand entlassen worden"

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