Fortgeschrittene React-Patterns

enterjs 2017

Nils Hartmann / @nilshartmann

Oliver Zeigermann / @DJCordhose

Folien: http://bit.ly/enterjs-advanced-react

Themen

  1. Optimierungen (I): Rendering
  2. Zustands-Management
  3. Integration mit 3rd-Party Bibliotheken
  4. Optimierungen (II): Build

Optimierungen (I): Rendering

Problem: React rendert oft zu viel

Beispiel: SVG Boxes

Eine einzelne Box mit D'n'D verschieben

SVG Boxes

Rendering aller Komponenten

Change Detection auf der Ebene des Virtual DOM

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

Change Detection auf der Ebene des Models

shouldComponentUpdate ist eine Lifecycle-Methode einer Komponente die diese Prozedur abkürzen kann

das alte Modell kann mit dem neuen verglichen werden

Das reduziert das Neu-Rendering auf genau die notwendigen Elemente

Macht nur Sinn, wenn der Check auf Modell-Änderung sehr billig ist, typischerweise ===

Die Box Component


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"/>;
  }
}
               

Naiver Code für Modifikation einer Box


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
    });
}
               

Kompletter Code

Immutable.js

Immutable persistent data collections for Javascript which increase efficiency and simplicity.

http://facebook.github.io/immutable-js/

  • Bietet List, Set, Map, etc.
  • Jede Modifikaton einer Collection erzeugt ein neues Collection Objekt
  • Kopiert nicht die komplette Collection, sondern effiziente Erzeugung mittels Persistent Data Structures

Mit 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
    });
}
               

Kompletter Code

Bibliotheken für Zustands-Management

Motivation

Komplexe Anwendung mit vielen interaktiven Abhängigkeit

Über Komponenten verteilter Zustand

Render Cycle in Pure React

Option: Redux

http://redux.js.org/

Option: MobX

https://mobx.js.org/

Redux vs MobX

Redux

  • ist die Mainstream-Lösung
  • Lässt sich sehr gut testen
  • Fehlersuche und Debugging erstaunlich einfach
  • Funktioniert auch in großen Anwendungen sehr gut
  • Dev-Tools erlauben Magisches

Allerdings: Lernkurve steil, gerade am Anfang wirklich viel Code

Redux ist für kleinere Anwendungen meistens Overkill

MobX erlaubt einen sehr einfachen Einstieg

Demo: Magische Redux Dev-Tools

Integration mit 3rd-Party Bibliotheken

3rd Party Libs?

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

Unser Beispiel: Verteilung der Grüße

nvd3 Pie Chart mit d3

Aufgabe: Einbetten des d3/nvd3 Pie Charts in eine React-Komponente

React Chart Komponente #1


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

    // called directly after first render
    componentDidMount() {
        // once rendered by react we create the nvd3 chart
        const chart = createNvd3Chart(this._chart, this.props.data);

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

    // ...
}
       

React Chart Komponent #2


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

    // if it returns false, component will not be rendered
    shouldComponentUpdate() {
        // once rendered react never renders again
        return false;
    }

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

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

Optimierungen (II): Build

Kudos an Webpack

Jeder kann ein Sponsor werden

https://opencollective.com/webpack#contributors

Produktionsbuild

Minifizieren, Optimieren und alle React-Warnungen herauskompilieren:


webpack --optimize-minimize --define process.env.NODE_ENV=\"'production'\"
            

Oder als Kurzform:


webpack -p
            

(Nutzt Uglify)

https://webpack.js.org/guides/production-build/

SourceMaps in eigene Datei schreiben (Webpack Konfiguration):


          devtool: 'source-map'
        

Build-Optimierungen

"Tree Shaking": Entfernt toten Code aus dem generierten JavaScript File
  • Reduziert die Größe der Ausgabedatei
  • Verfügbar in Webpack 2.x
  • Automatisch eingeschaltet, wenn mit -p aufgerufen
  • Arbeitet auf Basis statischer Code-Analyse der ES6 Imports und Exports
  • Anpassung in .babelrc:
    
    "presets": [["es2015", { "modules": false }]
                
  • https://webpack.js.org/guides/tree-shaking/

Code Splitting

Asynchrones Nachladen von Modulen

https://webpack.js.org/guides/code-splitting-async/

  • Erlaubt das dynamische Nachladen von Code-Teilen
  • So kann am Anfang für eine schnelle Ladezeit nur eine Minimalversion ausgespielt werden
  • Basiert auf dynamic import, der bereits in Stage 3 ist
  • Weitere Teile können unmittelbar oder erst nach Nutzerinteraktion nachgeladen werden

Nachladen von React-Komponenten

  • Erfordert die Darstellung von Platzhaltern, bis die eigentliche Komponente da ist
  • Wenn die Komponente da ist, muss die umschließende Komponente neu dargestellt werden
  • forceUpdate erzwingt ein Neuladen

class AsyncComponent extends React.Component {
  componentDidMount() {
    import(/* webpackChunkName: "component" */ './Component')
      .then(ComponentModule => {
        this.Component = ComponentModule.default;
        this.forceUpdate();
      });
  }

  render() {
    return this.Component ?
      <this.Component /> : <span>Loading...</span>;
  }
}
            

Fertige Lösung: React Loadable

Externals

Allgemeine Bibliotheken können von CDNs geladen werden und landen nicht im Webpack build

Vorteil: Caching (sowohl im Browser als auch in mittleren Schichten), mehrere parallele Requests


externals: {
    "react": "React",
    "react-dom": "ReactDOM",
    "prop-types": "PropTypes",
    "d3": "d3",
    "nvd3": "nv"
}
            

Dev-Build Größe: Ohne Optimierungen

Prod-Build Größe #1: Mit allen Abhängigkeiten

Prod-Build Größe #2: Ohne React

Prod-Build Größe #3: Ohne externe Abhängigkeiten (D3, NVD3)

Geschafft ;-)

Vielen Dank für Eure Aufmerksamkeit!

Fragen ?

Nils Hartmann / @nilshartmann

Oliver Zeigermann / @DJCordhose