(wenn noch Zeit ist, sonst später)
https://nilshartmann.net / Twitter: @nilshartmann
Freiberuflicher Software-Entwickler, Berater und Trainer aus Hamburg
Schritt 1: Klonen und installieren
git clone https://github.com/nilshartmann/react-training/cd react-training/blog-example/workspacenpm installnpm start
            Schritt 2: Starten
cd react-training/blog-example/backend-restnpm start
            cd react-training/blog-example/workspacenpm start
            Jederzeit: Fragen und Diskussionen!
          
        Zentrales Konzept in React: Komponenten
        Unser Beispiel in Komponenten
        Mehr Hintergründe zu Hooks: Ein Jahr React Hooks-API (heise Developer)
blog-example/workspace)PostEditor.js
          
  import React from "react";
  
  export default function PostEditor(props) {
    const [title, setTitle] = React.useState("");
  
    return (
      <div>
        <label>
          Title
          <input onChange={event => setTitle(event.target.value)} value={title} />
        </label>
      </div>
    );
  }
            
        
              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')
  );
  
          Untersuchen einer laufenden React Anwendung

Bootstrap von neuen React Anwendung
Fertige Konfiguration von React und Webpack mit TypeScript, Sass, Linter
Beispiel: npx create-react-app --template typescript
2020_ct_webdev.html: Die Slidesblog-example Verzeichnis
            blog-example/workspace: Verzeichnis für Eure Übungen blog-example/material: Code für einige der Übungen
            blog-example/steps: Fertiger Source-Code nach jeder Übung
            cd blog-example/workspace
            npm install
            npm start
            PostEditor.js mit dem React Code aus
              den vorherigen Slides.
            title) und ein Eingabefeld dafür haben
            
<div><input type="text"/></div>
                    
            class-Attribut heißt className:
              
                        <h1 className="title">...</h1>
                    
            
<Counter label="Count" count={7} showValues={true} />
                    
            
<h1>{title ? title.toUpperCase() : "New document"}</h1>
                    
            
const styles = { marginLeft: '10px', border: '1px solid red' };
<h1 style={styles}>...</h1>
                    
            
function Choice() { 
  return <>
    <li>Yes</li> 
    <li>No</li>
  </>              
}  
                    
            
function ErrorMessage(props) {
  if (!props.msg) {
    return null; // oder false oder true
  }
  return Fehler: {props.msg}
;
}              
                    
            
  function MyComponent() {
    return 
      { /* hier ist javascript, deswegen block-kommentare erlaubt */ }
    ;
  }              
                      
            
            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>
    );
  }
}
                
        
function HelloWorld(props) {
  const [title, setTitle] = React.useState(props.initialTitle);
  return <input onChange={e => setTitle(e.target.value) value={title} />;
}
                  
          use beginnen (useState,
              useEffect, ...)
            
                import React from "react";
                function HelloWorld(props) {
                  const [title, setTitle] = React.useState(props.initialTitle);
                  // ...
                }
                                  
              
                                      import React, { useState } from "react";
                      
                                      function HelloWorld(props) {
                                        const [title, setTitle] = useState(props.initialTitle);
                                        // ...
                                      }
                                                        
              React muss immer importiert werden, wenn JSX verwendet wird!)
                (https://reactjs.org/docs/hooks-rules.html)
Einschränkungen:
            Der Hooks-Mechanismus basiert intern darauf, dass React sich die Reihenfolge der
              useXyz-Aufrufe merkt!
          
              // ERLAUBT:
              function HelloWorld(props) {
                const [greeting, setGreeting] = React.useState(props.initialGreeting);
                const [name, setName] = React.useState(props.initialName);
                // ...
              }
                                
          
                                    // ERLAUBT:
                                    function HelloWorld(props) {
                                      const [greeting, setGreeting] = React.useState(props.initialGreeting);
                                      const uppercaseGreeting = greeting.toUpperCase(); 
                                      const [name, setName] = React.useState(props.initialName);
                                      // ...
                                    }
                                                      
          
                                    // VERBOTEN:
                                    function HelloWorld(props) {
                                      const [greeting, setGreeting] = React.useState(props.initialGreeting);
                                      if (greeting !== null) {
                                        const [name, setName] = React.useState(props.initialName);
                                      }
                                      // ...
                                    }
                                                      
          
                          // VERBOTEN:
                          function HelloWorld(props) {
                            const [title, setTitle] = React.useState(props.initialTitle);
                            if (title === null) {
                              return Please enter title first
;
                            }
                            const [body, setBody] = React.useState("");
                            // ...
                          }
                                            
        
              // VERBOTEN 
              function HelloWorld(props) {
                function initState() {
                  return React.useState(props.initialGreeting);
                }
                const [greeting, setGreeting] = initState();
              }
            
          
                // VERBOTEN (initState ist 'normale' Funktion)
                function initState() {
                  return React.useState(props.initialGreeting);
                }
                function HelloWorld(props) {
                  // wäre erlaubt, wenn initState 'useInitState' hieße
                  const [greeting, setGreeting] = initState();
                }
              
        
        "Rendern" hat leider doppelte Bedeutung!
        body.
            onClick.
              disabled Property auf true setzen.
                blog-example/steps/3-hierarchy
        JSX hat keine eigenen Konstrukte für Listen
            Üblicherweise verwendet man
            Array.map() um eine Liste von Objekten in eine Liste von JSX Elementen zu
            überführen
          
Jedes JSX Element in der Liste benötigt einen List-weit eindeutigen key
const posts = [
  { id: 0, title: 'Hello World', body: 'Lorem ipsum' },
  { id: 1, title: 'React in a Nutshell', body: 'Lets get started with React' }
];
function PostList(props) {
  return props.posts.map(post => (
    <div key={post.id}>
        <h1>{post.title}</h1>
        <p>{post.body}</p>
    </div>
  ))
}
        Wir haben zwei Views: Blog-List und Post-Editor
Welche ist sichtbar?
Wie fließen die Daten zwischen den beiden Komponenten?
(Eine Komponente kann die Properties oder einen Teil davon ihrerseits weiter nach "unten" reichen)
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-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!
          
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} />
}            
        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>;
  }            
  
        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>;
  }            
  
        
            
              Integriere deine bestehende PostEditor-Komponente und die neue
              PostList Komponente über die App-Komponente
          
blog-example/material/3-hierarchy/src in deinen
              source Ordner
              (Du kannst deinen eigenen PostEditor verwenden oder den aus
                material/3-hierarchy)
            App Komponente, so dass sie den
              PostEditor anzeigt, wenn der User auf den Add Button klickt.
              - In App.js stehen TODOs mit weiteren Infos
PostEditor benötigst Du einen Save Button, der die übergebene
              Callback-Funktion aufruft, die von der App als Property
              (onSave) übergeben wird. blog-example/material/3-hierarchy/src/PostEditor.js
            blog-example/steps/4-remote
Lesen von Daten mit HTTP GET
// wenn keine weiteren Parameter gesetzt sind, wird ein GET Request ausgeführt            
fetch('http://localhost:7000/posts')
  .then(response => response.json())
  .then(json => /* ... */)
  .catch(ex => console.error('request failed', ex));
          
          // Oder mit async/await: 
          try {
            const response = await fetch('http://localhost:7000/posts')
            const json = await response.json();
            // ...
          } (catch ex) {
            console.error('request failed', ex)
          }
          
        Schreiben von Daten mit HTTP POST
fetch erwartet als 2. Parameter ein Konfigurationsobjekt:
method: HTTP Methode (PUT, POST,
                DELETE, ...)
              headers: HTTP Header für den Request (z.B. Authorization)body: Der Payload (als)Der Returnwert ist derselbe wie bei Get
const response = await fetch(url, {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(payload)
})
// ... 
    
        
// Ein Promise (z.B. als Rückgabewert aus einer Funktion)
const promise = ...;
  
          // 'then' gibt IMMER ein Promise zurück
const promise2 = promise.then(name => `Hello, ${name}`);
          promise2.then(greeting => console.log(greeting));
// Ausgabe nach einer Sekunde: "Hello, Klaus"
  
        catch() kann man den Fehler fangen und darauf reagieren
  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
    
        
  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);
  }
  
  
          
        👉Schritt-für-Schritt
steps/3-hierarchy
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
Server-Aufrufe sind Seiteneffekte (andere Beispiele: DOM manipulieren, WebSocket öffnen)
Seiteneffekte sind in der Renderphase einer Komponente verboten!
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")
              );
            }
          
          
              function App(props) {
                React.useEffect( 
                  () => console.log("I will run only once after 1st rendering"),
                  []
                );
              }
            
          
                function App(props) {
                  React.useEffect(
                    () => console.log("..."), 
                    [props.postId]) 
                  );
                }
              
          
              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} />
                  ))}
                }
                
                
          Als Folge einer Benutzerinteraktion:
In einem Event-Handler können Seiteneffekte verwendet werden!
function App(props) {
  // Laden der Daten, wie zuvor gesehen
  React.useEffect( ... );
  function savePost(post) {
    fetch("http://localhost:7000/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(post)
    })
      .then(response => response.json())
      .then(newPost => setPosts([newPost, ...posts]));
  }
  return
    ...
      <PostEditor onSave={newPost => savePost(newPost)} />
    ...
}
                
        
            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
App.js-Datei aus
              blog-example/material/4-remote/App.js in deinen src-Folder.
            Example: code/blog-example/steps/6-typescript
TypeScript is a superset of JavaScript that compiles to plain JavaScript ( http://www.typescriptlang.org/)
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'.
  
        
  // 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 { /* ... */ }
              
        
  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!
  
  
  
        
            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 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'.
              
          
            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 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'.
            }
                           
        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
    
          
        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
  }
}
            
        
            Achtung! TypeScript-Dateien, die JSX-Code enthalten, müssen mit
            .tsx enden!
          
            👉 Lasst uns ausprobieren, wie das funktioniert! (workspace-typescript)
          
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) => {
      // ...
    }
    
            
        Code Completion
          Unbekanntes Property
          Fehlerhafte Verwendung eines Properties
          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
}
                            
          
            Events in React sind Instanzen von React.SyntheticEvent, die die nativen
            DOM Events wrappen (und so "ähnlich" aussehen)
          
Der Typ-Parameter für ein Event muss auf den Typ des HTML Elements gesetzt werden, dass das Event auslöst
TypeScript kennt dann die Eigenschaften des Events bei der Verarbeitung
function PostEditor(props) {
  const [title, setTitle] = React.useState("");
  function handleChange(e: React.SyntheticEvent<HTMLInputElement>) {
    setTitle(e.currentTaget.value);
  }
  return <input onChange={handleChange} value={title} />
}              
          
            
              target vs
              currentTarget
            
          
        WORKSPACE:
                Bitte benutze den Workspace blog-example/workspace-typescript. 
Dieser enthält die letzte Version unserer Anwendung, ist aber mit
                TypeScript konfiguriert.
              
VORBEREITUNG:
npm install in
                  blog-example/workspace-typescript aus
                npm start in
                  blog-example/workspace-typescript aus
                blog-example/workspace-typescript aus
            PostList.js und
              PostEditor.js hinzu
            .tsx and starte "npm
              run" ggf. neu (manchmal verhakt sich Webpack beim Umbennen von Dateien).
            Wenn ihr noch Fragen habt, könnt ihr mich erreichen:
Mail: nils@nilshartmann.net
Twitter: @nilshartmann