(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/workspace
npm install
npm start
Schritt 2: Starten
cd react-training/blog-example/backend-rest
npm start
cd react-training/blog-example/workspace
npm 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 type
kö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