React

React, overview

React, installation

React, notion of “component”

React, OO versus pure functional

// 'currencies-wrapper.component.js' file
import React, {Component} from "react";
import './currencies-wrapper.component.css'; // webpack (https://create-react-app.dev/docs/adding-a-stylesheet)
import Currencies from './currencies.component'; // As child...
…
export default class CurrenciesWrapper extends Component {
    …
    render() { return(/* JSX here... */); }
}
function CurrenciesWrapper(props) {
  return(/* JSX here... */);
}
…
ReactDOM.render(
    <CurrenciesWrapper />,
    window.document.getElementById('root') {/* JSX comment: from top of app. */}
);

React, props (push data)

props are injected data from parents to children by means of HTML tag properties.

// 'currencies-wrapper.component.js' file
import React, {Component} from "react";
import './currencies-wrapper.component.css'; // webpack (https://create-react-app.dev/docs/adding-a-stylesheet)
import Currencies from './currencies.component'; // As child...
…
export default class CurrenciesWrapper extends Component {
    …
    render() { // Wrapper displays "$", "€", etc.
        return (
            <div className="currencies-wrapper">
                <h2>Currency of interest: {this.read()} ('{this.context}' as global var.)</h2>
                {/* Child: */} <Currencies currency_of_interest={this.get_currency_of_interest()} emit={this.update}/>
            </div>
        );
    }
    // Proxy for 'emit={this.update}':
    update = this._update.bind(this); /* ES7 instance attribute outside constructor -> supported by Babel */
    …
}

React, props (pop data)

this.props acts as the way for children to access pushed data. “Data” can be handling functions that may serve as reverse communication paths from children to parents (see also: here…).

// 'currencies-component.js' file
…
export default class Currencies extends React.Component {
    …
    send = function (currency) { /* ES7 instance attribute outside constructor -> supported by Babel */
        // Contextual currency must be emitted to wrapper as currency of interest...
        this.props.emit(currency);
        this.setState({currency_of_interest: currency}); // Record "I am" the currency of interest...
    }

    constructor(props, context) {
        super(props, context);
        // Web service is immediately launched:
        this._currencies_service = new Currencies_service(Currencies.Currencies[Currencies.Dollar], Currencies.Currencies[Currencies.Euro]);
        this.state = {
            currency_of_interest: this.props.currency_of_interest // From parent...
        }
    }

React, “Currencies” nav.

React, context (see also: here… and there…)

context is the way of dispatching global data (e.g., chosen language, logged person…) as “globals”. This eliminates any possible excessive use of props. Through a context pushed from (top) parent, children, including sub-children may access to “globals”.

// 'Globals.js' file
import React from 'react';
export const Trainer = React.createContext("Anonymous"); // Default value...
// 'currencies-wrapper.component.js' file
…
import {Trainer} from './Globals';

export default class CurrenciesWrapper extends Component {
    static contextType = Trainer;
    …
                <h2>Currency of interest: {this.read()} ('{this.context}' as global var.)</h2>
    …
}

React, context cont'd

// 'App.js' file
…
import CurrenciesMenu from './currencies-menu.component';
import {Trainer} from './Globals';

export default function App() {
    return (
        <div className="App">
            <Trainer.Provider value="Franck Barbier">
                <CurrenciesMenu/>
            </Trainer.Provider>
        </div>
    );
}

React, component state

React puts forward stateful components in the sense these maintain a state inside an instance attribute named state. setState (inherited from React.Component) is in charge of updating state value(s) according to interaction. React smartly refreshes views from changes operated by setState (API: here…).

export default class CurrenciesWrapper extends Component {
    …
    state = { currency_of_interest_wrapper: null } /* ES7 instance attribute outside constructor -> supported by Babel */
    get_currency_of_interest() { return this.state.currency_of_interest_wrapper; }
    read() { // Wrapper displays "$", "€", etc.
        return this.state.currency_of_interest_wrapper !== null ? this.state.currency_of_interest_wrapper.common_symbol : "";
    }
    …
    _update(currency) {
        // 'setState' enqueues changes to the component state and
        // tells React that this component and its children need to be re-rendered with the updated state...
        this.setState({currency_of_interest_wrapper: currency}); // Asynchronous!
    }
}

React, refs (see also: here…)

Refs “point to” HTML elements as JavaScript objects whose type is HTMLElement (current attribute of a ref is the effective JavaScript object). Note that “direct” access to such objects is sometimes useless since props aim at (more “naturally”) communicating data from parents to children.

// In the constructor class:
this.iso_code = React.createRef();
…
componentDidMount() { // Hook: close to 'ngOnInit' in Angular...
    /* HTML5 Constraint Validation API */
    this.iso_code.current.addEventListener('input', (event) => { // Each new typed character...
    …
<label>ISO code: {/* HTML5 Constraint Validation API through 'ref={this.iso_code}' */}
    <input pattern={CurrenciesController.ISO_code_pattern_as_String_}
        placeholder={this.state.new_currency.iso_code}
        ref={this.iso_code} required type="text"/>
</label>

React, component lifecycle hooks (see also: here… and there…)

componentDidUpdate(prior_props, prior_state, getSnapshotBeforeUpdate) { // Hook...
    window.console.assert(getSnapshotBeforeUpdate === "Message to be checked in 'componentDidUpdate'");
    // this.my_form.current.reportValidity(); // "Error: Maximum update depth exceeded"
    if (this.my_form_validity)
        Swal.fire({
            position: 'top-end',
            icon: 'success',
            title: '(form valid) -> \'componentDidUpdate\'...',
            showConfirmButton: false,
            timer: 1500
        });
}

getSnapshotBeforeUpdate(prior_props, prior_state) { // Before the DOM is updated...
    // this.my_form.current.reportValidity(); // "Error: Maximum update depth exceeded"
    if (this.my_form_validity)
        window.console.log("'getSnapshotBeforeUpdate': " + this.my_form_validity);
    return "Message to be checked in 'componentDidUpdate'";
}

React, componentDidMount

componentDidMount() { // Hook: close to 'ngOnInit' in Angular...
    /* HTML5 Constraint Validation API */
    this.iso_code.current.addEventListener('input', (event) => { // Each new typed character...
        this.setState(prior_state => ({
            new_currency: {
                ...prior_state.new_currency, // Copy all other key-value pairs of 'new_currency' object...
                iso_code_validity: true // A priori, no problem...
            }
        }));
        this.iso_code.current.setCustomValidity(""); // A priori, no problem...
        this.iso_code.current.checkValidity(); // Send 'invalid' if 'false'...
    });
    this.iso_code.current.addEventListener('invalid', (event) => { // Called at form submission time as well...
        this.setState(prior_state => ({
            new_currency: {
                ...prior_state.new_currency, // Copy all other key-value pairs of 'new_currency' object...
                iso_code_validity: false // Problem...
            }
        }));
        if (this.iso_code.current.value === "")
            this.iso_code.current.setCustomValidity("Empty ISO code is not permitted...");
        else this.iso_code.current.setCustomValidity("3 digits excluding '000'");
    });
}

React, form (see also: here… and there…)

React manages forms in two ways: through uncontrolled components (form state is in HTML) versus controlled components (form state is in JavaScript). The latter mode is advised; it strongly relies on the setState (inherited from React.Component) primitive.

Contrary to Angular for instance, React has no enhanced support for form validation. As a result, the HTML5 Constraint Validation API still plays a great role. Beyond, cohabitation of “uncontrolled components” and “controlled components” modes is possible.

Illustration of “uncontrolled components” mode is in slides 11 and 13.

React, “New” nav.

React, form (“controlled components” mode cont'd)

class CurrenciesController extends Component {
    _initialization() {
        return {
            new_currency: {
                common_symbol: "e.g., $",
                common_name: "e.g., Dollar",
                description: "...",
                iso_code: "Three digits excluding '000'",
                iso_code_validity: false,
                iso_symbol: "Three upper case letters",
                iso_symbol_validity: false,
                substitution_date: null
            }
        };
    }
    constructor(props) {
        super(props);
        /* HTML5 Constraint Validation API */
        // const iso_code = window.document.getElementById("iso_code"); // No! Instead:
        this.iso_code = React.createRef(); // Direct access of DOM element (using 'current') fails because the DOM is not yet loaded...
        this.my_form = React.createRef(); // Direct access of DOM element (using 'current') fails because the DOM is not yet loaded...
        this.my_form_validity = false;
        this.state = this._initialization(); // "controlled components" mode...
    }
    …
}

React, form (“controlled components” mode cont'd)

class CurrenciesController extends Component {
    …
    render() { // This seems the best pattern (i.e., called inside 'render'):
        this.my_form_validity = this.state.new_currency.common_name.length > 0 && /* 'required' */ this.state.new_currency.common_symbol.length > 0 && /* 'required' */
            this.state.new_currency.iso_code_validity && this.state.new_currency.iso_symbol_validity;
        window.console.assert(!this.my_form_validity || this.my_form.current.reportValidity()); /* 'this.my_form_validity' => 'this.my_form.current.reportValidity()' */
        return (
             /*JSX fragment*/ <>
                <form action="" id="my_form" name="my_form" onSubmit={this.record_currency.bind(this)} ref={this.my_form}> {/* 'novalidate' -> HTML5 Constraint Validation API is inhibited... ('noValidate' in JSX?) */}
                    <p><label>Common name:
                        <input onChange={this.set_common_name.bind(this)} placeholder={this.state.new_currency.common_name} required type="text" value={this.state.new_currency.common_name}/>
                        <button onClick={this.reset_common_name.bind(this)} type="button">Reset</button></label></p>
                    <p><label>Common symbol:<input placeholder={this.state.new_currency.common_symbol} required type="text"/></label></p>
                    <p><label>Description:<input placeholder={this.state.new_currency.description} type="text"/></label></p>
                    <p><label>ISO code: {/* HTML5 Constraint Validation API through 'ref={this.iso_code}' */}
                        <input pattern={CurrenciesController.ISO_code_pattern_as_String_} placeholder={this.state.new_currency.iso_code} ref={this.iso_code} required type="text"/></label></p>
                    <p><label>ISO symbol: {/* Custom validation... */}
                        <input onChange={this.check_iso_symbol.bind(this)} placeholder={this.state.new_currency.iso_symbol} required type="text" value={this.state.new_currency.iso_symbol}/></label>
                        {/*Conditional rendering*/}{!this.state.new_currency.iso_symbol_validity && <small>At least 3, at most 3 capital letters...</small>}</p>
                    <p><label>Substitution date:<input name="substitution_date" type="date"/></label></p>
                </form>
                <p><button disabled={!this.my_form_validity} form="my_form" type="submit">Record currency...</button></p>
            </> /*JSX fragment*/ 
        );
    }
}

React, form (“controlled components” mode cont'd)

class CurrenciesController extends Component {
    …
    reset_common_name = function () { /* ES7 instance attribute outside constructor -> supported by Babel */
        // https://stackoverflow.com/questions/43638938/updating-an-object-with-setstate-in-react
        this.setState(prior_state => { // https://reactjs.org/docs/react-component.html#setstate
            const new_currency = Object.assign({}, prior_state.new_currency);
            new_currency.common_name = "";
            return {new_currency};
        });
    }
    set_common_name = function (event) { /* ES7 instance attribute outside constructor -> supported by Babel */
        const common_name = event.target.value;
        this.setState(prior_state => ({ // <- 'event.target.value' is unknown because updater function is asynchronous!
            new_currency: {
                ...prior_state.new_currency, // Copy all 'new_currency' key-value pairs...
                common_name: common_name.toUpperCase()
            }
        }));
    }
    …
}

React, form submission

record_currency(event) { // window.console.assert(event.target instanceof HTMLFormElement);
    event.preventDefault(); // This prevents the page from being refreshed...
    /* Add new data in 'Currencies.Currencies'... */
    /* Add new corresponding route in 'Currencies_menu.Navigations'... */
    const next_round = Math.floor(Math.random() * 2); // For the fun: '0' or '1'...
    if (next_round === 0) {
        Swal.fire({
            position: 'top-end',
            icon: 'success',
            title: '(form reset) -> "New" is following navigation...',
            showConfirmButton: false,
            timer: 1500
        });
        this.setState(this._initialization()); // controlled-component way: empty form...
        event.target.reset(); // uncontrolled-component way: empty form...
    } else { // window.console.assert(next_round === 1);
        Swal.fire({
            position: 'top-end',
            icon: 'success',
            title: '(form quit) -> "Currencies" is following navigation...',
            showConfirmButton: false,
            timer: 1500
        });
        this.props.history.push('/Currencies'); // 'export default withRouter(CurrenciesController);'
    }
}

React, routing (see also: here… and there…)

React requires an additional module as follows: npm install react-router-dom. Predefined tags and related management components from react-router-dom may then be reused to operate routing.

import React, {Component} from "react";
import {
    BrowserRouter as Router,
    Link,
    Redirect,
    Route,
    // Switch
} from "react-router-dom";
…
export default class CurrenciesMenu extends Component { …

React, routing (navigation)

export default class CurrenciesMenu extends Component {
    …
    render() {
        return (
            <Router>
                <nav className="currencies-menu">
                    { // <- JSX evaluate exp.
                        CurrenciesMenu._Links.map(link => ( // 4 links... <=> '<nav *ngFor="let navigation of navigations">' in Angular
                            <Link className="mat-raised-button" to={link.path}>
                                {/* Ligature principle (https://alistapart.com/article/the-era-of-symbol-fonts/): */}
                                <i className="currencies material-icons">{link.data.material_icon}</i>
                            </Link>
                        )) }
                </nav>
                { // <- JSX evaluate exp.
                    CurrenciesMenu._Routes.map(route => ( // 3 routes...
                        <Route component={route.component}
                               exact
                               path={("iso_code" in route.data) ? CurrenciesMenu._Routes[0].path + "/:iso_code" : route.path}>
                        </Route>
                    )) }
                <Route exact path="/"> {true ? <Redirect to={CurrenciesMenu._Routes[0].path}/> : <h1>No 'Redirect' yet...</h1>}</Route>
            </Router>
        );
    }
}

React, routing (routes)

export default class CurrenciesMenu extends Component {
    static _Links = [
        {// eslint-disable-next-line
            path: '/' + 'Currencies',
            component: CurrenciesWrapper,
            data: {material_icon: 'payment'} },
        {// eslint-disable-next-line
            path: '/' + 'New',
            component: CurrenciesController,
            data: {material_icon: 'fiber_new'} },
        {// eslint-disable-next-line
            path: '/' + 'Currencies' + '/' + `${Currencies.Currencies[Currencies.Dollar].iso_code}`,
            component: CurrenciesInformation,
            data: {iso_code: Currencies.Currencies[Currencies.Dollar].iso_code, material_icon: 'attach_money'} },
        {// eslint-disable-next-line
            path: '/' + 'Currencies' + '/' + `${Currencies.Currencies[Currencies.Euro].iso_code}`,
            component: CurrenciesInformation_, // => 4th component!
            data: {iso_code: Currencies.Currencies[Currencies.Euro].iso_code, material_icon: 'euro_symbol'} }
    ];
    static _Components = [...new Set(CurrenciesMenu._Links.map(link => link.component))]; // 3 or 4...
    static _Routes = CurrenciesMenu._Links.filter(link => {
        const index = CurrenciesMenu._Components.findIndex(component => component === link.component);
        CurrenciesMenu._Components.splice(index, 1);
        return index !== -1; // Found...
    });
    …
}

React, “Dollar” nav.

React, routing (router state) (see also: here… and there…)

import React, {Component} from "react";
import {withRouter} from "react-router-dom";
…
class CurrenciesInformation extends Component {
    // Disabling browser back button in React: https://medium.com/@subwaymatch/disabling-back-button-in-react-with-react-router-v5-34bb316c99d7
    back = function () { /* ES7 instance attribute outside constructor -> supported by Babel */
         this.props.history.goBack(); // Access to router state...
    }
    information() {
        return "Here: " + this.props.match.params.iso_code; // Access to router state...
    }
    render() {
        return (
            <div className="currencies-information">
                <h2>{this.information()}</h2>
                <button onClick={this.back.bind(this)} type="button">Back (leave from '{this.props.location.pathname}')</button>
            </div>
        );
    }
}
export default withRouter(CurrenciesInformation); // Router state (location, parameters...) are injected into 'props'...

React, routing (router state, variation)

import React, {Component} from "react";
import {useHistory, useLocation, useParams} from "react-router-dom"; // React Router >= v5.1...
…
export default function CurrenciesInformation() { // As proxy to use 'useParams'...
    const history = useHistory();
    const location = useLocation();
    const {iso_code} = useParams(); // Cannot call a hook such as 'useParams' inside 'React.Component'...
    return (<_CurrenciesInformation history={history} iso_code={iso_code} location={location}/>);
}
class _CurrenciesInformation extends Component {
    // Disabling browser back button in React: https://medium.com/@subwaymatch/disabling-back-button-in-react-with-react-router-v5-34bb316c99d7
    back = function () { /* ES7 instance attribute outside constructor -> supported by Babel */
        this.props.history.goBack();
    }
    information() {
        return "Here: " + this.props.iso_code;
    }
    render() {
        return (
            <div className="currencies-information">
                <h2>{this.information()}</h2>
                <button onClick={this.back.bind(this)} type="button">Back (leave from '{this.props.location.pathname}')</button>
            </div>
        );
    }
}

React, two-way binding (see also: here…)

Two-way binding in React just relies on the setState (inherited from React.Component) primitive.

React, test (static type checking (see also: here…)

Beyond the possibility of using TypeScript, React provides its own support for static type checking.

import PropTypes from "prop-types"; // TypeScript-like type checking...

TypeScript involves a special workspace at app. creation time.

npx create-react-app currencies.react --template typescript

© Franck Barbier