git clone https://github.com/nilshartmann/react-training
cd react-training/blog-example/workspace
npm install
Slides: https://nils.buzz/techlab
oder: react-training/2019_hands_on.html
https://nilshartmann.net / @nilshartmann
Freiberuflicher Entwickler, Architekt, Trainer aus Hamburg
Java
JavaScript, TypeScript
React
Single-Page-Anwendungen
GraphQL
git clone https://github.com/nilshartmann/react-training
cd react-training/blog-example/workspace
npm install
npm start
Aufrufen: http://localhost:3000
Slides: https://nils.buzz/techlab
Im Verzeichnis blog-example/workspace ist schon alles vorbereitet
Du kannst dort mit npm install und npm start die (leere) Anwendung starten
In der Datei App.js kannst Du deine Anwendung implementieren
Dazu bitte in
index.js die App-Komponente importieren (siehe Kommentare dort)
Wenn Du Änderungen machst, wird die Anwendung automatisch gebaut und im Browser aktualisiert
cd code/blog-example/workspace
npm start
In beispiele
befinden sich mehrere Stände der fertigen Anwendung. Diese kannst Du zum Beispiel
als Hilfe bei Problemen verwenden.
01_addpost-form: Nur das Formular zum Eingeben eines neuen Blog Posts. Du siehst hier die useState-Verwendung.
02_blog-app: Eingabeformular und darunter die Liste mit den Blog Posts, aber ohne Server-Calls. Du siehst hier, wie zwischen Komponenten kommuniziert wird (App und AddPostForm)
03_blog-app-mit-server: Genau wie 02, nur mit Server-Calls, dh Blog Posts werden vom Server gelesen und dort gespeichert
04_blog-app-mit-router: Ähnlich Stand 03, aber die einzelnen Komponenten sind über Links erreichbar. Außerdem gibt es eine Ansicht für einen einzelnen Blog-Post
Du kannst im Team eine App ganz nach deinen Wünschen bauen 😊
Zur Inspiration hier einige Ideen:
(Du siehst, dass entspricht "zufällig" auch den Ständen im beispiele
-Verzeichnis 😇)
Untersuchen der React Anwendung zur Laufzeit
Bootstrap von React Projekten
Fertige Konfiguration von Webpack, Bablel React, ...
Beispiel: npx create-react-app PROJEKTNAME
oder mit TypeScript Support: npx create-react-app PROJEKTNAME --typescript
blog-example/workspace
)HelloWorld.js
import React from "react";
export default function HelloWorld(props) {
const [title, setTitle] = React.useState(props.initialTitle || "");
return (
<div>
<input onChange={event => setTitle(event.target.value)} value={title} />
<p>Eingegebener Titel: {greeting}</p>
<button onClick={() => setTitle("")}>Clear</button>
</div>
);
}
index.html
<html>
<-- ... -->
<body>
</body>
</html>
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import HelloWorld from './HelloWorld';
ReactDOM.render(<HelloWorld initialTitle="Moin moin"/>,
document.getElementById('root')
);
<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 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 [greeting, setGreeting] = React.useState(props.initialGreeting);
if (greeting === null) {
return Please enter greeting first
;
}
// ...
}
// 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!
JSX bietet nichts für Listen
Ausgabe typischerweise über
Array.map()
Elemente einer Liste brauchen einen 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>
)}
}
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
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 AddForm
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("addForm")} />
}
return <AddForm 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 (AddForm) anzeigen
function BlogList(props) {
return <div>
// ... Liste anzeigen ...
<button onClick={props.onAdd}>Add Blog Post</button>
</div>;
}
Die AddForm 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 AddForm(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>;
}
Der Server ist bereits fertig. Zum Starten:
cd react-training/blog-example/backend
npm start
Der Server ist über Port 7000 erreichbar
Zum Testen: http://localhost:7000/posts
Die Endpunkte:
GET /posts Alle Blog Posts abfragen
GET /posts?short: Alle Blog Post IDs und deren Titel
GET /posts/:id Einen einzelnen Blog Post lesen
POST /posts Einen neuen Blog Post anlegen. Als Payload/Body muss ein Objekt mit
title
und string
übertragen werden. Als Ergebnis wird der
komplette, neue Blog Post (inklusive ID) zurück gegeben
DELETE /posts/:id Einen Blog-Post löschen
Außerdem kann allen Requests der URL Parameter?slow übergeben werden, um die Antwort künstlich zu verlangsamen (wenn ihr z.B. mit Wartezuständen arbeiten wollt)
React macht keine Angabe, wie Server-Calls (technisch) gemacht werden
Häufig in React verwendet: fetch API
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/posts')
.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/posts');
// 2. das Response Objekt enthält eine json() Funktion,
// die das geparse JSON aus der Antwort zurückliefert
const posts = await response.json();
// WAS MACHEN WIR MIT DER ANTWORT?
// ???
} catch (err) {
// 4. Falls etwas schief geht, Fehler loggen
console.error('request failed', err);
}
Der useEffekt-Hook erlaubt es dir aber, Seiteneffekte zu definierten (späteren) Zeitpunkten auszuführen
Als 1. Parameter übergibst Du eine Callback-Funktion, die deinen Code enthält. Dieser darf Seiteneffekte enthalten, z.B. einen fetch-Aufruf
function App(props) {
React.useEffect(
() => console.log("Ich werde nach JEDEM Rendern ausgeführt")
);
}
function App(props) {
React.useEffect(
() => console.log("Ich werde nur nach 1. Rendern ausgeführt"),
[]
);
}
function BlogPost(props) {
React.useEffect(
() => console.log("..."),
[props.postId]
);
}
Zwei Parameter:
useEffect
und (useState
) werden verwendet um Daten nach dem
1. Rendern 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} />
))}
}
Lifecycle des gezeigten Beispiels:
Zum Beispiel als Folge einer Benutzerinteraktion:
Im Event-Handler dürfen wir direkt Seiteneffekte nutzen
function App(props) {
// laden, wie gesehen
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
...
<CreatePostForm onAdd={newPost => addPost(newPost)} />
...
}
Beispiel: code/blog-example/beispiele/04_blog-app-mit-router
Mappen von URLs auf Komponenten
(Navigation findet ohne Server-Roundtrip statt)
Komponenten halten (Teil) des Zustandes der Anwendung
Welche "Seite" ist sichtbar (Blog Liste, Formular oder Einzelansicht)?
Welche Daten werden dafür geladen (z.B. Blog Post Id)
Top-Level-Objekt, das einmalig (oben) in der Komponenten Hierarchie eingebunden werden muss
Mehrere Ausprägungen zum Arbeiten mit den URL und der Browser History:
import {HashRouter as Router} from "react-router-dom";
const app = <Router><App/></Router>;
ReactDOM.render(app, document.getElementById(...));
Das Route
-Objekt mappt Pfade auf Komponenten
if
/ switch
statement
path
wird der Pfad übergeben, für den die Route matchen soll
import {HashRouter as Router, Route} from "react-router-dom";
const app = <Router>
<Route path="/post/:postId"><BlogPostPage /></Route>
<Route path="/add"><AddPostPage onAdd={...} /></Route>
<Route path="/" exact><BlogListPage /></Route>
</Router>;
ReactDOM.render(app, document.getElementById(...));
In Routen werden Pfade angegeben, die mit der aktuellen URL verglichen werden
exact
kann das Verhalten verändert werdenpath
matcht immer
// trifft zu für / und /greeting
<Route path="/">...</Route>
// trifft nur zu für /
<Route path="/" exact>...</Route>
// passt auf JEDE URL:
<Route>...</Route>
<Route path="/posts/:postId"><BlogPostPage/></Route>
import { useParams } from "react-router";
function BlogPostPage() {
const params = useParams();
// params.postId enthält den variablen Wert aus der URL
Switch
sorgt dafür, dass nur die erste Komponente im Block gerendert wird
import {HashRouter as Router, Route, Switch} from "react-router-dom";
const app = (
<Router>
<Switch>
<Route path="/post/:postId"><BlogPostPage /></Route>
<Route path="/" exact><BlogListPage/></Route>
// "No match": ohne Pfad
<Route><NotFoundPage/></Route>
</Switch>
</Router>
);
ReactDOM.render(app, document.getElementById(...));
Mit Link
und NavLink
können Links erzeugt werden
to
wird das Ziel angegebena
ElementactiveClassName
und activeStyle
auf
NavLink
können Styles übergeben werden, die angewendet werden, wenn der
Link der aktiven Route entspricht
import {Link, NavLink} from "react-router-dom";
<Link to='/'>Show all Posts</Link>
// Erzeugtes 'a' Element erhält 'highlight' CSS-Klasse, wenn die aktive Route
<NavLink to='/add' activeClassName="highlight">Add Post</NavLink>
Mit dem history
-Objekt kann mit der Browser History interagiert werden
Mit der History kann auf andere URLs gesprungen werden oder die Location abgefragt werden
Komponenten bekommen das history-Objekt über den useHistory Hook
import { useHistory } from "react-router";
function App() {
const history = useHistory();
function onAdd(newBlogPost) {
...
// gehe zu neuer URL
history.push("/");
// Alternativ: gehe zu neuer URL, lösche aber aktuelle aus
// History im Browser
history.replace("/...")
}
return ...;
}
Kontakt:
Mail: nils@nilshartmann.net
Twitter: @nilshartmann