React Workshop - Vorbereitung

Falls noch nicht gemacht:

  1. git clone https://github.com/nilshartmann/react-workshop.git
  2. Im geklonten Verzeichnis: npm install
    (Dafür evtl. einen Proxy konfigurieren http://wil.boayue.com/blog/2013/06/14/using-npm-behind-a-proxy/)
  3. npm start
  4. In neuem Terminal: in code/workspace wechseln
  5. Ausführen: npm start
  6. Browser öffnen: http://localhost:8080
  7. Wenn dort ein Hello-World-Text erscheint ist alles gut

Folien: Im geklonten Verzeichnis 2017_jax.html

React Workshop

Nils Hartmann / @nilshartmann

Oliver Zeigermann / @DJCordhose

http://bit.ly/jax2017-react-workshop

React Buch (https://reactbuch.de)

React Workshop (https://react-workshop.de)

Werbung

Lust bekommen, React zu entwickeln?

Wir stellen ein :-)

EOS Technology Solutions

Inhalt

Beispiel-Anwendung

Teil 0

React-Einführung und Build-Prozess

React

  • Framework für Facebook und viele andere
  • Komponenten kapseln Template und Logik
  • Deklarativ
  • Abstraktion vom DOM: f(model) -> UI
  • Minimales API
  • Ein-Weg-Data-Binding: Zustandsänderungen stellen Komponente komplett neu dar
  • Kann im Browser und auf dem Server rendern
  • wird für komplette Anwendung typischerweise mit weiteren Libs verwendet (Redux, Flux, Router, ...)
  • viel Inspiration aus der funktionalen Ecke (Immutable, pure functions, stateless)

React Komponenten

  • Werden als ES6 Klasse oder Funktion implementiert
  • Keine Templatesprache (stattdessen JavaScript)
  • Templates können HTML-artige Syntax enthalten (JSX)
class HelloMessage extends React.Component {
  render() {
    return <h1 className='title'>Hello, World!</h1>
  }
}

Eine erste Komponente: Hello, World!

Demo

ES6-Referenz

ES6 Features werden vorgestellt, wo wir sie brauchen

http://exploringjs.com/es6/

ES6: Template Strings

Template Strings werden in Backticks (``) geschrieben und können Ausdrücke (in ${}) enthalten:

const name = "Susi";
const greeting = `Hello, ${name}`; // Hello, Susi

const four = `Two and two is: ${2+2}` // Two and two is: 4

const time = `The time is: ${new Date()}`); // The time is: ...
            

ES6: Klassen

class Person {
    constructor(name) {
        this._name = name;
    }
    get name() {
        return this._name;
    }
}
class Programmer extends Person {
    constructor(name, language) {
        super(name);
        this.language = language;
    }
    code() {
        return `${this.name} codes in ${this.language}`;
    }
}
const programmer = new Programmer('Erna', 'JavaScript');
console.log(programmer.code());
console.log(programmer instanceof Programmer); // true
console.log(programmer instanceof Person); // true

ES6: Arrow Functions

const displayInPage = (text) => {
   return document.body.innerHTML +=
       `${text}
`; };
// Klammern können weggelassen werden, genau ein Parameter
// ebenso die geschweiften Klassen, wenn nur ein Statement:
const displayInPage = text => document.body.innerHTML += `${text}
`;

Hello World React

class HelloMessage extends React.Component {
  render() {
    return (<div>
            <input ref={input => this.input = input}
                   onChange={event => this.updateModel(event)}
                   value={this.state.greeting} />
            <p>{this.state.greeting}, World</p>
            <button
                onClick={() => this.reset()}>
                Clear
            </button>
        </div>);
  }
  constructor(props) {
    super(props);
    this.state = {greeting: this.props.greeting};
  }
  updateModel(event) {
    this.setState({greeting: event.target.value});
  }
  reset() {
    this.setState({greeting: ""});
    this.input.focus();
  }
}

Aufruf


// 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 greeting="Hello"/>, mountNode);
Run

Buildprozess

  • ES6- und JSX-Code muss nach ECMAScript 5 übersetzt werden
  • Typische Werkzeuge
    • Babel (Compiler)
    • Webpack (Bundler)
    • Webpack Dev Sever (HTTP Server mit Hot Reload)

Webpack und Babel

Webpack Development Server

  • Zum Ausführen der Anwendung wird ein Webserver benötigt
  • Für die Entwicklung Webpack Dev Server:
    • ...führt Webpack auf Basis der Projekt Webpack Konfiguration aus
    • ...erzeugt Ausgabe in Memory (Performance)
    • ...Hot Reloading: Automatische Aktualisierung nach Code Änderung
    • ...React Hot Loader: Zustand bleibt nach Aktualisierung erhalten (Beta)
  • npm start in unserem Beispiel-Projekt

Übung 0: React Hello-World

Mach dich mit den Werkzeugen vertraut und schreib deine erste React-Komponente

Schritt #1: Tool-Chain starten (falls noch nicht gemacht)

Nutze für die Übungen das workspace-Verzeichnis in diesem Repository. Hier ist eine Tool-Chain mit Webpack und Babel vorbereitet

  1. npm install (auf oberster Ebene)
    (Dafür evtl. einen Proxy konfigurieren http://wil.boayue.com/blog/2013/06/14/using-npm-behind-a-proxy/)
  2. cd code/workspace
  3. npm start
  4. Öffne http://localhost:8080 im Browser
  5. Wenn auf der Seite 'Hello, World' steht, ist alles gut

Schritt #2: Deine erste React-Komponente

  • Ersetze die "statische" Komponente (HelloMessage) mit der React-Komponente aus dem vorherigen Beispiel aus den Folien
  • Zusatzaufgabe: Experimentiere mit der Anwendung, mache einige Änderungen, wie z.B.
    • Alle Eingaben sollen in Großbuchstaben auftauchen
    • Gib irgendeine Rückmeldung wenn die Eingabe erfolgreich gelöscht wurde

Teil I

React-Komponenten

Ziel-Anwendung

Referenz

https://facebook.github.io/react/docs

Themen

  • Rendering
  • Properties und Zustand
  • Referenzen auf DOM-Elemente (Refs)

React: Rendering

Jede React-Komponente braucht eine render-Methode:

  • wird aufgerufen beim ersten Rendering und wenn sich der Zustand ändert
  • liefert genau ein Element (oder keins) zurück
  • kann HTML-artige JSX-Syntax nutzen

class GreetingDetail extends React.Component {
  render() {
    return (
      
<input onChange={event => this.updateModel(event.target.value)} value={this.state.greeting} />

{this.state.greeting}, World

); } // ... }

React: JSX

  • Wird wie HTML hingeschrieben, inkl Attribute:
    
    <div><input type="text"/></div>
                        
  • Attribute, die keine Strings sind, müssen in {} eingechlossen werden:
    
    <Counter label="Count" count={7} showValues={true} />
                        
  • Kann pures JavaScript enthalten, eingeschlossen in {}:
    
    const title = 'Hello, World';
    <h1>{title.toUpperCase()</h1>
                        
  • class-Attribut heißt className:
    
                            <h1 className="title">...</h1>
                        
  • CSS-Eigenschaften werden als Objekt übergeben in Camel-Case-Notation:
    
    const styles = { marginLeft: '10px', border: '1px solid red' };
    <h1 style={styles}>...</h1>
                        

React: Rendering #2

  • DOM-Events werden in React-Events verpackt
  • React-Events haben weitgehend selbe API wie DOM-Events
  • Es gibt keine automatische Bindung an Modelle (2-Wege Databinding)

class GreetingDetail extends React.Component {
  render() {
    return (
      
<input onChange={event => this.updateModel(event.target.value)} value={this.state.greeting} />

{this.state.greeting}, World

); } updateModel(greeting) { this.setState({greeting}); } // ... }

ES6: Erweiterte Objekt-Literale

const name = 'Oma';
const person = {
    // ES5: name: name
    name
};
console.log(person.name); // Oma
   

ES6: Module, Importe und Exporte

ES6: Export einer einzigen Klasse

// Person.js
class Person {
  // ...
}
export default Person;
   
// Person.js
// in einer Zeile zusammengefasst
export default class Person {
  // ...
}
   

ES6: Import

// Programmer.js
import Person from './Person';

export default class Programmer extends Person {
  // ...
}
   

ES6: Benannte Exporte

// util.js
export function displayInPage(text) {
    document.body.innerHTML +=
        `${text}
` ; } export showInfo = msg => window.alert(`Wichtige Info: ${msg}`); // or function displayInPage(text) { . . . } const showInfo = ...; export { displayInPage, showInfo };
import {displayInPage} from "./util";
displayInPage('Hello, World');
   

ES6: Destructuring von Objekten

const person = {
  name: 'Olli',
  email: 'oliver.zeigermann@gmail.com'
};
const {name, notThere} = person;
console.log(`name=${name}`);
// name=Olli
console.log(`notThere=${notThere}`);
// notThere=undefined
function someFunction({name, notThere}) {
  console.log(`name=${name}`);
  // name=Olli
  console.log(`notThere=${notThere}`);
  // notThere=undefined
}
someFunction(person);

React: Properties und Zustand

  • Properties werden der Komponente von außen übergeben (und nicht verändert)
  • Zustand (State) ist eine innere Eigenschaft der Komponente (die verändert werden kann)
  • Beides sind Objekte mit Key-Value-Paaren
  • Beide können an Unterkomponenten übergeben werden

Properties einer Komponente

  • Properties werden über den Konstruktor in die Komponente hineingereicht
  • Properties dürfen nicht verändert werden
  • Zugriff über this.props
  • this.props.children enthält Kind-Elemente

class TitleComponent extends React.Component {
    constructor(props) {
        super(props);
    }
    render() {
      return <h1>{this.props.title}</h1>
    }
   // ...
}

  <TitleComponent title='Hello World' />

Zustand einer Komponente

  • Beispiel: Inhalt eines Eingabefelds, Daten vom Server
  • Werte üblicherweise immutable
  • Initialisieren im Konstruktor mit this.state={}
  • Zustand lesen über this.state
  • Zustand setzen über this.setState()
    • Achtung: kein "reiner" Setter
    • Führt alten und neuen Zustand zusammen
    • Wird asynchron ausgeführt!
    • Löst erneutes rendern der gesamten Komponente aus

Beispiel: Zustand einer Komponente


class GreetingDetail extends React.Component {
    constructor(props) {
        super(props);
        this.state = { name: 'Klaus' };
    }

    updateModel(event) {
        // Zustand ändern: Komponente wird neu gerendert
        this.setState({name: event.target.value});
    }

    render() {
        return <input value={this.state.name}
            onChange={e => this.updateModel(e)} />
    }
   // ...
}

Render Zyklus

Virtual DOM

React: Referenzen auf nativen DOM

  • Elementen kann eine Callback-Funktion refs übergeben werden
  • Funktion wird nach dem Rendern mit Referenz auf DOM Element aufgerufen (oder null)
  • Diese Referenz kann man z.B. als Member-Variable speichern

class HelloMessage extends React.Component {
    render() {
        return (
            
<input ref={input => this.input = input} /> <button onClick={() => this.input.focus()}> Focus </button>
); } }

Übung 1: Detail-Ansicht für unsere Gruß-Anwendung

Erzeuge eine Komponente mit der man einen Namen und eine Grußformel eingeben kann

Schritte

Erweitere deine erste React-Komponente im workspace-Ordner

  • benenne deine Komponente in GreetingDetail um
  • du brauchst zwei Eingabefelder, die name und greeting im Zustand der Komponente setzen
  • Zusatzaufgabe: Erweitere deine Komponente so, dass man von außen Properties übergeben kann, die den Zustand initialisieren

Teil II

Komponentenhierarchien

Komplette Anwendung aus Komponenten bauen

Ziel-Anwendung

Thinking in React

https://facebook.github.io/react/docs/thinking-in-react.html

Themen

  • Komponenten als Funktion
  • Darstellung von Listen und Keys
  • Architektur-Idee: Controller-Komponente und View-Komponente
  • Durchreichen von Zustand und Callbacks
  • PropTypes

Komponenten als Funktion

  • Komponente ist eine einfache Funktion
  • entspricht der render-Methode
  • Properties werden als Object per Parameter übergeben
  • Zurzeit nur ohne Zustand und ohne Lifecycle-Methoden
  • Empfehlung: Funktionen statt Klassen verwenden (wenn möglich)

import React from 'react';
export default function Greet(props) {
  return (
      

{props.greeting}

{props.name}

); } // Verwendung: <Greet name="Susi" greeting="Hello" />

Komponenten als Funktion


// Mit Destructuring
export default function Greet({greeting, name}) {
  return (
      

{greeting}

{name}

); }

// Als Arrow Function
const Greet = ({greeting, name}) => 

{greeting}

{name}

} export default Greet;

Listen und Keys

  • JSX bietet nichts für Listen
  • Ausgabe typischerweise über Array.map
  • Elemente einer Liste brauchen einen eindeutigen Key

const greetings = [{
    id: 0,
    name: 'Olli',
    greeting: 'Huhu'
},
{
    id: 1,
    name: 'Oma',
    greeting: 'Hallo'
}
];
const body = greetings.map(greeting =>
    <tr key={greeting.id}>
        <td>{greeting.name}
        <td>{greeting.greeting}
    </tr>);

Komponenten in Hierarchien

Beispiel: Unsere Anwendung

Controller und Child-Views

Durchreichen vom Zustand und Callbacks

  • Funktioniert beides über Properties

class GreetingController extends React.Component {
    render() {
        const {greetings} = this.state;
        return <div>
                <GreetingMaster greetings={greetings}
                    onAdd={() => this.setState({mode: MODE_DETAIL})} />
            </div>;
    }
    // ...
}
    

Properties übergeben mit Object-Spread

  • Übergibt alle Eigenschaften eines Objektes als individuelle Properties

class GreetingController extends React.Component {


    render() {
        const greeting = { name: 'Klaus', greeting: 'Hello' };

        return <GreetingDetail {...greeting} />

        // entspricht:
        // <GreetingDetail name='Klaus' greeting='Hello' />
    }
}
        

PropTypes

  • Eine Komponente kann deklarieren, welche Properties sie erwartet
  • Auch der Typ kann angegeben werden (Mögliche Typen)
  • Fehlende / falsche Properties führen zu Laufzeitfehlern
  • Ab React 15.5 eigenes npm modul: prop-types
  • Alternative: Typsystem (Flow oder TypeScript)

import PropTypes from 'prop-types';

class GreetingDetail extends React.Component { . . . };

GreetingDetail.propTypes = {
    greeting: PropTypes.shape({
      name: PropTypes.string.isRequired,
      greeting: PropTypes.string.isRequired
    }),
    onAdd: PropTypes.func.isRequired
};

PropTypes

Mit statischen Properties (static noch kein JS Standard!)


import PropTypes from 'prop-types';

class GreetingDetail extends React.Component {
  static propTypes = {
    greeting: PropTypes.shape({
      name: PropTypes.string.isRequired,
      greeting: PropTypes.string.isRequired
    }),
    onAdd: PropTypes.func.isRequired
  };

  render() { . . . }
};
            

PropTypes

Für Komponenten als Funktionen


import PropTypes from 'prop-types';

function HelloMessage(text) { . . . }
HelloMessage.propTypes = {
  text: PropTypes.string.isRequired
}
            

Übung 2: Eine komplette Anwendung zusammen setzen

Füge deinen bestehenden Detail-View (GreetingDetail) und einen Master-View über eine Controller-Komponente zusammen

Schritte

  1. kopiere das Material aus code/material/2-hierarchy in deinen src-Ordner
  2. erweitere im GreetingController die render-Methode, so dass dein GreetingDetail angezeigt wird, wenn der Benutzer den Add-Button klickt:
    • dort gibt es bereits einen Kommentar, der dir die richtige Stelle anzeigt und weitere Details enthält
    • übergib einen Callback der addGreeting nutzt
  3. im deinem GreetingDetail brauchst du einen neuen Knopf, der mit dem neuen Gruß den Callback aufruft
    (Du kannst dein GreetingDetail verwenden, oder die Vorlage code/material/2-hierarchy/src/_GreetingDetail.js verwenden)

Architektur Beispiel-Anwendung

GreetingMaster

GreetingDetail

Teil III

Remote-Calls gegen Server

Ziel-Anwendung

Herausforderungen

  • Wie machen wir das Laden und Speichern technisch?
  • Wo steht der Code zum initialen Laden der Grüße?
  • Wo speichern wir?
  • Wie funktioniert asynchrone Verarbeitung in React?

Server-Calls

Beispiel: fetch


// GET
fetch('http://localhost:7000/api/greetings')
  .then(response => response.json())
  .then(json => /* ... */)
  .catch(ex => console.error('request failed', ex));

// PUT
fetch(url, {
    method: 'PUT',
    headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
})
  .then(...)

ES6: Promises

Beispiel: fetch

const url = `${BACKEND_URL}${path}`;

return fetch(url)
    .then(response => response.json())
    .then(json => /* ... */)
    .catch(ex => console.error('request failed', ex));

Ein Promise ist ein Versprechen auf einen Wert

Wird evtl. erst in der Zukunft eingelöst

Erzeugen und asynchron reagieren

const promise = new Promise(resolve =>
    setTimeout(
        () => resolve('Result from promise'),
        1000)
);
promise.then(value => console.log(value));
// Output after 1 second: Result from promise

Verkettete Ausführung

const promise = new Promise(resolve =>
    setTimeout(
        () => resolve('Result from promise'),
        1000)
);
// then returns a new promise
const promise2 = promise.then(value => `${value} plus stuff`);
promise2.then(value => console.log(value));
// Output after 1 second: Result from promise plus stuff

Fehler führen zum Abbruch der Chain

Allerdings muss man etwas tun, um das mitzubekommen

Catch-Klausel im Erfolgsfall

Promise
    // creates and directly resolves promise
    .resolve('Result from promise')
    .then(x => {
        // this will be printed
        console.log(x);
    })
    .then(() => {
        console.log('This will be printed');
    })
    // this will NOT be printed as no error occured
    .catch(e => console.log('error: ', e))

// Output:
// Result from promise
// This will be printed

Catch-Klausel im Fehlerfall

Promise
    // creates and directly resolves promise
    .resolve('Result from promise')
    .then(x => {
        // this will be printed
        console.log(x);
        throw new Error('Something went wrong');
    })
    .then(() => {
        console.log('This will NOT be printed');
    })
    // this will be printed
    .catch(e => console.log('error: ', e))

// Output:
// Result from promise
// error:  [Error: Something went wrong]

fetch im Detail


// 1. fetch returns a promise, that will be resolved
// with a Response object when response is received
// from server
fetch('http://localhost:7000/api/greetings')

// 2. the Response object contains a json() function,
// that returns the parsed JSON from the Response body
  .then(response => response.json())

// 3. with the resolved JSON object we set the
// component state (=> leads to re-rendering)
  .then(json => this.setState({greetings: json})

// 4. in case something goes wrong (during request,
// request processing or rendering)
  .catch(ex => console.error('request failed', ex));
// as an alternative we could set and render an error msg:
//  .catch(ex => this.setState({error: ex})

Initiales Laden von Daten

  • Komponenten können Lifecycle-Methoden haben: https://facebook.github.io/react/docs/react-component.html#the-component-lifecycle (nur Klassen!)
  • componentDidMount wird aufgerufen, wenn Komponente ins DOM gerendert wurde
  • Hier werden Daten üblicherweise initial geladen:
    class GreetingController extends React.Component {
      constructor(props) {
        // intial state (empty now)
        this.state = { greetings: [] };
      }
      componentDidMount() {
        fetch('/api/greetings')
          .then(response => response.json())
          .then(json => this.setState({greetings: json})
          .catch(. . .)
        ;
      }
    }
                    

Speichern von Daten

Zum Beispiel als Folge einer Benutzerinteraktion:

class GreetingController extends React.Component {
  render() {
    ...
      <GreetingDetail onAdd={greeting => this.saveGreeting(greeting)} />
    ...
  }
  saveGreeting(greetingToBeSaved) {
    fetch('/api/greetings', {
      method: 'POST',
      headers: ...,
      body: JSON.stringify(greetingToBeSaved)
    })
      .then(response => response.json())
      .then(json => ...)
      .catch(. . .);
  }
}
                

Übung: Laden und Speichern der Daten von/auf einem Server

  • Entwickle eine Version vom GreetingController, die die Daten auf dem Server laden und dort wieder speichern kann
  • Der Server ist bereits vorgegeben und kann mit npm start im Root-Verzeichnis gestartet werden. Er ist dann unter Port 7000 erreichbar

Vorbereitung für die Übung

  1. Kopiere code/material/3-remote/backend.js und code/material/3-remote/main.js in deinen Workspace
  2. Du kannst außerdem code/material/3-remote/_GreetingController.js als Template verwenden. Die notwendigen Schritte sind dort als Kommentar markiert.

Stelle den GreetingController um

  1. Importiere loadFromServer und saveToServer aus dem backend-Modul
  2. Füge in der Komponente GreetingController zwei Methoden zum Laden und Speichern der Greetings hinzu
  3. Nach dem Mounten der Komponente in den DOM sollen die Greetings geladen werden (Lifecycle-Methode componentDidMount)
  4. Das onAdd-Callback von GreetingDetail soll die übergebene Greeting speichern und im Erfolgsfall das gespeicherte Greeting in die Liste aller Greetings zufügen.

Weitere Details findest du unter: code/material/3-remote/AUFGABEN.md

Teil IV

Testen (Überblick)

Was testen wir überhaupt?

  • UI-unabhängige Logik (z.B. Services, Backend-Calls)
  • Rendern (wird meine Greeting-Liste für ein Satz von Greetings korrekt dargestellt)
  • Interaktionen (funktionieren meine Event-Listener korrekt)
  • Verhalten im Browser (z.B. korrekte Darstellung, Browser-spezifisches JavaScript, Arbeiten mit history, Titelzeile, Scrollbars etc)

Was testen wir überhaupt?

  • UI-unabhängige Logik
  • Rendern (wird meine Greeting-Liste für ein Satz von Greetings korrekt dargestellt) React-spezifisch!
  • Interaktionen (funktionieren meine Event-Listener korrekt) React-spezifisch!
  • Verhalten im Browser (z.B. korrekte Darstellung, Browser-spezifisches JavaScript, Arbeiten mit history, Titelzeile, Scrollbars etc)

Anforderungen

  • Komponenten müssen im Test gerendert werden können
  • Gerenderte Komponenten/HTML-Elemente müssen gefunden und validiert werden
  • Zustand und Properties der Komponenten müssen veränderbar sein
  • Events müssen getriggert oder simuliert werden können
  • Interaktion mit DOM muss evtl möglich sein

Jest

"Painless JavaScript Testing" (http://facebook.github.io/jest/)

Vollständige Test-Lösung, wird von Facebook für Testen von React verwendet:

  • Test Runner
  • Specs, Assertions, Mocks
  • Code Coverage
  • Snapshot testing

Beispiel: Ein einfacher Test

// sum.js
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);
});
            

React Test Renderer

https://www.npmjs.com/package/react-test-renderer

Rendert React Komponenten in JSON Objekte (ohne DOM):

import renderer from 'react-test-renderer';
const component = renderer.create(
        <GreetingMaster greetings={someGreetings} />
);

console.log(component.toJSON());
            

{ type: 'div',
  props: {},
  children:  [
    { type: 'table', props: {}, children: [Object] },
    { type: 'button', props: [Object], children: [Object] }
  ]
}
            

"Snapshot Testing" mit Jest

expect(obj).toMatchSnapshot() vergleicht ein JSON-Objekt mit einer gespeicherten Datei:

  • Bei erster Ausführung: legt Snapshot-File an (Beispiel)
    • Snapshotdateien werden in Git versioniert
  • Bei folgenden Ausführungen: erzeugt neuen Snapshot und vergleicht mit gespeichtertem Snapshot
  • Wenn Snapshots unterschiedlich
    • Fehler samt Diff (Beispiel)
    • Im Watch Mode kann Snapshot aktualisiert werden

Snapshot Testing mit Jest und React

import renderer from 'react-test-renderer';

test('it should render correctly', () => {

  const someGreetings = [ . . . ];

  const component = renderer.create(
    <GreetingMaster greetings={someGreetings} />
  );

  expect(component.toJSON()).toMatchSnapshot();
});
            

Snapshot sagt nicht, ob UI richtig oder falsch gerendert wird, sondern nur, ob sie verändert wurde!

Enzyme

JavaScript Testing utilities for React (http://airbnb.io/enzyme/)

Bibliothek mit Funktionen zum Testen von React Komponenten

  • Rendern von Komponenten:
    • shallow zum "flachen" Rendern einer Komponente
    • mount zum Rendern einer Komponete in einen (headless) DOM, z.B. jsdom
  • Navigieren durch den DOM und Suchen von Elementen und Komponenten
  • Modifizieren von Komponenten und Auslösen von Events

Testen einer Komponente mit Jest und Enzyme

import {mount} from 'enzyme';
import GreetingController from '...';
import GreetingDetail from '...';
test('it should open detail view on button click', () => {
    // mount the component into a real dom (implemented by JSDom)
    const component = mount(<GreetingController  />);

    // on initial render the list with greetings (GreetingMaster)
    // is visible but no GreetingDetail
    expect(component.find(GreetingDetail)).toHaveLength(0);

    // find the "add" Button...
    const addButton = component.find('button');

    // click on the button
    addButton.simulate('click');

    // now the GreetingDetail should be visible
    expect(component.find(GreetingDetail)).toHaveLength(1);
});
            

Teil V

Integration mit 3rd-Party Bibliotheken

Ziel-Anwendung

3rd Party Libs?

Es gibt eine große Anzahl von sehr praktischen JavaScript-Bibliotheken

Viele davon sind aber nicht als React-Komponenten entwickelt worden

Beispiels

jQuery und jQuery Plugins wie z.B. Bootstrap

d3 für interaktive SVGs und Chart Bibliotheken wie nvd3

Unser Beispiel: Verteilung der Grüße

nvd3 Pie Chart mit d3

Aufgabe: Einbetten des Pie Charts in eine React-Komponente

Herausforderungen?

  1. Wie kommen wir an den DOM Knoten der React-Komponente?
  2. Wie sagen wir React, dass nun NVD3 den Rest macht? Also, dass React nicht mehr neu rendern soll.
  3. Änderungen des Zustands sollen nach wie vor richtig dargestellt werden
  4. Ab welchem Zeitpunkt soll NVD3 das Rendern übernehmen?
  5. Wie können wir aus NVD3 heraus auch wieder andere React-Komponenten beeinflussen?
  6. Wie räumen wir wieder auf?

Hintergrund: Charts mit NVD3

Basiert auf D3.js

Benötigt svg DOM-Element um sich zu rendern

Stark vereinfachtes Beispiel:

import d3 from 'd3';
import nv from 'nvd3';

// Chart erzeugen
const chart = nv.models.pieChart();
// ...Chart Config ausgelassen ...

// mit d3 rendern und mit Daten versorgen
const element = document.getElementById('chart');
d3.select(element);
  .datum(data)
  .call(chart);

// Callbacks registrieren (z.B. bei Klick auf ein Element)
chart.pie.dispatch.on("elementClick",
                       e => console.log(e.data.label));
            

React Chart Komponent #2

React Callback Funktionen

  • (einmalig) componentDidMount(): Komponente wurde gerendert, Element sind im DOM
  • componentWillReceiveProps(nextProps): An die Komponente wurden neue Properties übergeben. Die neuen Properties werden als Parameter übergeben
  • shouldComponentUpdate(): Entscheidet, ob Komponete erneut gerendert werden soll (default: true)
  • (einmalig) componentWillUnmount(): Wird aufgerufen, bevor Komponente aus dem DOM entfernt wird

React Chart Komponente #1


class Chart extends React.Component {
    render() {
        // (1) we render an empty svg and
        //     remember the reference to the DOM node
        return <svg ref={c => this._chart = c}></svg>
    }

    componentDidMount() {
        const {data, onSegmentSelected} = this.props;

        // (2) once rendered by react we create the nvd3 chart
        const chart = createNvd3Chart(this._chart, data);

        // (3) we delegate the label of clicked segment
        //     back to parent component
        if (onSegmentSelected) {
            chart.pie.dispatch.on(
                "elementClick", e => onSegmentSelected(e.data.label));
        }
    }

    // ...
}
       

React Chart Komponent #2


class Chart extends React.Component {
    // ...

    componentWillReceiveProps(nextProps) {
        const {data} = nextProps;
        // (4) we get updates of properties making it reactive
        updateNvd3Chart(this._chart, data);
    }

    shouldComponentUpdate() {
        // (5) once rendered we will never render again
        // (svg-Element is managed by D3)
        return false;
    }

    // (6) called just before destroying component
    componentWillUnmount() {
        this._d3selection.remove();
    }
}
       

Übung: Ein Pie-Chart integrieren

Das Pie Chart soll eine Übersicht aller Grüße anzeigen

Ein Klick auf ein Segment des Pie Charts soll die Liste der Grüße filtern

Schritte

  • kopiere das Material aus code/material/5-third-party in deinen src-Ordner
  • binde die Chart-Komponente in die render-Methode des GreetingController ein
  • alle Stellen die du ändern musst sind dort bereits mit einem Kommentar versehen

Zusatzaufgabe: Ein zweites Mal klicken auf das Segment soll den Filter wieder löschen

Teil VI (Überblick)

Client-seitiges Routing

Ziel-Anwendung

Warum Routing?

Mappen von URLs auf Komponenten
(Navigation findet ohne Server-Roundtrip statt)

Komponenten halten (Teil) des Zustandes der Anwendung
Welche Komponente ist sichtbar (Master oder Detail)?
Welche Daten werden dafür geladen (z.B. Greeting Id)

Herausforderungen

Auf Änderungen der URL reagieren
Ableiten des Zustandes aus der URL
Wenn sich die URL ändert, kein Server roundtrip

Hierarchische Komponentenstrukturen

Beim Klick auf Links etc aktualisieren der URL

React Router

Router

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:

  • HashRouter: codiert Pfad in angehängten Hash (#/greetings)
  • BrowserRouter: codiert Pfad direkt in URL (/greetings)


import {HashRouter as Router} from "react-router-dom";

const app = <Router><GreetingController/></Router>;

ReactDOM.render(app, document.getElementById(...));
            

Route

  • Kann überall in der Anwendung verwendet werden, wo Pfad-abhängig Komponenten ausgewählt werden sollen
  • Mit path wird der Pfad übergeben, für den die Route matchen soll
  • Mit component wird die Komponente übergeben

import {HashRouter as Router, Route} from "react-router-dom";

const app = <Router>
    <Route path="/greet/:greetingId" component={GreetingDisplayController}/>
    <Route path="/" component={GreetingController}/>
</Router>;

ReactDOM.render(app, document.getElementById(...));
            

Route

  • Statt einer Komponente kann eine Funktion übergeben werden, die eine Komponente zurückliefert
  • Ermöglicht es, zusätzliche Properties an die Komponente zu übergeben
  • Kann unterschiedliche Komponenten zurückliefern (z.B. für Authorisierung)
  • 
    <Route path="/"
      render={() => {
        return loggedIn ? <GreetingMaster greetings={initialGreetings} />
                        :
                        <LoginForm />
                        }
    />
                

Pfade

  • Sind per Default gültig für Teilstrings
  • Mit exact kann das Verhalten verändert werden
    
    // trifft zu für / und /greeting
    <Route path="/" component={GreetingController}/>
    // trifft nur zu für /
    <Route path="/" exact component={GreetingController}/>
                
  • Route ohne path matcht immer
  • Kann variable Segmente enthalten:
    
    <Route path="/greeting/:greetingId"
              component={<GreetingDisplay />} />
    
    // in GreetingDisplay kann die greetingId über
    // this.props.match.params.greetingId abgefragt werden
                

Switch

  • Wenn mehrere path-Ausdrücke matchen, werden mehrere Komponenten gerendert (z.B. "/" und "/greetings")
  • 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="/greet/:greetingId" component={GreetingDisplayController}/>
    <Route path="/" component={GreetingController}/>

    // "No match": ohne Pfad
    <Route component={NotFoundPage}/>

  </Switch>
</Router>;

ReactDOM.render(app, document.getElementById(...));
            

Router Properties für Komponente

Der Router übergibt drei Properties an die gerenderte Komponente (oder an die render()-Funktion)

  • match: Enthält u.a. die Parameter aus dem Pfad (match.params.xyz)
  • history: Zum Arbeiten mit der History (z.B. push() zum Navigieren)
  • location: Der aktuelle Pfad (z.B. pathname, search)

const HelloComponent = ({match, history, location}) => (
  <div>
    <h1>Hello, {match.params.name}</h1>
    <small>The current path is: {location.pathname}</small>
    <button onClick={() => history.push('/greetings')}>Show Greetings</button>
  </div>
);

// ...
<Route path="/greet/:name" component={HelloComponent}/>

            

Links

Mit Link und NavLink können Links erzeugt werden

  • Mit to wird das Ziel angegeben
  • Gerendert wird per default ein a Element
  • URL wird entsprechend der History (Browser oder Hash) erzeugt
  • Mit activeClassName 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='/greetings'>Show all greetings</Link>
<NavLink to='/greeting/me' activeClassName="highlight">Greet me</NavLink>

            

Geschafft ;-)

Vielen Dank für Eure Teilnahme!

Kontakt: nils@nilshartmann.net