Falls noch nicht gemacht:
git clone https://github.com/DJCordhose/react-workshop.git
npm install
npm start
code/workspace
wechselnnpm start
Folien: Im geklonten Verzeichnis 2017_ix.html
class 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 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: ""});
}
}
// 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
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
const schluessel = 'name';
const person = {
[schluessel]: 'Klaus'
};
console.log(person.name); // Klaus
let id = 1;
const personen = {
[`id_${id++}`]: 'Klaus',
[`id_${id++}`]: 'Susi'
};
console.log(personen); // Object {id_1: "Klaus", id_2: "Susi"}
// 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 name="name"
value={this.state.name}
onChange={e => this.updateModel(e)} />
}
// ...
}
ref
übergeben werden
null
)
class HelloMessage extends React.Component {
render() {
return (
<div>
<input ref={inputNode => this.inputNode = inputNode} />
<button
onClick={() => this.inputNode.focus()}>
Focus
</button>
</div>);
}
}
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';
function Greet(props) {
return (
<div>
<h1>{props.greeting}</h1>
<h2>{props.name}</h2>
</div>
);
}
// Verwendung:
<Greet name="Susi" greeting="Hello" />
// Mit Destructuring
function Greet({greeting, name}) {
return (
{greeting}
{name}
);
}
// Als Arrow Function
const Greet = ({greeting, name}) =>
{greeting}
{name}
}
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 (
<GreetingMaster greetings={greetings}
onAdd={() => this.setState({mode: MODE_DETAIL})}
);
}
// ...
}
class GreetingController extends React.Component {
const greeting = { name: 'Klaus', greeting: 'Hello' };
render() {
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
}),
onSave: 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
}),
onSave: 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)
Browser-API zum Laden und Speichern von Daten
fetch(url, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(json => /* ... */)
.catch(ex => console.error('request failed', ex));
const url = `${BACKEND_URL}${path}`;
return fetch(url)
.then(response => response.json())
.then(json => /* ... */)
.catch(ex => console.error('request failed', ex));
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/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('/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 onSave={greeting => this.saveGreeting(greeting)} />
...
}
saveGreeting(greetingToBeSaved) {
fetch('/greetings', {
method: 'POST',
headers: ...,
body: JSON.stringify(greetingToBeSaved)
})
.then(response => response.json())
.then(json => ...)
.catch(. . .);
}
}
Entwickle auf Basis von fetch eine Version des GreetingControllers, der die Daten auf dem Server laden und dort wieder speichern kann
Der Server ist bereits vorgegeben und kann mit npm start
im Root-Verzeichnis gestartet werden. Er ist dann unter Port 7000 erreichbar
Kopiere code/material/3-remote/GreetingController.js
in deinen Arbeitsbereich
Die Serverzugriffe sollen in loadGreetings
und saveGreeting
erfolgen
Dort sind bereits entsprechende TODOs für dich eingetragen
Es gibt eine große Anzahl von sehr praktischen JavaScript-Bibliotheken
Viele davon sind aber nicht als React-Komponenten entwickelt worden
Beispiele
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));
componentDidMount()
: Komponente wurde gerendert, Elemente sind im DOM (einmalig)componentWillReceiveProps(nextProps)
: An die Komponente wurden neue Properties übergeben. Die neuen Properties
werden als Parameter übergebenshouldComponentUpdate()
: Entscheidet, ob Komponente erneut gerendert werden soll (default: true
)componentWillUnmount()
: Wird aufgerufen, bevor Komponente aus dem DOM entfernt wird (einmalig)
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>
}
shouldComponentUpdate() {
// (2) once rendered react never renders again
return false;
}
componentWillReceiveProps(nextProps) {
const {data} = nextProps;
// (3) we still get updates of properties making it reactive
updateNvd3Chart(this._chart, data);
}
// ...
}
class Chart extends React.Component {
// ...
componentDidMount() {
const {data, onSegmentSelected} = this.props;
// (4) once rendered by react we create the nvd3 chart
const chart = createNvd3Chart(this._chart, data);
// (5) we delegate the label of clicked segment
// back to parent component
if (onSegmentSelected) {
chart.pie.dispatch.on("elementClick",
e => onSegmentSelected(e.data.label));
}
}
// (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
Zustand, der über viele Komponenten verteilt ist, macht Programme kompliziert
Gemeinsame Nutzung von State in unterschiedlichen Komponenten-Hierarchien ist schwierig
Zustandmanagement und UI-Handler werden aus React-Komponenten extrahiert
Besseres SoC
Schreibe die erste Hello-World Anwendung auf Redux um
Im ersten Schritt tust du dies im Plenum mit den Workshop-Leitern
Dabei lernst du anhand des Live-Codings alle Bestandteile der Redux-Architektur kennen
Dazu erstellen wir zusammen eine Übersicht über alle notwendigen Teile spezifisch für die Hello-World Anwendung
Im zweiten Schritt baust du die Anwendung unter code/material/hello-world
selbst noch einmal um
export const setFilter = filter => {
return {
type: SET_FILTER,
filter
};
}
export const loadGreeting = greetingId => dispatch => {
fetch(BACKEND_URL+'/'+greetingId)
.then(response => response.json())
.then(greetings => dispatch({
type: SET_GREETINGS,
greetings
});
};
Action-Creators sind die einzigen Teile einer Redux-Anwendung, die asynchrone Operationen ausführen dürfen
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { rootReducer } from './reducers';
// http://redux.js.org/docs/api/createStore.html
const store = createStore(
rootReducer // reducer
);
ReactDOM.render(
<Provider store={store}>
<GreetingController />
</Provider>,
mountNode
);
import { applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(
rootReducer, // reducer
applyMiddleware(thunk) // middleware as enhancer
);
import {combineReducers} from 'redux';
// http://redux.js.org/docs/api/combineReducers.html
export const rootReducer = combineReducers({
greetings, // updates greeting partial state
filter,
mode
});
const mode = (state = MODE_MASTER, action) => {
switch (action.type) {
case SET_MODE:
return action.mode;
default:
return state;
}
};
import { connect } from 'react-redux';
import * as actions from './actions';
export default connect(
state => ({
mode: state.mode
// ...
}),
actions
)(GreetingController);
class GreetingController extends React.Component {
render() {
// state from store
const {aggregatedGreetings, greetings, mode} = this.props;
// action creators bound to dispatch from store
const {setMode, saveGreeting, setFilter} = this.props;
// ...
}
}
export const selectGreetings = ({greetings}) => greetings;
export default connect(
state => ({
greetings: selectGreetings(state),
// ...
}),
// ...
)(GreetingController);
code/material/6-redux
in deinem Source-Ordner kopieren
combineReducers
hinzu
GreetingController
in die Chart-Komponente in der du auf das
Filter-Event reagierst
https://facebook.github.io/react/docs/optimizing-performance.html
Eine einzelne Box mit D'n'D verschieben
bei jeder Bewegung der Maus werden alle Boxen neu gerendert
Nicht direkt im DOM, sondern in einer leichtgewichtigen Datenstruktur (Virtual DOM)
Änderungen im echten DOM werden aus Änderungen im Virtual DOM errechnet
shouldComponentUpdate ist eine Lifecycle-Methode einer Komponente die diese Prozedur abkürzen kann
das alte Modell kann mit dem neuen verglichen werden
immutable data structures (immutable.js) können den Vergleich beschleunigen
class Box extends React.Component {
shouldComponentUpdate(nextProps) {
// simple check: every change to a box creates a new object
const changed = this.props.box !== nextProps.box;
return changed;
}
render() {
const {box} = this.props;
return <rect data-id={box.id} x={box.x} y={box.y}
width="10" height="10"
stroke="black" fill="transparent" strokeWidth="1"/>;
}
}
updateBox(id, x, y) {
const {boxes} = this.state;
const modifiedBox = {
id,
x,
y
};
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/
// Reference/Global_Objects/Array/slice
const boxesBefore = boxes.slice(0, id);
const boxesAfter = boxes.slice(id + 1);
const modifiedBoxes = [...boxesBefore, modifiedBox, ...boxesAfter];
this.setState({
boxes: modifiedBoxes
});
}
Immutable persistent data collections for Javascript which increase efficiency and simplicity.
http://facebook.github.io/immutable-js/
import {List} from 'immutable';
this.state = {
boxes: List(boxes)
};
updateBox(id, x, y) {
const {boxes} = this.state;
const modifiedBox = {
id,
x,
y
};
const modifiedBoxes = boxes.set(id, modifiedBox);
this.setState({
boxes: modifiedBoxes
});
}
Minifizieren, Optimieren und alle React-Warnungen herauskompilieren:
webpack --optimize-minimize --define process.env.NODE_ENV=\"'production'\"
Oder als Kurzform:
webpack -p
https://webpack.js.org/guides/production-build/
(NODE_ENV="production"
wird nicht in der webpack.config
gesetzt!)
-p
aufgerufen.babelrc
:
"presets": [["es2015", { "modules": false }]
https://webpack.js.org/guides/code-splitting-async/
npm install babel-plugin-syntax-dynamic-import --save
// .babelrc
"plugins": ["syntax-dynamic-import", ...]
// calculator.js
export default function calculator(a, b) {
return a+b;
}
import('./calculator')
.then(calculatorModule => {
// import calculator from './calculator';
const calculator => calculatorModule.default
console.log(calculator(7, 8));
});
class AsyncComponent extends React.Component {
componentDidMount() {
import('./Component').then(ComponentModule => {
this.Component = ComponentModule.default;
this.forceUpdate();
});
}
render() {
return this.Component ?
<this.Component /> :
<span>Loading...</span>;
}
}
Baue deine Anwendung so um, dass die Detail-Ansicht erst bei Bedarf nachgeladen wird
Beginne mit der Version in code/schritte/redux/8-redux-prod-build
, die bereits ein Produktionsbuild bietet