git clone https://github.com/nilshartmann/react-training
cd react-training
npm install
Slides: react-training/2019_react_grundlagen.html
oder: https://nils.buzz/react-grundlagen
Workspace
2019_react_grundlagen.html
Slides im Root-Verzeichnis
code/workspace
: Verzeichnis für Eure Übungencode/material
: Code als Ausgangspunkt für Übungen
code/schritte
: Fertige Stände nach den einzelnen Teilen
https://nilshartmann.net / @nilshartmann
Freiberuflicher Entwickler, Architekt, Trainer aus Hamburg
Java
JavaScript, TypeScript
React
Single-Page-Anwendungen
GraphQL
Jederzeit: Fragen und Diskussionen!
ist sehr stabil (16.x seit September 2017!)
Zentrales Konzept in React: Komponenten
Unser Beispiel in Komponenten
code/workspace-live-coding
)
import React from "react";
export default function HelloMessage(props) {
const [name, setName] = React.useState(props.initialGreeting || "");
return (
<div>
<input onChange={event => setName(event.target.value)} value={name} />
<p>{name}, World</p>
</div>
);
}
index.html
<html>
<body>
</body>
<script src="dist/main.js"></script>
</html>
main.js
import React from 'react';
import ReactDOM from 'react-dom';
import HelloMessage from './HelloMessage';
const mountNode = document.getElementById('mount');
ReactDOM.render(<HelloMessage initialGreeting="Hello"/>, mountNode);
Bootstrap von neuen React Anwendung
Fertige Konfiguration von React und Webpack mit TypeScript, Sass, Linter
Beispiel: npx create-react-app PROJEKTNAME --typescript
Untersuchen der React Anwendung zur Laufzeit
Workspace
2019_react_grundlagen.html
Slides im Root-Verzeichnis
code/workspace
: Verzeichnis für Eure Übungencode/material
: Code als Ausgangspunkt für Übungen
code/schritte
: Fertige Stände nach den einzelnen Teilen
cd code/workspace
npm start
<div><input type="text"/></div>
class
-Attribut heißt className
:
<h1 className="title">...</h1>
<Counter label="Count" count={7} showValues={true} />
const title = 'Hello, World';
<h1>{title.toUpperCase()}</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 GreetingDetail(props) {
const [greeting, setGreeting] = React.useState(props.initialGreeting);
return <input onChange={e => setGreeting(e.target.value) value={greeting} />;
}
use
beginnen (useState, useEffect,
...)
import React from "react";
function GreetingDetail(props) {
const [greeting, setGreeting] = React.useState(props.initialGreeting);
// ...
}
import React, { useState } from "react";
function GreetingDetail(props) {
const [greeting, setGreeting] = useState(props.initialGreeting);
// ...
}
React
muss immer importiert werden, wenn JSX verwendet wird!)
(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 GreetingDetail(props) {
const [greeting, setGreeting] = React.useState(props.initialGreeting);
const [name, setName] = React.useState(props.initialName);
// ...
}
// ERLAUBT:
function GreetingDetail(props) {
const [greeting, setGreeting] = React.useState(props.initialGreeting);
const uppercaseGreeting = greeting.toUpperCase();
const [name, setName] = React.useState(props.initialName);
// ...
}
// VERBOTEN:
function GreetingDetail(props) {
const [greeting, setGreeting] = React.useState(props.initialGreeting);
if (greeting !== null) {
const [name, setName] = React.useState(props.initialName);
}
// ...
}
// VERBOTEN:
function GreetingDetail(props) {
const [greeting, setGreeting] = React.useState(props.initialGreeting);
if (greeting === null) {
return Please enter greeting first
;
}
// ...
}
// VERBOTEN
function GreetingDetail(props) {
function initState() {
return React.useState(props.initialGreeting);
}
const [greeting, setGreeting] = initState();
// ...
}
// VERBOTEN (initState ist 'normale' Funktion)
function initState() {
return React.useState(props.initialGreeting);
}
function GreetingDetail(props) {
// wäre erlaubt, wenn initState 'useInitState' hieße
const [greeting, setGreeting] = initState();
// ...
}
"Rendern" hat leider doppelte Bedeutung!
greeting
heißtinitialGreeting
und
initialName
)
onClick
-Event)
JSX bietet nichts für Listen
Ausgabe typischerweise über
Array.map()
Elemente einer Liste brauchen einen eindeutigen Key
const greetings = [
{ id: 0, name: 'Klaus', greeting: 'Hallo' },
{ id: 1, name: 'Susi', greeting: 'Moin' }
];
const GreetingsTable(props) => (
<table>
{props.greetings.map(greeting =>
<tr key={greeting.id}>
<td>{greeting.name}</td>
<td>{greeting.greeting}</td>
</tr>
)}
</table>
);
#1: Welche Komponente soll angezeigt werden (Master oder Detail?)
#2: Wo wird der State (Greetings) verwaltet?
Master oder Detail?
Schritt-für-Schritt in code/workspace-live-coding
Verwaltet den Zustand (u.a. welche Komponente sichtbar ist)
Rendering der Children
function GreetingController(props) {
const [mode, setMode] = React.useState(MODE_MASTER);
if (mode === 'MASTER') {
return <GreetingMaster />;
}
return <GreetingDetail />;
}
Wie wird zwischen den Komponenten kommuniziert?
Beispiel: Child-Komponente will Parent Informationen übermitteln
Callback-Funktionen als Properties #1
Kommunikation mit Children: Callback-Funktionen als Properties #2
function GreetingController(props) {
const [mode, setMode] = React.useState('MASTER');
if (mode === 'MASTER') {
return <GreetingMaster onAdd={() => setMode('DETAIL')} />;
}
return <GreetingDetail onSave={() => setMode('MASTER')} />;
}
function GreetingMaster(props) {
return (
// Tabelle mit Greetings ...
<button onClick={props.onAdd}>Add</button>
)
}
Wie kommen neue Greetings (GreetingDetail) in die Liste (GreetingMaster)?
Verwaltet den "globalen" State
State wird als Property an Children übergeben
State wird als Property an Children übergeben #2
function GreetingController(props) {
const [mode, setMode] = React.useState('MASTER');
const [greetings, setGreetings] = React.useState(props.initialGreetings);
if (mode === 'MASTER') {
return <GreetingMaster
greetings={greetings}
onAdd={() => setMode('DETAIL')}
/>
}
// ...
}
function GreetingMaster(props) {
return (
<table>
{ props.greetings.map(g => <tr>...</tr>) }
</table>
<button onClick={props.onAdd}>Add</button>
)
}
Verwaltet den "globalen" State
Neues Greeting wird per Callback-Funktion zurück gegeben
Neues Greeting wird per Callback-Funktion übergeben #2
function GreetingController(props) {
const [mode, setMode] = React.useState('MASTER');
const [greetings, setGreetings] = React.useState(props.initialGreetings);
if (mode === 'MASTER') {
// ...
}
return <GreetingDetail
onSave={newGreeting => {
setGreetings: [...greetings, newGreeting];
setMode('MASTER');
}}
/>;
}
function GreetingDetail(props) {
const [name, setName] = React.useState();
const [greeting, setGreeting] = React.useState();
return (
<input name="name" . . . />
<input name="greeting" . . . />
<button onClick={() => props.onSave({name: name, greeting: greeting})}>
Add
</button>
)
}
Beispiel: Unsere Anwendung (Zusammenfassung)
(Alternativ: Container und Presentation Components)
Füge deinen bestehenden Detail-View GreetingDetail
und einen Master-View
über eine Controller-Komponente zusammen
code/material/2-hierarchy
in deinen src-Ordner
(oder dein fertiges GreetingDetail
verwenden)
GreetingController
, so dass dein
GreetingDetail
angezeigt wird, wenn der Benutzer den Add-Button klickt.
Dort gibt es bereits einen Kommentar (TODO), der weitere Details enthält
GreetingDetail
brauchst du einen neuen Knopf, der mit dem neuen Gruß
den Callback aufruft
State als this.state
und this.setState()
Properties als this.props
import React from "react";
export default class GreetingDetail extends React.Component {
constructor(props) {
super(props);
this.state = {
name: props.initialName,
greeting: props.initialGreeting
};
}
render() {
return (
<div>
<input
value={this.state.name}
onChange={event => this.setState({name: event.target.value})}
/>
<input
value={this.state.greeting}
onChange={event => this.setState({greeting: event.target.value})}
/>
</div>
);
}
}
Daten lesen per GET
// Für GET Zugriff reicht es, die URL anzugeben:
try {
const response = await fetch('http://localhost:7000/api/greetings')
const json = await response.json();
// ...
} (catch ex) {
console.error('request failed', ex)
}
// Alternative mit Promise:
fetch('http://localhost:7000/api/greetings')
.then(response => response.json())
.then(json => /* ... */)
.catch(ex => console.error('request failed', ex));
Daten lesen per POST
fetch
erwartet als zweiten Parameter ein Objekt mit
Konfigurationsparametern, u.a:
method
: gibt die HTTP Methode an ( PUT
, POST
,
DELETE
, ...)
headers
: Objekt mit HTTP Headern für den Requestbody
: Der Request-Payload (als String)
const response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
// ...
try {
// 1. fetch gibt ein Promise zurück, dass mit dem
// Response-Objekt aufgelöst wird, wenn die Antwort vom Server
// kommt
const response = await fetch('http://localhost:7000/greetings');
// 2. das Response Objekt enthält eine json() Funktion,
// die das geparse JSON aus der Antwort zurückliefert
const greetings = await response.json();
// WAS MACHEN WIR MIT DER ANTWORT?
// ???
} catch (err) {
// 4. Falls etwas schief geht, Fehler loggen
console.error('request failed', err);
}
Wann müssen die Daten in der Greeting-Anwendung gelesen werden?
Schritt-für-Schritt (code/workspace-live-coding, vorher schritte/2-hierarchy reinkopieren
)
function GreetingController(props) {
React.useEffect(
() => console.log("Ich werde nach JEDEM Rendern ausgeführt")
);
}
function GreetingController(props) {
React.useEffect(
() => console.log("Ich werde nur nach 1. Rendern ausgeführt"),
[]
);
}
function GreetingController(props) {
React.useEffect(
() => console.log("..."),
[props.greetingId])
);
}
Zwei Parameter:
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])
);
}
useEffect
und (useState
) werden verwendet um Daten nach dem
1. Rendern zu laden:
function GreetingController() {
const [mode, setMode] = React.useState(MODE_MASTER);
const [greetings, setGreetings] = React.useState([]);
React.useEffect(() => {
async function loadGreetings() {
let greetings = null;
try {
const response = await fetch(BACKEND_URL);
greetings = await response.json();
} catch (err) {
console.error("LOADING GREETINGS FAILED:", err);
return;
}
setGreetings(greetings);
}
loadGreetings();
}, []);
// ...
}
Wann müssen die Daten in der Greeting-Anwendung gespeichert werden?
Im Event-Handler als Folge einer Benutzerinteraktion
Im Event-Handler sind wir nicht in der Render-Phase!
function GreetingController(props) {
async function addGreeting(greetingToBeAdded) {
let newGreeting;
try {
const response = await fetch(BACKEND_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(greetingToBeAdded)
});
if (response.status !== 201) {
throw new Error("Invalid status code: " + response.status);
}
newGreeting = await response.json();
} catch (err) { ... }
// use updater function (in setGreetings) to make sure
// we get the latest 'greetings' value from state
setGreetings(currentGreetings => [...currentGreetings, newGreeting]);
setMode(MODE_MASTER);
}
return
...
<GreetingDetail onSave={newGreeting => addGreeting(newGreeting)} />
...
}
Entwickle auf Basis von fetch eine Version des GreetingControllers, der die Daten auf dem Server laden und dort wieder speichern kann
Der Server ist bereits fertig. Zum Starten:
cd react-training/code/server
npm start
Zum Testen: http://localhost:7000/greetings
code/material/3-remote/GreetingController.js
in deinen
Arbeitsbereich
loadGreetings
und
saveGreeting
erfolgen
BACKEND_URL
. const BACKEND_URL = "http://localhost:7000/greetings";
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'.
class Sayer {
what: string; // Typ-Angabe für Felder ist erforderlich
constructor(what: string) { // Typ-Angabe für Parameter ist erforderlich
this.what = what;
}
// Angabe des Return-Types optional
sayIt(): string {
return `Saying: ${this.what}`;
}
}
class Sayer {
// Erlaubte Sichtbarkeiten: private | protected | public
private what: string;
constructor(what: string) {
this.what = what;
}
sayIt(): string {
return `Saying: ${this.what}`;
}
}
const sayer = new Sayer("Susi");
sayer.what = ""; // ERROR: Property 'greeting' is private
class Sayer {
// identisch zu vorherigem Beispiel
constructor(private what: string) {
}
sayIt(): string {
return `Saying: ${this.what}`;
}
}
class Sayer {
readonly what: string;
// Alternativ:
constructor(readonly public what: string) {
}
setWhat(newWhat: string) {
this.what = newWhat; // ERR Cannot assign to 'what'
// because it is a read-only property.
}
}
Klassen können ähnlich wie in Java mit Generics parametrisiert werden:
class Store<T> {
private value: T;
setValue(t: T) { this.value = t};
getValue() { return this.value ; }
}
const s = new Store<string>();
s.set("Klaus"); // ok
s.set(123); // err
const x:number = s.get(); // err
const y = s.get(); //OK (y ist string)
const z:string = s.get(); OK;
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'.
}
interface Book {
title: string
}
interface Movie {
title: string
}
const book:Book = { title: "Das Kapital" };
const movie:Movie = book; // OK, obwohl Book !== Movie
Mit type können Aliase für Typen definiert werden.
Mittlerweile sehr ähnlich zu Interfaces und fast können sowohl interface als auch type verwendet werden
type Book = {
title: string
}
type Movie = {
title: string
}
const book:Book = { title: "Das Kapital" };
const movie:Movie = { title: "Der weiße Hai" };
type MODE_DETAIL = "MODE_DETAIL";
let s:MODE_DETAIL = "MODE_DETAIL";
s = "MODE_MASTER" // ERROR
Generische Typen verwenden
type Person = { name: string };
type Movie = { title: string };
let persons:Array<Person> = [];
let movies:Array<Movie> = [];
persons.push({name: 'Klaus'});
movies.push({title: 'Batman'});
persons.push({title: 'Casablanca'}) // error ('title' not in Person)
persons = movies; // error
Methoden können beschrieben werden
type AnEvent = { type: string; payload: any }
type ListenerFunction = (e: AnEvent) => void;
class EventEmitter {
// addEventListener erwartet EINEN Parameter:
// eine Funktion, die ein 'AnEvent' entgegennimmer
addEventListener(listenerFn: ListenerFunction) {
listenerFn(null); // err
listenerFn({ type: "GREET", payload: "Hello"}); // OK
}
}
const c = new EventEmitter();
c.addEventListener( (e) => console.log(e)); // OK
c.addEventListener( (e,p: string) => console.log(e));
// ERR: Argument of type '(e: any, p: string) => void' is not
// assignable to parameter of type '(e: AnEvent) => void'.
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
Types können kombiniert werden. Sie enthalten dann alle Properties aus allen Types:
type Person = { name: string };
type Birthday = { dateOfBirth: number };
type PersonWithBirthday = Person & Birthday;
const x:PersonWithBirthday = {
name: "Klaus",
dateOfBirth: 1976
} // OK
const x:PersonWithBirthday = {
name: "Klaus"
} // ERR: Missing Property 'dateOfBirth'
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
}
}
Bestehende Typen können auf neue Typen mit neuen Eigenschaften gemappt werden:
type Person = { name: string; lastname: string};
const p:Person = {
name: "Klaus",
lastname: "mueller"
}
p.lastname = "Meier"; // OK
// ReadonlyPerson ist ein "mapped type"
type ReadonlyPerson = Readonly<Person>;
const p2:ReadonlyPerson = {
name: "Klaus",
lastname: "mueller"
}
p2.name = "Karl"; // Cannot assign to 'name' because it is a read-only property.
Achtung! TypeScript-Dateien, die JSX enthalten müssen mit
.tsx
enden!
👨💻Zeigen in workspace-typescript
type GreetingMasterProps = {
greetings: Greeting[]
onAdd: () => void
};
function GreetingMaster(props: GreetingMasterProps) {
props.greetings.length // OK
props.greeting // compile ERROR: Property 'greeting' does not exist on type 'GreetingMasterProps'.
props.onAdd("huhu"); // compile ERROR: Expected 0 arguments, but got 1.
}
// Mit Destructuring
function GreetingMaster({greetings, onAdd}: GreetingMasterProps) => {
// ...
}
Code Completion
Unbekanntes Property
Falsche Verwendung eines Properties
Der Typ des States bei useState kann abgeleitet
type GreetingDetailProps = { intitialGreeting: string };
function GreetingDetailProps(props: GreetingDetailProps) {
const [greeting, setGreeting] = React.useState(props.initialGreeting);
// greeting ist string, weil initialGreeting ein string ist
setGreeting("huhu"); // OK
setGreeting(666); // ERROR (falscher Typ)
setGreeting(null); // ERROR (falscher Typ)
}
Oder explizit angeben
type MODE_STATE = "MODE_MASTER" | "MODE_DETAIL";
function GreetingController() {
const [mode, setMode] = React.useState<MODE_STATE>("MODE_MASTER");
// Mode ist entweder String "MODE_MASTER" oder "MODE_DETAIL"
// setMode akzeptiert nur Strings "MODE_MASTER" oder "MODE_DETAIL"
setMode("NOT_FOUND"); // compile error
setMode(null); // compile error
}
React.Component
ist eine generische Klasse, die einen Typ für Properties
und State erwartet
type Greeting = {name: string; greeting: string};
type GreetingDetailProps = {
greeting?: Greeting;
onSave: (newGreeting: NewGreeting) => void;
}
type GreetingDetailState = {
name: string;
greeting: string;
}
class GreetingDetail
extends React.Component<GreetingDetailProps, GreetingDetailState> {
// ...
}
Properties und State sind typsicher
constructor(props: Props) {
super(props);
this.state = { name: '', greeting: ''} // OK
// ERROR: Object literal may only specify known properties,
// and 'aha' does not exist in type 'Readonly<State>'
this.state = {name: '', greeting: '', aha: 10};
// ERROR: Cannot assign to 'greeting' because
// it is a constant or a read-only property.
this.state.greeting = 'no way';
}
render() {
// ERROR: Property 'nothere' does not exist on type...
return <div>{this.props.nothere}</iv>;
}
Generisches React Event: React.SyntheticEvent
Wird parametrisiert mit dem Element, auf dem es definiert ist, z.B. HTMLInputElement
TypeScript weiß dann, wie das Event aussieht
function GreetingDetail(props) {
const [greeting, setGreeting] = React.useState("");
function handleChange(e: React.SyntheticEvent<HTMLInputElement>) {
setGreeting(e.currentTaget.value);
}
return <input onChange={handleChange} value={greeting} />
}
useRef
liefert Container für beliebigen Typ zurück.
Der gehaltene
Wert kann immer entweder vom vorgegebenen Typ sein (z.B. ein HTML Element) oder
null
function GreetingDetail(props) {
const inputRef = React.useRef<HTMLInputElement>(null);
// inputRef.current kann entweder HTMLInputElement oder null ein
function reset() {
inputRef.current.focus(); // ERR: current kann null sein
if (inputRef.current) {
inputRef.current.focus(); // OK
}
}
}
WORKSPACE:
Bitte arbeite in dem neuen Workspace code/workspace-typescript
, der die Anwendung aus dem letzten Schritt enthält, aber in TypeScript implementiert
VORBEREITUNG:
npm start
useApi.js
in useApi.tsx
. Sobald Du die Datei umbenennst,
sollten Compile-Fehler angezeigt werden)
GreetingDetail.js
in GreetingDetail.tsx
. Sobald Du die Datei
umbenennst, sollten Compile-Fehler angezeigt werden)
MASTER --onAdd--> DETAIL --onSave--> FEEDBACK --onOk--> MASTER
Wo muss ich nach Fehlern suchen? Wo ist die Logik?
Geht entweder nicht oder "Gott-Komponente" entsteht
Wie kommt Zustand von ganz oben nach ganz unten?
Wiederverwendung? React-unabhängigkeit? Testbarkeit?
Habt ihr Ideen?
Wie können wir Zustand und/oder Logik aus den Komponenten befreien?
(npm start in code/schritte/redux/7-redux-complete-app)
(npm start in code/workspace-live-coding-redux)
export function setFilter(filter) {
return {
type: SET_FILTER,
filter
};
}
export const loadGreeting = greetingId => dispatch => {
fetch(BACKEND_URL+'/'+greetingId)
.then(response => response.json())
.then(greetings => dispatch({
type: SET_GREETINGS,
greetings
});
};
Action-Creators sind die einzigen Teile einer Redux-Anwendung, die asynchrone Operationen ausführen dürfen
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { rootReducer } from './reducers';
// http://redux.js.org/docs/api/createStore.html
const store = createStore(
rootReducer // reducer
);
ReactDOM.render(
<Provider store={store}>
<GreetingController />
</Provider>,
mountNode
);
import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
rootReducer, // reducer
applyMiddleware(thunk) // middleware as enhancer
);
import {combineReducers} from 'redux';
// http://redux.js.org/docs/api/combineReducers.html
export const rootReducer = combineReducers({
greetings, // updates greeting partial state
filter,
mode
});
const mode = (state = MODE_MASTER, action) => {
switch (action.type) {
case SET_MODE:
return action.mode;
default:
return state;
}
};
import { useSelector } from 'react-redux';
function GreetingController() {
const mode = useSelector(state => state.mode);
...
}
import { useDispatch } from 'react-redux';
import * as actions from './actions';
function GreetingController() {
const dispatch = useDispatch();
async function addGreeting(greetingToBeAdded) {
await dispatch(actions.saveGreetingToServer(greetingToBeAdded));
}
...
}
Es gibt unterschiedliche Arten von "State"
Kontakt:
Mail: nils@nilshartmann.net
Twitter: @nilshartmann