React-Workshop

React-Workshop für Einsteiger

Avatar von Christopher Stock

In diesem React-Workshop möchte ich einen schnellen und praktischen Einstieg in die Entwicklung mit dem JavaScript-Framework bieten und alle Bestandteile vorstellen, die man benötigt, um eigene Anwendungen mit dem erfolgreichen und weit verbreiteten System zu entwickeln.

Hierfür erstellen wir zusammen eine kleine Web-Applikation, die den Benutzer eine ToDo-Liste verwalten lässt, in der er neue Tasks anlegen sowie bestehende Tasks löschen und umpriorisieren kann. Im Laufe der Umsetzung werden alle wichtigen Grundprinzipien von React Schritt für Schritt vorgestellt und in die Praxis umgesetzt.

Was ist React?

React-Logo

Bei React handelt es sich um ein von Facebook ins Leben gerufenes Web-Framework für die Entwicklung von Single-Page-Webanwendungen. Die einzelnen Bestandteile unserer Anwendung werden unter React konsequent durch die Realisierung unabhängiger und modularer Komponenten konzeptioniert.

Der Benutzer erfährt hierdurch eine sehr responsive und gesamtheitliche User-Experience, die mit Hilfe des Frameworks sehr einfach und gut strukturiert umgesetzt werden kann.

Welche Kenntnisse werden für den React-Workshop vorausgesetzt?

Da es sich um einen React-Workshop für Einsteiger handelt, wird bewusst auf den Einsatz von weiterführenden Technologien wie npm, TypeScript und Webpack, die eine komfortablere und professionellere Entwicklung unserer Beispielanwendung ermöglichen, verzichtet.

Vorrausgesetzt werden lediglich Grundkenntnisse in den Web-Technologien HTML, CSS und JavaScript ES6.

Hinweis

Aus Platzgründen sind einige der Codebeispiele in diesem React-Workshop minimiert, meist werden nur die Änderungen dargestellt – ein Klick auf den grauen Balken unter den Beispielen genügt jedoch, um den vollständigen Code mit den hervorgehobenen Änderungen anzuzeigen.

1. Das Grundgerüst

Das Grundgerüst unserer Anwendung besteht aus einer HTML- und einer CSS-Datei. Zudem wollen wir die React-Bibliothek in unsere HTML-Datei einbinden und eine JavaScript-Datei als Einstiegspunkt für unsere Anwendung festlegen. Funktionalitäten aus dem React-Framework selbst werden in diesem Schritt aber noch nicht eingesetzt.

1.1. Erstellen der HTML-Datei

In einem beliebigen und leeren Ordner unserer Wahl erstellen wir uns eine neue Datei index.html mit dem folgenden Inhalt:

<!DOCTYPE html>
<html>
    <head>

        <!-- default encoding -->
        <meta charset="UTF-8" />

        <!-- external stylesheet and page icon -->
        <link rel="stylesheet" href="css/styles.css">

    </head>
    <body>
        <div id="mainContainer"></div>
    </body>
</html>

1.2. Erstellen der Stylesheet-Datei

Um unserer Anwendung eine ansprechende Optik zu verleihen, erstellen wir die im Kopf unserer HTML-Datei referenzierte Stylesheet-Datei css/styles.css mit dem Inhalt des folgenden Listings.

body
{
    background:         #c0c0c0;
    color:              #3d3d3d;
    margin:             50px;
}

*
{
    font-size:          15px;
    font-family:        sans-serif;
    margin:             0;
    padding:            0;
    border:             0;
    border-radius:      5px;
    transition:         all 0.3s ease-in;
    outline:            none;
}

h1
{
    font-size:          35px;
}

div#mainContainer
{
    background:         #ffffff;
    text-align:         center;
    margin:             0 auto 0 auto;
    padding:            15px;
    width:              640px;
    height:             auto;
}

input#newTask
{
    width:              400px;
    height:             40px;
    margin-top:         15px;
    text-align:         center;
}

input#submitButton,
button#submitButton
{
    height:             40px;
    margin-top:         15px;
}

@keyframes fadeIn { from { opacity: 0.0; } to { opacity: 1.0; } }

ul#taskList
{
    list-style-type:    none;
}

ul#taskList li
{
    background:         #a5e2bf;
    animation:          fadeIn 1.0s ease-in;
    height:             40px;
    line-height:        40px;
    margin-top:         15px;
}

ul#taskList button
{
    float:              right;
    line-height:        30px;
    margin:             5px;
}

.input
{
    background:         #e2e2e2;
}

.input:focus
{
    background:         #cacaca;
}

.input.error,
.input.error:focus
{
    background:         #ff7086;
}

.button
{
    background:         #8c8c8c;
    color:              #ffffff;
    padding:            0 10px 0 10px;
}

.button:hover
{
    background:         #a8a8a8;
}

.button:disabled
{
    background:         #c5c5c5;
}

1.3. Einbinden der React-Bibliothek

Die zum Verwenden des React-Frameworks erforderlichen Klassen können direkt vom Facebook-Server in unsere Webseite eingebunden werden. Hierfür müssen die folgenden Script-Tags zum Kopf unserer index.html-Datei hinzugefügt werden:

    <!-- library sources -->
    
    
    
<!DOCTYPE html>
<html>
    <head>

        <!-- default encoding -->
        <meta charset="UTF-8" />

        <!-- external stylesheet and page icon -->
        <link rel="stylesheet" href="css/styles.css">

        <!-- library sources -->
        
        
        

    </head>
    <body>
        <div id="mainContainer"></div>
    </body>
</html>

Hinweis

Um auch offline an unserer Webanwendung arbeiten zu können, besteht die Möglichkeit, diese drei Bibliotheksdateien herunterzuladen und lokal zu referenzieren. PHPStorm oder IntelliJ bieten hierfür beispielsweise einen Quick Fix an.

1.4. Erstellen und Einbinden unserer ersten JavaScript-Quelldatei

Unser erstes eigenes Skript legen wir nun unter js/index.jsx an. Es handelt sich bei diesem Dateiformat nicht um eine reguläre JavaScript-Datei sondern um eine von React ins Leben gerufene Erweiterung, der JavaScript Syntax Extension (JSX). Diese Skripte verwenden den Mime-Type text/jsx und erfordern zum Betrieb die im letzten Schritt eingebundene Bibliothek JSXTransformer. Mit Hilfe dieser an XML angelehnten Template-Sprache steht optional eine Syntax für die Deklaration von React-Komponenten zur Verfügung, die es erlaubt, Javascript-Logik, HTML und CSS in eine React-Komponente zu kapseln und modular innerhalb unserer Web-Applikation einzusetzen.

Unsere erste Skript-Datei js/index.jsx dient als Einstiegspunkt in unsere Applikation und hat vorerst die Aufgabe, den Titel unserer Webseite zu setzen und ihn in der Enwticklerkonsole auszugeben:

// specify the application title
const APPLICATION_TITLE = "React Task List";

// acclaim debug console and set page title
console.log(     APPLICATION_TITLE );
document.title = APPLICATION_TITLE;

Damit dieser JavaScript-Code auch ausgeführt wird, müssen wir diese neu erstellte Quelldatei natürlich noch in den Kopf unserer Webseite in der index.html einbinden:

        <!-- custom sources -->
        <script src="js/index.jsx" type="text/jsx"></script>
<!DOCTYPE html>
<html>
    <head>

        <!-- default encoding -->
        <meta charset="UTF-8" />

        <!-- external stylesheet -->
        <link rel="stylesheet" href="css/styles.css">

        <!-- library sources -->
        
        
        

        <!-- custom sources -->
        <script src="js/index.jsx" type="text/jsx"></script>

    </head>
    <body>
        <div id="mainContainer"></div>
    </body>
</html>

Die index.html-Datei können wir nun in einem Browser unserer Wahl öffnen. Es sollte der Titel der Webseite zu sehen sein. Zudem können wir in den Developer-Tools unseres Browsers diesen Titel als Ausgabe in der Konsole sehen. Im Körper der Seite wird bisher lediglich unser leeres mainContainer-Div angezeigt.

1.5. Troubleshooting

Sofern wir die Datei über das Dateisystem geöffnet haben, kann es in manchen Browsern vorkommen, dass an dieser Stelle Fehler wie der Folgende in der Konsole erscheinen:

Failed to load file:///C:/Users/stock/workspaces/JavaScript/ReactBasics/js/component/App.jsx: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

Das ist eine Sicherheitseinstellung des Browsers, sofern Skriptdateien über unterschiedliche Protokolle geladen werden. Das Problem kann umgangen werden, indem ein anderer Browser, beispielsweise Mozilla Firefox, verwendet wird oder indem das Projekt auf einem Webserver abgelegt und darüber geöffnet wird.

Hinweis

Entwicklungsumgebungen wie PHPStorm oder IntelliJ bieten einen integrierten Webserver, auf dem unsere Webseite ausgeführt werden kann. Hierfür muss im Project-Fenster einfach auf die index.html rechtsgeklickt und der Kontextmenüpunkt Open in Browser genutzt werden.

2. React Components

Um eine eigene React–Komponente zu erstellen, muss die Klasse React.Component erweitert und deren nicht-statische Methode render() überschrieben werden. Diese Methode gibt das Stück HTML-Code in Form eines JSX-Objektes zurück, die diese Komponente verwalten soll.

Um eine Komponente in unser DOM einzuhängen, wird die statische Methode ReactDOM.render() benötigt. Zur Verdeutlichung dieser Funktionsweise wollen wir eine einfache React-Komponente erstellen, die vorerst lediglich den Titel unserer Applikation konfektioniert und diese Komponente anschließend in unser DOM einhängen.

Die Komponente App soll in der Datei js/component/App.jsx definiert werden:

/**
/**
*   The entire application component.
*/
class App extends React.Component
{
    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        return <div>

            { /* title */ }
            <h1 id="appTitle">Static App Title</h1>

        </div>;
    }
}

Die Schreibweise zur Definition von JSX-Elementen ist etwas gewöhnungsbedürftig, geht aber nach einiger Zeit leicht von der Hand. Auf die Verwendung von Code innerhalb der geschweiften Klammern wird zu einem späteren Zeitpunkt weiter eingegangen.

Da die render()-Methode nur ein einzelnes Tag zurückgeben darf, schachteln wir unsere Elemente einfach in einem umfassenden div. Somit können wir hier im Laufe unseres Workshops noch weitere Bestandteile unserer App hinzufügen.

Damit unsere Webseite mit unserer Komponente arbeiten kann, müssen wir deren JavaScript-Datei noch zum Kopf unserer Webseite index.html hinzufügen:

        <script src="js/component/App.jsx" type="text/jsx"></script>
<!DOCTYPE html>
<html>
    <head>

        <!-- default encoding -->
        <meta charset="UTF-8" />

        <!-- external stylesheet -->
        <link rel="stylesheet" href="css/styles.css">

        <!-- library sources -->
        
        
        

        <!-- custom sources -->
        <script src="js/component/App.jsx" type="text/jsx"></script>
        <script src="js/index.jsx"         type="text/jsx"></script>

    </head>
    <body>
        <div id="mainContainer"></div>
    </body>
</html>

Im Anschluss können wir unsere erste React-Komponente in unserer Datei js/index.jsx in das DOM einhängen. Das machen wir über das einzige div-Tag unserer HTML-Datei, das wir über die id mainContainer referenzieren können:

// reference the main container
let mainContainer = document.getElementById( "mainContainer" );

// render the App component into the main container
ReactDOM.render(
    <App />,
    mainContainer
);
// specify the application title
const APPLICATION_TITLE = "React Task List";

// acclaim debug console and set page title
console.log(     APPLICATION_TITLE );
document.title = APPLICATION_TITLE;

// reference the main container
let mainContainer = document.getElementById( "mainContainer" );

// render the App component into the main container
ReactDOM.render(
    <App />,
    mainContainer
);

Damit React die Namen unserer eigenen Komponenten von denen der Standard-HTML-Tags abgrenzen kann, ist es obligatorisch, dass wir all unsere Komponenten-Klassen mit einem großen Buchstaben beginnen lassen.

Beim Aktualisieren unserer Webseite wird der HTML-Teil unserer App-Komponente in den mainContainer gerendert und somit das h1-Element mit dem statischen Text auf unserer Webseite angezeigt:

3. Properties

Bei Properties handelt es sich um Eigenschaften, die in die Komponente hineingereicht werden und auf die auch ausschließlich die Komponente selbst Zugriff hat. Somit stellen sie eine klar definierte Schnittstelle nach außen dar, über die beliebige Werte, Callbacks, Arrays oder Objekte zur Initialisierung der Komponente angegeben werden können.

Alle Properties werden in Form von Attributen an das Tag unserer Komponente übergeben und stehen der Komponente innerhalb eines Objekts in dem nicht-statischen Feld props der Klasse React.Component als unveränderbare Werte zur Verfügung.

Um die Funktionsweise zu verdeutlichen, wollen wir den Titel unserer Applikation an unsere App-Komponente übergeben und in deren render()-Methode in das h1-Tag einsetzen.

In unserer Datei js/index.jsx können wir dem App-Tag ein neues Attribut mit dem selbstgewählten Namen title übergeben:

ReactDOM.render(
    <App
        title={ APPLICATION_TITLE }
    />,
    mainContainer
);
// specify the application title
const APPLICATION_TITLE = "React Task List";

// acclaim debug console and set page title
console.log(     APPLICATION_TITLE );
document.title = APPLICATION_TITLE;

// reference the main container
let mainContainer = document.getElementById( "mainContainer" );

// render the App component into the main container
ReactDOM.render(
    <App
        title={ APPLICATION_TITLE }
    />,
    mainContainer
);

Unsere App-Komponente in der js/component/App.jsx kann nun auf diesen Wert über das nicht-statische Feld props lesend zugreifen und somit den übergebenen Titel in unser h1-Tag einsetzen:

            <h1 id="appTitle">{ this.props.title }</h1>
/**
*   The entire application component.
*/
class App extends React.Component
{
    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        return <div>

            { /* title */ }
            <h1 id="appTitle">{ this.props.title }</h1>

        </div>;
    }
}

Wie in den letzten beiden Änderungen angewandt, kann innerhalb der JSX-Syntax auf dynamische Werte mittels geschweifter Klammern zugegriffen werden. Innerhalb dieser Klammern ist der Einsatz einer Variablen, der Aufruf einer Funktion oder das Schreiben eines Blockkommentars möglich. Zudem bleibet bei dieser Schreibweise der Datentyp bei der Übergabe an die Komponente erhalten. Kontrollstrukturen die über den Einsatz des ternären Operators hinausgehen sowie runde Klammern sind hier allerdings nicht erlaubt.

4. State

Das State-System bietet unserer React-Komponente einen Mechanismus, mit der veränderbare Werte innerhalb der Komponente gespeichert werden können.

Das nicht-statische Feld state der Klasse React.Component beinhaltet ein Objekt, in dem beliebige Status-Variablen definiert werden können. Genau wie beim Feld props kann auf das Feld state ausschließlich von der React-Komponente selbst zugegriffen werden.

Im Gegensatz zu den Properties können die Werte des States allerdings verändert werden. Das funktioniert ausschließlich über den Aufruf der nicht-statischen Funktion setState() und bewirkt, dass die render()-Methode der Komponente erneut aufgerufen und somit der entsprechende HTML-Teil vom React-System aktualisiert wird.

4.1. Stateful Components

Sobald eine Komponente den beschriebenen State-Mechanismus verwendet, spricht man von einer stateful Component – unsere App-Komponente hatte bisher nur als eine stateless Component fungiert.

In diesem Schritt wollen wir unsere App-Komponente in eine stateful Component umwandeln. Hierfür wollen wir alle aktuell in unserer Anwendung vorhandenen Task-Items in einem String-Array festhalten und dieses über das State-System verwalten.

Da wir unser State-Objekt initialisieren wollen und der Konstruktor der einzige Ort ist, an dem wir das nicht-statische Feld state direkt zuweisen können, überschreiben wir den Konstruktor unserer App-Komponente und setzen dort den initialen State. Testweise können wir in unserer Task-Liste ein paar hardgecodete Task-Items definieren.

Der Konstruktor der Klasse React.Component bekommt alle in die Komponente hineingereichten Properties in dem Parameter props übergeben. Somit besteht hier auch die Möglichkeit, übergebene Property-Werte initial an die State-Variable zu übergeben.

Die erforderliche Erweiterung an der js/component/App.jsx sieht folgendermaßen aus:

    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            taskList: [
                "Milch kaufen",
                "Brownies backen",
                "Wäsche waschen",
                "Workshop vorbereiten",
            ],
        }
    }
/**
*   The entire application component.
*   This is an example for a stateful component.
*/
class App extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            taskList: [
                "Milch kaufen",
                "Brownies backen",
                "Wäsche waschen",
                "Workshop vorbereiten",
            ],
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        return <div>

            { /* title */ }
            <h1 id="appTitle">{ this.props.title }</h1>

        </div>;
    }
}

Damit wir diese Task-Items nun auch angezeigt bekommen, müssen wir die render()-Methode unserer App-Komponente erweitern. Der unmittelbare Einsatz einer for-Schleife innerhalb der geschweiften Klammer ist leider nicht möglich, allerdings speichern wir die JSX-Elemente in einem Array und zeigen es innerhalb der geschweiften Klammern an. Zu einem späteren Zeitpunkt werden wir diese Erstellung in eine separate Funktion auslagern.

        // create items array with all task list items
        let items = [];
        for ( let index = 0; index < this.state.taskList.length; ++index )
        {
            items.push(
                <li key={ index }>{ this.state.taskList[ index ] }</li>
            );
        }
            { /* task list */ }
            <ul id="taskList">
            {
                items
            }
            </ul>

Damit React die einzelnen HTML-Elemente performanter im DOM identifizieren und rendern kann, ist es innerhalb einer Komponente erforderlich, gleichen Kindelementen auf der selben Ebene einen eindeutigen key mittels des gleichnamigen Attributs zuzuweisen. Hier noch einmal die App.jsx mit allen Änderungen.

/**
*   The entire application component.
*   This is an example for a stateful component.
*/
class App extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            taskList: [
                "Milch kaufen",
                "Brownies backen",
                "Wäsche waschen",
                "Workshop vorbereiten",
            ],
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        // create items array with all task list items
        let items = [];
        for ( let index = 0; index < this.state.taskList.length; ++index )
        {
            items.push(
                <li key={ index }>{ this.state.taskList[ index ] }</li>
            );
        }

        return <div>

            { /* title */ }
            <h1 id="appTitle">{ this.props.title }</h1>

            { /* task list */ }
            <ul id="taskList">
            {
                items
            }
            </ul>

        </div>;
    }
}

Im Anschluss können wir die statisch erstellten Task-Items auf unserer Webseite sehen:

Die render()-Methode unserer Komponente wird aktuell nur einmalig, nämlich beim initialen Rendern durch das Einbinden der Komponente in das DOM, durchlaufen. Um eine Änderung des States und den somit wiederholten Durchlauf der render()-Methode herbeizuführen, wollen wir im ersten Schritt einen neuen Button direkt in die Komponente einfügen, der beim Anklicken ein neues Task-Item mit dem aktuellen Zeitstempel hinzufügt:

            { /* new task button */ }
            <button id="submitButton" className="button" onClick={
                () => this.createTask(
                    "New Task on " + "[" + new Date().toISOString() + "]"
                )
            }>
                Create Task
            </button>
    /**
    *   Creates a new task in the TaskList component.
    *
    *   @param {string} taskName The name of the task to create.
    */
    createTask( taskName )
    {
        console.log( "App.createTask( " + taskName + " ) being invoked" );

        // copy original array and append new task
        let newTaskList = this.state.taskList.slice();
        newTaskList.push( taskName );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }
/**
*   The entire application component.
*   This is an example for a stateful component.
*/
class App extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            taskList: [
                "Milch kaufen",
                "Brownies backen",
                "Wäsche waschen",
                "Workshop vorbereiten",
            ],
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        // create items array with all task list items
        let items = [];
        for ( let index = 0; index < this.state.taskList.length; ++index )
        {
            items.push(
                <li key={ index }>{ this.state.taskList[ index ] }</li>
            );
        }

        return <div>

            { /* title */ }
            <h1 id="appTitle">{ this.props.title }</h1>

            { /* new task button */ }
            <button id="submitButton" className="button" onClick={
                () => this.createTask(
                    "New Task on " + "[" + new Date().toISOString() + "]"
                )
            }>
                Create Task
            </button>

            { /* task list */ }
            <ul id="taskList">
            {
                items
            }
            </ul>

        </div>;
    }

    /**
    *   Creates a new task in the TaskList component.
    *
    *   @param {string} taskName The name of the task to create.
    */
    createTask( taskName )
    {
        console.log( "App.createTask( " + taskName + " ) being invoked" );

        // copy original array and append new task
        let newTaskList = this.state.taskList.slice();
        newTaskList.push( taskName );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }
}

Die Verwendung der JavaScript Arrow-Syntax im onClick-Attribut unseres neu hinzugefügten Buttons ist obligatorisch, da ansonsten die Referenz zu this verloren gehen würde.

Am Ende der nicht-statischen Methode createTask() wird nun der State durch einen Aufruf von setState neu zugewiesen, wodurch die Komponente vom React-System neu gerendert und somit der neu erstellte Task in unserer Task-Liste angezeigt wird.

Wird im State mit Objekten oder Arrays gearbeitet, so muss bei einer Veränderung dieser Elemente immer eine Kopie zugewiesen werden, da das React-System diese andernfalls nicht als eine Veränderung des States interpretiert. Daher erstellen wir vor dem Neuzuweisen eine Kopie unseres Arrays taskList mit Hilfe der Methode Array.slice().

Hinweis

Dies ist eine häufige Fehlerquelle! So mancher Entwickler hat bereits Stunden damit verbracht, herauszufinden, warum eine Komponente vom React-System nicht neu gerendert wird obwohl sich offensichtlich deren State-Werte verändern. Das Problem kann von vornherein ausgeschlossen werden, indem Array und Objekte innerhalb des States bei deren Veränderung nie erneut zugewiesen werden sondern für diese immer eine neue Instanz erzeugt wird.

Beim Einsatz erweiterter Techniken wie Redux ist diese Vorgehensweise zwingend erforderlich. Dafür ermöglicht der konsequente Einsatz dieser Technik die schnelle und problemlose Realisierung von statebezogenen Funktionalitäten wie beispielsweise einer Undo- oder Redo-Funktion.

4.2. Stateless Components

Bei den stateless Components handelt es sich simplerweise um Komponenten, die keinen eigenen State verwalten. Somit haben sie lediglich die Aufgabe, mit den in sie hineingereichten Properties zu arbeiten.

Zur Realisierung einer stateless Component wollen wir die Anzeige unserer Task-Liste in eine separate Komponente TaskList auslagern. Das in der State-Variablen unserer App-Komponente gespeicherte Array können wir an die neue Komponente TaskList dann als Property übergeben.

Zuerst müssen wir für unsere neue Komponente ein neues Skript in unsere index.html einbinden:

        <script src="js/component/TaskList.jsx" type="text/jsx"></script>
<!DOCTYPE html>
<html>
    <head>

        <!-- default encoding -->
        <meta charset="UTF-8" />

        <!-- external stylesheet -->
        <link rel="stylesheet" href="css/styles.css">

        <!-- library sources -->
        
        
        

        <!-- custom sources -->
        <script src="js/component/TaskList.jsx" type="text/jsx"></script>
        <script src="js/component/App.jsx"      type="text/jsx"></script>
        <script src="js/index.jsx"              type="text/jsx"></script>

    </head>
    <body>
        <div id="mainContainer"></div>
    </body>
</html>

Die Realisierung der neuen Komponente TaskList beinhaltet keine neuen Erkenntnisse. Wir wollen hier aber die Erstellung der HTML-Elemente für die Task-Liste in eine separate, nicht-statische Methode auslagern, um die Lesbarkeit unseres Quellcodes zu erhöhen.

Außerdem wrappen wir die Beschreibung unseres Items in ein div-Tag, da wir zu einem späteren Zeitpunkt hier noch weitere Elemente hinzufügen wollen.

Der Quellcode der neuen Datei js/component/TaskList.jsx sieht nun also folgendermaßen aus:

/**
*   Represents the TaskList component.
*   This is an example for a stateless component.
*/
class TaskList extends React.Component
{
    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "TaskList.render() being invoked" );

        return <ul id="taskList">
        {
            this.createItems()
        }
        </ul>;
    }

    /**
    *   Creates and returns all items for the task list.
    *
    *   @return {JSXTransformer[]} The rendered JSX elements.
    */
    createItems()
    {
        let items = [];

        // browse all task list items
        for ( let index = 0; index < this.props.taskList.length; ++index )
        {
            items.push(
                <li key={ index }>

                    { /* The item description */ }
                    { this.props.taskList[ index ] }

                </li>
            );
        }

        return items;
    }
}

Das Einbinden der neu erstellten stateless Component TaskList erfolgt durch Hinzufügen des gleichnamigen Tags in die render()-Methode unserer App-Komponente. Im Gegenzug kann hier die Erstellung des lokalen Arrays mit den Task-Items entfernt werden, wodurch ebenfalls die Lesbarkeit unserer js/component/App.jsx erhöht wird:

            { /* task list */ }
            <TaskList
                taskList={ this.state.taskList }
            />
/**
*   The entire application component.
*   This is an example for a stateful component.
*/
class App extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            taskList: [
                "Milch kaufen",
                "Brownies backen",
                "Wäsche waschen",
                "Workshop vorbereiten",
            ],
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        return <div>

            { /* title */ }
            <h1 id="appTitle">{ this.props.title }</h1>

            { /* new task button */ }
            <button id="submitButton" className="button" onClick={
                () => this.createTask(
                    "New Task on "
                    + "["
                    + new Date().toISOString()
                    + "]"
                )
            }>
                Create Task
            </button>

            { /* task list */ }
            <TaskList
                taskList={ this.state.taskList }
            />

        </div>;
    }

    /**
    *   Creates a new task in the TaskList component.
    *
    *   @param {string} taskName The name of the task to create.
    */
    createTask( taskName )
    {
        console.log( "App.createTask( " + taskName + " ) being invoked" );

        // copy original array and append new task
        let newTaskList = this.state.taskList.slice();
        newTaskList.push( taskName );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }
}

5. Data flow

Eines der wichtigsten Konzepte von React ist der unidirektionale Datenfluss. Die beiden Komponenten App und TaskList sind hierarchisch aufgebaut und der Datenfluß findet immer „Top-Down“ von der Komponente App in Richtung TaskList statt.

React wurde zudem so designed, dass es nicht vorgesehen ist, eine Referenz einer Komponente an eine andere Komponente zu übergeben. Lediglich die Properties können verwendet werden, um Daten an eine, innerhalb des Rendering-Prozesses eingebundene, Komponente zu übergeben.

Zudem sind die Properties und auch der State so ausgelegt, dass sie nur innerhalb der eigenen Komponente verwendet werden können. Um eine Komponente mit einer anderen Property auszustatten, muss sie also von außen neu gerendert werden.

Somit lassen sich durch den Einsatz des React-Frameworks Web-Anwendungen sehr modular und gut gekapselt designen und realisieren.

Oftmals ist es natürlich trotzdem erforderlich, dass die eingebundene Komponente mit der Parent-Komponente kommunizieren muss. In unserem Fall wollen wir im nächsten Schritt hinter den einzelnen Task-Items unserer TaskList-Komponente Buttons anzeigen, mit der jedes Task-Item gelöscht oder umsortiert werden kann. Da diese Task-Items allerdings innerhalb der App-Komponente verwaltet werden, scheint diese Anforderung erstmal schwierig umzusetzen.

Umsetzen lässt sich dieses Problem mittels Callbacks, die als Properties in die Child-Komponente hineingereicht werden. Wir können der Komponente TaskList drei neue Properties übergeben, in denen wir Callbacks der Komponente App übergeben. Zudem definieren wir die drei nicht-statischen Methoden zum Löschen, Aufpriorisieren und Abpriorisieren unserer Task-Items in der App-Komponente, da hier die Task-Items im State verwaltet werden.

Die Erweiterungen an der js/component/App.jsx sehen folgendermaßen aus:

                onTaskDelete={   ( taskIndex ) => this.deleteTask(   taskIndex ) }
                onTaskMoveUp={   ( taskIndex ) => this.moveTaskUp(   taskIndex ) }
                onTaskMoveDown={ ( taskIndex ) => this.moveTaskDown( taskIndex ) }

und

    /**
    *   Deletes the task with the specified index.
    *
    *   @param {number} taskIndex The index of the task to delete.
    */
    deleteTask( taskIndex )
    {
        console.log( "App.deleteTask( " + taskIndex + " ) being invoked" );

        // copy original array and delete specified task
        let newTaskList = this.state.taskList.slice();
        newTaskList.splice( taskIndex, 1 );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }

    /**
    *   Moves the task with the specified index up.
    *
    *   @param {number} taskIndex The index of the task to move up.
    */
    moveTaskUp( taskIndex )
    {
        console.log( "App.moveTaskUp( " + taskIndex + " ) being invoked" );

        if ( taskIndex > 0 )
        {
            // copy original array
            let newTaskList = this.state.taskList.slice();

            let taskToMoveUp   = newTaskList[ taskIndex     ];
            let taskToMoveDown = newTaskList[ taskIndex - 1 ];

            newTaskList[ taskIndex - 1 ] = taskToMoveUp;
            newTaskList[ taskIndex     ] = taskToMoveDown;

            // set new state forcing the component to re-render
            this.setState(
                {
                    taskList: newTaskList,
                }
            )
        }
    }

    /**
    *   Moves the task with the specified index down.
    *
    *   @param {number} taskIndex The index of the task to move down.
    */
    moveTaskDown( taskIndex )
    {
        console.log( "App.moveTaskDown( " + taskIndex + " ) being invoked" );

        if ( taskIndex < this.state.taskList.length - 1 )
        {
            // copy original array
            let newTaskList = this.state.taskList.slice();

            let taskToMoveDown = newTaskList[ taskIndex     ];
            let taskToMoveUp   = newTaskList[ taskIndex + 1 ];

            newTaskList[ taskIndex + 1  ] = taskToMoveDown;
            newTaskList[ taskIndex      ] = taskToMoveUp;

            // set new state forcing the component to re-render
            this.setState(
                {
                    taskList: newTaskList,
                }
            )
        }
    }
/**
*   The entire application component.
*   This is an example for a stateful component.
*/
class App extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            taskList: [
                "Milch kaufen",
                "Brownies backen",
                "Wäsche waschen",
                "Workshop vorbereiten",
            ],
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        return <div>

            { /* title */ }
            <h1 id="appTitle">{ this.props.title }</h1>

            { /* new task button */ }
            <button id="submitButton" className="button" onClick={
                () => this.createTask(
                    "New Task on "
                    + "["
                    + new Date().toISOString()
                    + "]"
                )
            }>
                Create Task
            </button>

            { /* task list */ }
            <TaskList
                taskList={ this.state.taskList }
                onTaskDelete={   ( taskIndex ) => this.deleteTask(   taskIndex ) }
                onTaskMoveUp={   ( taskIndex ) => this.moveTaskUp(   taskIndex ) }
                onTaskMoveDown={ ( taskIndex ) => this.moveTaskDown( taskIndex ) }
            />

        </div>;
    }

    /**
    *   Creates a new task in the TaskList component.
    *
    *   @param {string} taskName The name of the task to create.
    */
    createTask( taskName )
    {
        console.log( "App.createTask( " + taskName + " ) being invoked" );

        // copy original array and append new task
        let newTaskList = this.state.taskList.slice();
        newTaskList.push( taskName );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }

    /**
    *   Deletes the task with the specified index.
    *
    *   @param {number} taskIndex The index of the task to delete.
    */
    deleteTask( taskIndex )
    {
        console.log( "App.deleteTask( " + taskIndex + " ) being invoked" );

        // copy original array and delete specified task
        let newTaskList = this.state.taskList.slice();
        newTaskList.splice( taskIndex, 1 );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }

    /**
    *   Moves the task with the specified index up.
    *
    *   @param {number} taskIndex The index of the task to move up.
    */
    moveTaskUp( taskIndex )
    {
        console.log( "App.moveTaskUp( " + taskIndex + " ) being invoked" );

        if ( taskIndex > 0 )
        {
            // copy original array
            let newTaskList = this.state.taskList.slice();

            let taskToMoveUp   = newTaskList[ taskIndex     ];
            let taskToMoveDown = newTaskList[ taskIndex - 1 ];

            newTaskList[ taskIndex - 1 ] = taskToMoveUp;
            newTaskList[ taskIndex     ] = taskToMoveDown;

            // set new state forcing the component to re-render
            this.setState(
                {
                    taskList: newTaskList,
                }
            )
        }
    }

    /**
    *   Moves the task with the specified index down.
    *
    *   @param {number} taskIndex The index of the task to move down.
    */
    moveTaskDown( taskIndex )
    {
        console.log( "App.moveTaskDown( " + taskIndex + " ) being invoked" );

        if ( taskIndex < this.state.taskList.length - 1 )
        {
            // copy original array
            let newTaskList = this.state.taskList.slice();

            let taskToMoveDown = newTaskList[ taskIndex     ];
            let taskToMoveUp   = newTaskList[ taskIndex + 1 ];

            newTaskList[ taskIndex + 1  ] = taskToMoveDown;
            newTaskList[ taskIndex      ] = taskToMoveUp;

            // set new state forcing the component to re-render
            this.setState(
                {
                    taskList: newTaskList,
                }
            )
        }
    }
}

Die drei neuen Methoden deleteTask, moveTaskUp und moveTaskDown arbeiten nach dem selben Prinzip wie unsere createTask-Methode und entfernen dementsprechend ein Task-Item aus dem kopierten Array oder ändern dessen Reihenfolge.

Diese Callbacks können wir nun in unserer Komponente TaskList über die Properties ansprechen. Hierfür können wir die folgenden drei Buttons mit dem folgenden Code zu unserer Datei js/component/TaskList.jsx hinzufügen:

                    { /* Button 'Delete' */ }
                    <button
                        onClick={ () => { this.props.onTaskDelete( index ); } }
                        className="button"
                    >
                        ✖
                    </button>

                    { /* Button 'Move Down' */ }
                    <button
                        onClick={ () => { this.props.onTaskMoveDown( index ); } }
                        disabled={ index === this.props.taskList.length - 1 }
                        className="button"
                    >
                        ▼
                    </button>

                    { /* Button 'Move Up' */ }
                    <button
                        onClick={ () => { this.props.onTaskMoveUp( index ); } }
                        disabled={ index === 0 }
                        className="button"
                    >
                        ▲
                    </button>
/**
*   Represents the TaskList component.
*   This is an example for a stateless component.
*/
class TaskList extends React.Component
{
    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "TaskList.render() being invoked" );

        return <ul id="taskList">
        {
            this.createTaskListItems()
        }
        </ul>;
    }

    /**
    *   Creates and returns all items of the task list.
    *
    *   @return {JSXTransformer[]} The rendered JSX elements.
    */
    createTaskListItems()
    {
        let items = [];

        // browse all task list items
        for ( let index = 0; index < this.props.taskList.length; ++index )
        {
            items.push(
                <li key={ index }>

                    { /* The item description */ }
                    { this.props.taskList[ index ] }

                    { /* Button 'Delete' */ }
                    <button
                        onClick={ () => { this.props.onTaskDelete( index ); } }
                        className="button"
                    >
                        ✖
                    </button>

                    { /* Button 'Move Down' */ }
                    <button
                        onClick={ () => { this.props.onTaskMoveDown( index ); } }
                        disabled={ index === this.props.taskList.length - 1 }
                        className="button"
                    >
                        ▼
                    </button>

                    { /* Button 'Move Up' */ }
                    <button
                        onClick={ () => { this.props.onTaskMoveUp( index ); } }
                        disabled={ index === 0 }
                        className="button"
                    >
                        ▲
                    </button>

                </li>
            );
        }

        return items;
    }
}

Im Anschluss werden die Buttons in unserer Task-Liste angezeigt und wir können sie nutzen, um erstellte Task-Items zu löschen oder umzupriorisieren.

6. Controlled und uncontrolled components

Im Bezug auf Formulare gibt es in React zu beachten, dass das Standardverhalten von Forms generell verändert werden muss, da das Absenden eines Formulars standardmäßig eine neue Webseite öffnet, was natürlich nicht im Sinne einer Single-Page-Applikation ist.

Auch bei der Verwendung von Formularfeldern kann das Standardverhalten des DOMs komplett von der React-Komponente selbst übernommen werden. Daher unterscheidet man in React zwischen controlled und uncontrolled components.

6.1. Uncontrolled components

Dabei handelt es sich um Komponenten, die Formularfelder nutzen, aber diese nicht über eigene State-Variablen kontrollieren. Zur Verdeutlichung dieser Technik wollen wir die dritte und letzte Komponente TaskInput erstellen, die ein Eingabefeld für das Erstellen neuer Task-Items repräsentieren und verwalten soll.

Diese neue Skriptdatei müssen wir zuerst zu unserer index.html hinzufügen:

        <script src="js/component/TaskInput.jsx" type="text/jsx"></script>
<!DOCTYPE html>
<html>
    <head>

        <!-- default encoding -->
        <meta charset="UTF-8" />

        <!-- external stylesheet -->
        <link rel="stylesheet" href="css/styles.css">

        <!-- library sources -->
        
        
        

        <!-- custom sources -->
        <script src="js/component/TaskInput.jsx" type="text/jsx"></script>
        <script src="js/component/TaskList.jsx"  type="text/jsx"></script>
        <script src="js/component/App.jsx"       type="text/jsx"></script>
        <script src="js/index.jsx"               type="text/jsx"></script>

    </head>
    <body>
        <div id="mainContainer"></div>
    </body>
</html>

Die Datei js/component/TaskInput.jsx definiert unsere Komponente, die das Eingabeformular zum Erstellen neuer Tasks verwaltet. Diese Datei besteht aus dem folgenden Code:

/**
*   Represents the input component that lets the user create new tasks.
*   This is an example for a stateful and uncontrolled component.
*/
class TaskInput extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            inputError: false,
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "TaskInput.render() being invoked" );

        return <form onSubmit={ ( event ) => { this.onFormSubmit( event ); } }>

            { /* new task input */ }
            <input
                id="newTask"
                type="text"
                maxLength="50"
                className={ this.state.inputError ? "input error" : "input" }
            />

            <br />

            { /* new task button */ }
            <input
                id="submitButton"
                type="submit"
                value="Create Task"
                className="button"
            />

        </form>;
    }

    /**
    *   Being invoked when the form is submitted.
    *
    *   @param {Event} event The form submission event.
    */
    onFormSubmit( event )
    {
        console.log( "TaskInput.onFormSubmit being invoked" );

        // suppress page reload
        event.preventDefault();

        // get input field and trim entered text
        let inputField  = event.target.firstChild;
        let enteredText = inputField.value.trim();

        // clear input field
        inputField.value = "";

        // check entered text
        console.log( "Trimmed text in the box is [" + enteredText + "]" );
        if ( enteredText.length === 0 )
        {
            console.log( "Empty text input detected." );

            // set error state
            this.setState(
                {
                    inputError: true,
                }
            );
        }
        else
        {
            // clear error state
            this.setState(
                {
                    inputError: false,
                }
            );

            // invoke parent listener
            this.props.onTaskCreate( enteredText );
        }
    };
}

In dem Form-Tag unseres Formulars wird über das onSubmit-Attribut der Aufruf der nicht-statischen Methode onFormSubmit zugewiesen, die beim Versenden des Formulars aufgerufen wird. Darin wird mit event.preventDefault() unterbunden, dass der Browser einen neuen URL-Request ausführt.

Das restliche Handling aber wird dem DOM überlassen. Damit der Wert aus dem Eingabefeld ausgelesen und selbiges auch wieder geleert werden kann, ist ein Zugriff auf das Eingabefeld über das DOM erforderlich. Somit stellt diese Komponente eine uncontrolled component dar.

Damit wir diese neue Komponente in unserer Anwendung sehen, müssen wir sie noch in unsere App-Komponente einbauen und den bestehenden Button entfernen. Die Callback-Funktion onTaskCreate zum Erstellen eines neuen Tasks übergeben wir, wie gewohnt, als Property an unsere Komponente TaskInput. Der Code unserer js/component/App.jsx ist dementsprechend folgendermaßen abzuändern:

            { /* task input form */ }
            <TaskInput
                onTaskCreate={ ( taskName ) => this.createTask( taskName ) }
            />
/**
*   The entire application component.
*   This is an example for a stateful component.
*/
class App extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            taskList: [
                "Milch kaufen",
                "Brownies backen",
                "Wäsche waschen",
                "Workshop vorbereiten",
            ],
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        return <div>

            { /* title */ }
            <h1 id="appTitle">{ this.props.title }</h1>

            { /* task input form */ }
            <TaskInput
                onTaskCreate={ ( taskName ) => this.createTask( taskName ) }
            />

            { /* task list */ }
            <TaskList
                taskList={ this.state.taskList }
                onTaskDelete={   ( taskIndex ) => this.deleteTask(   taskIndex ) }
                onTaskMoveUp={   ( taskIndex ) => this.moveTaskUp(   taskIndex ) }
                onTaskMoveDown={ ( taskIndex ) => this.moveTaskDown( taskIndex ) }
            />

        </div>;
    }

    /**
    *   Creates a new task in the TaskList component.
    *
    *   @param {string} taskName The name of the task to create.
    */
    createTask( taskName )
    {
        console.log( "App.createTask( " + taskName + " ) being invoked" );

        // copy original array and append new task
        let newTaskList = this.state.taskList.slice();
        newTaskList.push( taskName );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }

    /**
    *   Deletes the task with the specified index.
    *
    *   @param {number} taskIndex The index of the task to delete.
    */
    deleteTask( taskIndex )
    {
        console.log( "App.deleteTask( " + taskIndex + " ) being invoked" );

        // copy original array and delete specified task
        let newTaskList = this.state.taskList.slice();
        newTaskList.splice( taskIndex, 1 );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }

    /**
    *   Moves the task with the specified index up.
    *
    *   @param {number} taskIndex The index of the task to move up.
    */
    moveTaskUp( taskIndex )
    {
        console.log( "App.moveTaskUp( " + taskIndex + " ) being invoked" );

        if ( taskIndex > 0 )
        {
            // copy original array
            let newTaskList = this.state.taskList.slice();

            let taskToMoveUp   = newTaskList[ taskIndex     ];
            let taskToMoveDown = newTaskList[ taskIndex - 1 ];

            newTaskList[ taskIndex - 1 ] = taskToMoveUp;
            newTaskList[ taskIndex     ] = taskToMoveDown;

            // set new state forcing the component to re-render
            this.setState(
                {
                    taskList: newTaskList,
                }
            )
        }
    }

    /**
    *   Moves the task with the specified index down.
    *
    *   @param {number} taskIndex The index of the task to move down.
    */
    moveTaskDown( taskIndex )
    {
        console.log( "App.moveTaskDown( " + taskIndex + " ) being invoked" );

        if ( taskIndex < this.state.taskList.length - 1 )
        {
            // copy original array
            let newTaskList = this.state.taskList.slice();

            let taskToMoveDown = newTaskList[ taskIndex     ];
            let taskToMoveUp   = newTaskList[ taskIndex + 1 ];

            newTaskList[ taskIndex + 1  ] = taskToMoveDown;
            newTaskList[ taskIndex      ] = taskToMoveUp;

            // set new state forcing the component to re-render
            this.setState(
                {
                    taskList: newTaskList,
                }
            )
        }
    }
}

Wir können nun in unserer Web-Applikation neue Task-Items erstellen, wobei die eingegebene Beschreibung zu unserer Task-Liste hinzugefügt wird. Wird vom Benutzer ein leerer String im Formularfeld abgesendet, so färbt sich das Eingabefeld rot und weist auf diesen Fehler hin. Das Eingabefeld färbt sich erst wieder um, nachdem das Formular erneut mit einer gültigen Eingabe abgesendet wurde.

6.2. Controlled components

Im Gegensatz zu den uncontrolled components wird bei den controlled components die volle Kontrolle über die Bestandteile unserer Formulare übernommen und keine Kontrolle an das DOM abgegeben. Somit können wir zudem erreichen, dass sich unsere Anwendung noch responsiver anfühlt.

Wir wollen unsere TaskInput-Komponente nun in eine controlled component überführen. Hierfür führen wir ein State-Handling in diese Komponente ein und halten hier den aktuellen Inhalt unseres Eingabefeldes fest. Selbiges erweitern wir mit den Attributen value und onChange, sodass jede Änderung am Inhalt dieses Feldes eine sofortige Änderung des States unserer Komponente zur Folge hat.

Die genannten Änderungen äußern sich in unserer js/component/TaskInput.jsx wie folgt:

/**
*   Represents the input component that lets the user create new tasks.
*   This is an example for a stateful and controlled component.
*/
class TaskInput extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            inputError: false,
            inputText:  "",
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "TaskInput.render() being invoked" );

        return <form onSubmit={ ( event ) => { this.onFormSubmit( event ); } }>

            { /* new task input */ }
            <input
                id="newTask"
                type="text"
                maxLength="50"
                className={ this.state.inputError ? "input error" : "input" }
                value={     this.state.inputText }
                onChange={  ( event ) => { this.onInputChange( event ); } }
            />

            <br />

            { /* new task button */ }
            <input
                id="submitButton"
                type="submit"
                value="Create Task"
                className="button"
            />

        </form>;
    }

    /**
    *   Being invoked when the input field value changes.
    *
    *   @param {Event} event The event when the input field value changes.
    */
    onInputChange( event )
    {
        console.log( "TaskInput.onInputChange being invoked" );

        this.setState(
            {
                inputError: false,
                inputText:  event.target.value,
            }
        );
    }

    /**
    *   Being invoked when the form is submitted.
    *
    *   @param {Event} event The form submission event.
    */
    onFormSubmit( event )
    {
        console.log( "TaskInput.onFormSubmit being invoked" );

        // suppress page reload
        event.preventDefault();

        // trim entered text
        let enteredText = this.state.inputText.trim();

        // check entered text
        console.log( "Trimmed text in the box is [" + enteredText + "]" );
        if ( enteredText.length === 0 )
        {
            console.log( "Empty text input detected." );

            // set error state
            this.setState(
                {
                    inputError: true,
                    inputText:  "",
                }
            );
        }
        else
        {
            // clear error state
            this.setState(
                {
                    inputError: false,
                    inputText:  "",
                }
            );

            // invoke parent listener
            this.props.onTaskCreate( enteredText );
        }
    };
}

Die Änderungen haben nun zur Folge, dass sich im Falle einer falschen Eingabe das rot eingefärbte Eingabefelde bei der ersten Eingabe eines Zeichens wieder zurückfärbt und der Benutzer somit eine responsivere User Experience erfährt.

Controlled Components bieten uns somit alle Möglichkeiten, die wir für die Realisierung unserer Benutzerschnittstelle benötigen, da wir die Kontrolle über alle UX-Elemente behalten und alle Elemente über den State unserer Komponente unmittelbar verwalten können.

7. Component lifecycle callbacks

Die Klasse React.Component bietet bestimmte nicht-statische Methoden zum Überschreiben an, die wir bei Bedarf innerhalb unserer React-Komponente definieren können. Hierüber können wir bestimmte Rückmeldungen vom React-System erhalten – beispielsweise wenn eine Komponente unmittelbar vor dem erneuten Rendern steht – sodass wir die Gelegenheit haben, in diesem Vorfeld beliebige eigene Aktionen durchzuführen. Das kann beispielsweise der Aufruf einer Tracking-Methode oder eines asynchronen Ladevorgangs sein.

Zum Testen dieses Mechanismus können wir die folgenden Methoden zu unserer App-Komponente in der js/component/App.jsx hinzufügen:

    /**
    *   Being invoked before this component has been mounted.
    */
    componentWillMount()
    {
        console.log( "App.componentWillMount() being invoked" );
    }

    /**
    *   Being invoked after this component has been mounted.
    */
    componentDidMount()
    {
        console.log( "App.componentDidMount() being invoked" );
    }

    /**
    *   Being invoked before this component has been unmounted.
    */
    componentWillUnmount()
    {
        console.log( "App.componentWillUnmount() being invoked" );
    }

    /**
    *   Being invoked before this component has been updated.
    *
    *   @param {Object} nextProps   The props to set on updating.
    *   @param {Object} nextState   The state to set on updating.
    *   @param {Object} nextContext The context to set on updating.
    */
    componentWillUpdate( nextProps, nextState, nextContext )
    {
        console.log( "App.componentWillUpdate() being invoked" );
    }

    /**
    *   Being invoked after this component has been updated.
    */
    componentDidUpdate()
    {
        console.log( "App.componentDidUpdate() being invoked" );
    }

    /**
    *   Being invoked before this component receives props.
    *
    *   @param {Object} nextProps   The props to set on updating.
    *   @param {Object} nextContext The context to set on updating.
    */
    componentWillReceiveProps( nextProps, nextContext )
    {
        console.log( "App.componentWillReceiveProps() being invoked" );
    }
/**
*   The entire application component.
*   This is an example for a stateful component.
*/
class App extends React.Component
{
    /**
    *   Initializes this component by setting the initial state.
    *
    *   @param {Object} props The initial properties being passed in the component tag.
    */
    constructor( props )
    {
        super( props );

        this.state = {
            taskList: [
                "Milch kaufen",
                "Brownies backen",
                "Wäsche waschen",
                "Workshop vorbereiten",
            ],
        }
    }

    /**
    *   Being invoked every time this component renders.
    *
    *   @return {JSXTransformer} The rendered JSX.
    */
    render()
    {
        console.log( "App.render() being invoked" );

        return <div>

            { /* title */ }
            <h1 id="appTitle">{ this.props.title }</h1>

            { /* task input form */ }
            <TaskInput
                onTaskCreate={ ( taskName ) => this.createTask( taskName ) }
            />

            { /* task list */ }
            <TaskList
                taskList={ this.state.taskList }
                onTaskDelete={   ( taskIndex ) => this.deleteTask(   taskIndex ) }
                onTaskMoveUp={   ( taskIndex ) => this.moveTaskUp(   taskIndex ) }
                onTaskMoveDown={ ( taskIndex ) => this.moveTaskDown( taskIndex ) }
            />

        </div>;
    }

    /**
    *   Creates a new task in the TaskList component.
    *
    *   @param {string} taskName The name of the task to create.
    */
    createTask( taskName )
    {
        console.log( "App.createTask( " + taskName + " ) being invoked" );

        // copy original array and append new task
        let newTaskList = this.state.taskList.slice();
        newTaskList.push( taskName );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }

    /**
    *   Deletes the task with the specified index.
    *
    *   @param {number} taskIndex The index of the task to delete.
    */
    deleteTask( taskIndex )
    {
        console.log( "App.deleteTask( " + taskIndex + " ) being invoked" );

        // copy original array and delete specified task
        let newTaskList = this.state.taskList.slice();
        newTaskList.splice( taskIndex, 1 );

        // set new state forcing the component to re-render
        this.setState(
            {
                taskList: newTaskList,
            }
        )
    }

    /**
    *   Moves the task with the specified index up.
    *
    *   @param {number} taskIndex The index of the task to move up.
    */
    moveTaskUp( taskIndex )
    {
        console.log( "App.moveTaskUp( " + taskIndex + " ) being invoked" );

        if ( taskIndex > 0 )
        {
            // copy original array
            let newTaskList = this.state.taskList.slice();

            let taskToMoveUp   = newTaskList[ taskIndex     ];
            let taskToMoveDown = newTaskList[ taskIndex - 1 ];

            newTaskList[ taskIndex - 1 ] = taskToMoveUp;
            newTaskList[ taskIndex     ] = taskToMoveDown;

            // set new state forcing the component to re-render
            this.setState(
                {
                    taskList: newTaskList,
                }
            )
        }
    }

    /**
    *   Moves the task with the specified index down.
    *
    *   @param {number} taskIndex The index of the task to move down.
    */
    moveTaskDown( taskIndex )
    {
        console.log( "App.moveTaskDown( " + taskIndex + " ) being invoked" );

        if ( taskIndex < this.state.taskList.length - 1 )
        {
            // copy original array
            let newTaskList = this.state.taskList.slice();

            let taskToMoveDown = newTaskList[ taskIndex     ];
            let taskToMoveUp   = newTaskList[ taskIndex + 1 ];

            newTaskList[ taskIndex + 1  ] = taskToMoveDown;
            newTaskList[ taskIndex      ] = taskToMoveUp;

            // set new state forcing the component to re-render
            this.setState(
                {
                    taskList: newTaskList,
                }
            )
        }
    }

    /**
    *   Being invoked before this component has been mounted.
    */
    componentWillMount()
    {
        console.log( "App.componentWillMount() being invoked" );
    }

    /**
    *   Being invoked after this component has been mounted.
    */
    componentDidMount()
    {
        console.log( "App.componentDidMount() being invoked" );
    }

    /**
    *   Being invoked before this component has been unmounted.
    */
    componentWillUnmount()
    {
        console.log( "App.componentWillUnmount() being invoked" );
    }

    /**
    *   Being invoked before this component has been updated.
    *
    *   @param {Object} nextProps   The props to set on updating.
    *   @param {Object} nextState   The state to set on updating.
    *   @param {Object} nextContext The context to set on updating.
    */
    componentWillUpdate( nextProps, nextState, nextContext )
    {
        console.log( "App.componentWillUpdate() being invoked" );
    }

    /**
    *   Being invoked after this component has been updated.
    */
    componentDidUpdate()
    {
        console.log( "App.componentDidUpdate() being invoked" );
    }

    /**
    *   Being invoked before this component receives props.
    *
    *   @param {Object} nextProps   The props to set on updating.
    *   @param {Object} nextContext The context to set on updating.
    */
    componentWillReceiveProps( nextProps, nextContext )
    {
        console.log( "App.componentWillReceiveProps() being invoked" );
    }
}

In der Konsole unserer Entwicklerwerkzeuge können wir nun beobachten, wann diese lifecycle callbacks aufgerufen werden.

React FTW

Das React-Framework bietet sehr gute Möglichkeiten zur Erstellung und Verwaltung von modular aufgebauten und somit gut strukturierten Frontend-Anwendungen. Vor allem in Kombination mit anderen Frameworks entfaltet React seine volle Flexibilität und Stärke.

Der Code dieses Beispielprojekts befindet sich auf GitHub.

Mein Ziel ist es, mit meinem React-Workshop einen schnellen Einstieg in das Framework zu geben – und ich hoffe, dass mir das gut gelungen ist. Wie immer bin ich für Feedback unter christopher.stock@mayflower.de erreichbar und dankbar.

Unser React-Workshop


Avatar von Christopher Stock

Kommentare

3 Antworten zu „React-Workshop für Einsteiger“

  1. Lust auf #React? Unser @jenetic1980 bietet einen umfassenden Einstieg in die Thematik: https://t.co/51CtoQpM5k

  2. Mehr #React geht nicht – unser großes Einsteiger-Tutorial! https://t.co/51CtoQpM5k

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.