Falls noch nicht gemacht:
git clone https://github.com/nilshartmann/react-workshop.git
npm install
npm start
code/workspace
wechselnnpm start
Folien: Im geklonten Verzeichnis 2017_jax.html
Oliver Zeigermann / @DJCordhose
http://bit.ly/jax2017-react-workshop
Lust bekommen, React zu entwickeln?
Wir stellen ein :-)
EOS Technology Solutionsclass HelloMessage extends React.Component {
render() {
return <h1 className='title'>Hello, World!</h1>
}
}
ES6 Features werden vorgestellt, wo wir sie brauchen
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: ...
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
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}
`;
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();
}
}
// 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
npm start
in unserem Beispiel-ProjektNutze für die Übungen das workspace
-Verzeichnis in diesem Repository. Hier ist eine Tool-Chain mit Webpack und Babel vorbereitet
npm install
(auf oberster Ebene)
cd code/workspace
npm start
Jede React-Komponente braucht eine render
-Methode:
class GreetingDetail extends React.Component {
render() {
return (
<input onChange={event => this.updateModel(event.target.value)}
value={this.state.greeting} />
{this.state.greeting}, World
);
}
// ...
}
<div><input type="text"/></div>
<Counter label="Count" count={7} showValues={true} />
const title = 'Hello, World';
<h1>{title.toUpperCase()</h1>
class
-Attribut heißt className
:
<h1 className="title">...</h1>
const styles = { marginLeft: '10px', border: '1px solid red' };
<h1 style={styles}>...</h1>
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});
}
// ...
}
const name = 'Oma';
const person = {
// ES5: name: name
name
};
console.log(person.name); // Oma
// Person.js
class Person {
// ...
}
export default Person;
// Person.js
// in einer Zeile zusammengefasst
export default class Person {
// ...
}
// Programmer.js
import Person from './Person';
export default class Programmer extends Person {
// ...
}
// 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');
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);
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' />
this.state={}
this.state
this.setState()
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)} />
}
// ...
}
refs
übergeben werden
null
)
class HelloMessage extends React.Component {
render() {
return (
<input ref={input => this.input = input} />
<button
onClick={() => this.input.focus()}>
Focus
</button>
);
}
}
GreetingDetail
um
name
und greeting
im Zustand der Komponente setzen
https://facebook.github.io/react/docs/thinking-in-react.html
render
-Methode
import React from 'react';
export default function Greet(props) {
return (
{props.greeting}
{props.name}
);
}
// Verwendung:
<Greet name="Susi" greeting="Hello" />
// Mit Destructuring
export default function Greet({greeting, name}) {
return (
{greeting}
{name}
);
}
// Als Arrow Function
const Greet = ({greeting, name}) =>
{greeting}
{name}
}
export default Greet;
Array.map
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>);
Beispiel: Unsere Anwendung
class GreetingController extends React.Component {
render() {
const {greetings} = this.state;
return <div>
<GreetingMaster greetings={greetings}
onAdd={() => this.setState({mode: MODE_DETAIL})} />
</div>;
}
// ...
}
class GreetingController extends React.Component {
render() {
const greeting = { name: 'Klaus', greeting: 'Hello' };
return <GreetingDetail {...greeting} />
// entspricht:
// <GreetingDetail name='Klaus' greeting='Hello' />
}
}
prop-types
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
};
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() { . . . }
};
Für Komponenten als Funktionen
import PropTypes from 'prop-types';
function HelloMessage(text) { . . . }
HelloMessage.propTypes = {
text: PropTypes.string.isRequired
}
GreetingDetail
) und einen Master-View über eine Controller-Komponente zusammencode/material/2-hierarchy
in deinen src-Ordner
GreetingController
die render-Methode, so dass dein GreetingDetail
angezeigt wird, wenn der Benutzer den Add-Button klickt:
addGreeting
nutzt
GreetingDetail
brauchst du einen neuen Knopf, der mit dem neuen Gruß den Callback aufruft
GreetingDetail
verwenden, oder die Vorlage code/material/2-hierarchy/src/_GreetingDetail.js verwenden)
// 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(...)
const url = `${BACKEND_URL}${path}`;
return fetch(url)
.then(response => response.json())
.then(json => /* ... */)
.catch(ex => console.error('request failed', ex));
const promise = new Promise(resolve =>
setTimeout(
() => resolve('Result from promise'),
1000)
);
promise.then(value => console.log(value));
// Output after 1 second: Result from promise
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
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
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]
// 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})
componentDidMount
wird aufgerufen, wenn Komponente ins DOM gerendert wurdeclass 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(. . .)
;
}
}
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(. . .);
}
}
npm start
im Root-Verzeichnis gestartet werden. Er ist dann unter Port 7000 erreichbarcode/material/3-remote/backend.js
und code/material/3-remote/main.js
in deinen Workspacecode/material/3-remote/_GreetingController.js
als Template verwenden. Die notwendigen
Schritte sind dort als Kommentar markiert.loadFromServer
und saveToServer
aus dem backend
-Modul
GreetingController
zwei Methoden zum Laden und Speichern der Greetings hinzu
componentDidMount
)
onAdd
-Callback von GreetingDetail
soll die übergebene Greeting speichern und im Erfolgsfall
das gespeicherte Greeting in die Liste aller Greetings zufügen.
"Painless JavaScript Testing" (http://facebook.github.io/jest/)
Vollständige Test-Lösung, wird von Facebook für Testen von React verwendet:
// 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);
});
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] }
]
}
expect(obj).toMatchSnapshot()
vergleicht ein JSON-Objekt mit einer gespeicherten Datei:
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!
JavaScript Testing utilities for React (http://airbnb.io/enzyme/)
Bibliothek mit Funktionen zum Testen von React Komponenten
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);
});
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
nvd3 Pie Chart mit d3
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 Callback Funktionen
componentDidMount()
: Komponente wurde gerendert, Element sind im DOMcomponentWillReceiveProps(nextProps)
: An die Komponente wurden neue Properties übergeben. Die neuen Properties
werden als Parameter übergebenshouldComponentUpdate()
: Entscheidet, ob Komponete erneut gerendert werden soll (default: true
)componentWillUnmount()
: Wird aufgerufen, bevor Komponente aus dem DOM entfernt wird
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));
}
}
// ...
}
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();
}
}
code/material/5-third-party
in deinen src-Ordner
GreetingController
ein
Zusatzaufgabe: Ein zweites Mal klicken auf das Segment soll den Filter wieder löschen
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)
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
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><GreetingController/></Router>;
ReactDOM.render(app, document.getElementById(...));
path
wird der Pfad übergeben, für den die Route matchen sollcomponent
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 path="/"
render={() => {
return loggedIn ? <GreetingMaster greetings={initialGreetings} />
:
<LoginForm />
}
/>
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}/>
path
matcht immer
<Route path="/greeting/:greetingId"
component={<GreetingDisplay />} />
// in GreetingDisplay kann die greetingId über
// this.props.match.params.greetingId abgefragt werden
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(...));
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}/>
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='/greetings'>Show all greetings</Link>
<NavLink to='/greeting/me' activeClassName="highlight">Greet me</NavLink>
Kontakt: nils@nilshartmann.net