git clone https://github.com/nilshartmann/react-training
oder: git pull
https://nilshartmann.net / @nilshartmann
Freiberuflicher Software-Entwickler, Berater und Trainer aus Hamburg
Java
JavaScript, TypeScript
React
Single-Page-Applications
GraphQL
Alle Dienstleistungen auch online!
Dienstag
Mittwoch
Kürze Pausen zwischendurch
Jederzeit: Fragen und Diskussionen!
Fragen per Audio oder per Chat
Zentrales Konzept in React: Komponenten
Unser Beispiel in Komponenten
blog-example/workspace)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>
);
}
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 my_new_app_name
2020_1211_react.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
blog-example/steps/XX/build/index.html: Die fertige Anwendung nach einem Schritt zum Öffnen im Browser
cd blog-example/workspace
npm install
npm start
PostEditor.jstitle) und ein Eingabefeld dafür haben
blog-example/steps/1-hello-world
<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 HelloWorld() {
const [title, setTitle] = React.useState("");
return <input onChange={e => setTitle(e.target.value) value={title} />;
}
use beginnen (useState,
useEffect, ...)
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!)
(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() {
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("");
}
// ...
}
// 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();
}
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
}
function PostEditor() {
// type, onChange und value sind Properties!
return (
<input type="text" onChange={() => setTitle("...")} value={title}</h1>
);
}
}
name=value hingeschrieben
<PostEditor initialTitle="Hello" initialBody="World" />
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 ...;
}
}
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} />
body. Ein textarea-Element hat dieselbe API wie ein
input-Feld.
onClick.
disabled Property auf
true setzen.
initialTitle bzw. initialBody heißen.
index.js Initial-Werte für
die beiden Properties
blog-example/steps/2-editor-mit-props
"Rendern" hat leider doppelte Bedeutung!
Beispiel: blog-example/steps/2b-rendering
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 => (
<article key={post.id}>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
))
}
Wir haben nun 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/3-hierarchy
Jede React Komponenten-Klasse braucht eine render-Methode:
this.props
class Header extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<h1 style={{color: this.props.titleColor}}>{this.props.title}</h1>
);
}
}
function Header(props) {
return (
<h1 style={{color: props.titleColor}}>{props.title}</h1>
)
}
this.state={}
this.statethis.setState()
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>
);
}
}
blog-example/steps/4-remote
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));
// 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"
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
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)
}
"Wrapper" um Promises
Erlaubt es, asynchronen Code "linear" hinzuschreiben
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 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"}
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")
}
}
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)
})
// ...
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])
);
}
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 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) {
// 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)} />
...
}
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
blog-example/material/4-remote/App.js in deinen src-Folder und
füge dort den Code zum Laden und Speichern hinzu.
App.js sind TODOs enthalten.?slow an die URLs im fetch-Aufruf hängst, wird
der Server-Call künstlich verlangsamt
blog-example/steps/4-remote und
blog-example/step/4-remote-with-loading
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)
}
Beispiele: Laden von Daten, Arbeiten mit spezialisiertem State, ...
usebeginnen
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];
}
Example: code/blog-example/steps/5-typescript
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)
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
}
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
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
Füge die fehlenden Typ-Informationen in PostList.js und
PostEditor.js hinzu
PostList.js in PostList.tsx um und starte
npm start neu
PostList.tsx auf TypeScript umPostEditor.js in PostEditor.tsx um und starte
npm start neu
PostEditor.tsx auf TypeScript umblog-example/steps/5-typescript
Beispiel: steps/6-tests-with-material-ui
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>
))}
</>
}
classes-Property angeben:
<Input classes={{
focused: "my-focus-class",
disabled: "my-disabled-class"
}} ... />
my-focus-class und
my-disabled-class in Eurem Code irgendwo definiert sind, z.B. in einem
externen CSS-Stylesheet
className oder
classes angeben.
style-Elemente. Die Styles werden also nicht "inline" auf die Elemente
angewandt
makeStyles erzeugt.
makeStyles übergebt ihr ein Objekt mit CSS-Klassen und deren
Eigenschaften
// 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>
}
export const LargeInput = styled(Input)({
fontSize: props => props.xxl ? "2rem" : "1rem"
})
// Verwendung wie eine "normale" Komponente
<LargeInput xxl="true" />
createMuiTheme-Funktion verwendet
const blogTheme = createMuiTheme({ ... });
return <App>
<ThemeProvider theme={blogTheme}>
...alle Komponenten hierunter verwenden automatisch das eingestellte Theme...
</ThemeProvider>
</App>;
}
useTheme darauf zugreifen und die
Informationen auswerten:
function BlogList() {
const theme = useTheme();
return <article><h1 styles={{font: theme.typography.fontWeightBold}}>...</h1></article>;
}
Habt ihr noch Fragen, zur Material UI Bibliothek, die wir gemeinsam besprechen sollen?
"Delightful JavaScript Testing" ( https://jestjs.io/)
Complete Test solution for React (and others):
// 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);
});
test or
it
test('it should work', () => {
expect(...).toBe(...);
});
it('it should also work', () => {
expect(...).toBe(...);
});
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);
jest.fn() creates a mock function
// Returns undefined when executed
const aMockFn = jest.fn();
aMockFn("huhu"); // => undefined
expect(aMockFn).toHaveBeenCalled());
expect(aMockFn.toHaveBeenCalledWith("huhu"));
const aMockFn = jest.fn( param => `Hello, ${param}` );
console.log(aMockFn('World'));
// => Hello, World
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] }
]
}
expect(obj).toMatchSnapshot() compares a JSON-Object with a stored object
in a file:
import renderer from 'react-test-renderer';
test('it should render correctly', () => {
const somePosts = [ . . . ];
const component = renderer.create(
<PostList posts={somePosts} />
);
expect(component.toJSON()).toMatchSnapshot();
});
"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
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();
});
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>
);
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" });
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)
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
Challenges:
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
});
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
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));
}, []);
}
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
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
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 />);
// ...
})
Schritte:
blog-example/workspace-typescriptnpm test (der Prozess läuft "ewig")
PostEditor.test.tsx.
PostEditor.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.
blog-example/steps/6-tests
Often used:
testcafe firefox,chrome,safari test-cafe-tests/*.ts
👉 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-Funktion werden Tests geschrieben (nicht verwechseln mit
Jest...)
import { t } from "testcafe"
click) oder auch
Assertions ausführen (expect)
Selector-Instanz erzeugt:
const titleField = Selector("#titleField");
const thirdArticle = Selector("article").nth(2);
const thirdArticle = Selector(".btn").withText("Save");
import { ReactSelector } from 'testcafe-react-selectors';
const buttons = ReactSelector('PostEditor Button');
expect-Funktion definiert, die einen Wert
nimmt und ein Assertion-Objekt zurückliefert
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);
// 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();
expect-Aufrufe mit
await gewartet werden
await t.typeText(titleField, "Learning Testcafe");
await t.click(saveButton);
await t.typeText(titleField, "Learning Testcafe")
.expect(saveButton.getAttribute("disabled").notOk()
.click(saveButton);
Beispiel: steps/6-tests-with-material-ui/testcafe/app-test-rtl.ts
// 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);
Um die Entwicklung von Tests zu vereinfachen und Fehler zu finden, gibt es unter anderem folgende Features:
await t.typeText(titleField, "Hello")
.screenshot()
.click(saveButton);
await t.debug();
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?
Beispiele:
blog-example/steps/30-complete-app-with-hooksadvanced/steps/4a-context-hello-world
(advanced/exercise/4_context)
erlaubt das Durchreichen von Informationen ohne explizites angeben als Properties
Provider und Consumererzeugt 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
Eine React-Komponente, die einen Context zur Verfüng stellt
render eingebunden
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>;
}
}
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;
}
(npm start in code/schritte/redux/7-redux-complete-app)
Hello World-Redux (PostEditor mit Redux)
advanced/workspace-reduxadvanced/steps/5a-redux-hello-world
export function setFilter(filter) {
return {
type: SET_FILTER,
filter
};
}
export const loadPost = postId => dispatch => {
fetch(BACKEND_URL+'/'+postId)
.then(response => response.json())
.then(post => dispatch({
type: ADD_POST,
post
});
};
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}>
<App />
</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({
posts, // Blog Posts
auth // Aktueller Benutzer
});
const auth = (state = {}, action) => {
switch (action.type) {
case "LOGIN":
return { currentUser: action.username };
case "LOGOUT":
return {};
default:
return state;
}
};
import { useSelector } from 'react-redux';
function App() {
const currentUser = useSelector(state => state.auth.currentUser);
...
}
import { useDispatch } from 'react-redux';
import * as actions from './actions';
function App() {
const dispatch = useDispatch();
async function login(newUser) {
await dispatch(actions.login(newUser));
}
...
}
There are a lot of places where you can hold state
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)
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
Hands-On 👉 advanced/exercices/7a-mobx
Fertig 👉 advanced/steps/7a-mobx
class BlogAppState = {
blogPosts:Array<BlogPost> = [];
constructor() {
// Diese Instanz "observieren", MobX verfolgt jetzt alle Änderungen
makeAutoObservable(this);
}
}
// Unser globaler Zustand
const blogAppState = new blogAppState();
const PostList = observer(function PostList() {
return <div>
{blogAppState.blogPosts.map( p => /* wie gewohnt */ )}
</div>
})
makeAutoObservable erkannt,
oder ihr gebt sie explizit an
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() }, []);
// ...
}
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
observe-Funktion übergeben, die eine eine observed
Komponente zurückliefert
const PostList = observer(function PostList() {
return <div>
{blogAppState.blogPosts.map( p => /* wie gewohnt */ )}
</div>
})
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={...});
})
Wenn ihr noch Fragen habt, könnt ihr mich erreichen:
Mail: nils@nilshartmann.net
Twitter: @nilshartmann