npm
“package binary” named create-react-app
(a kind of Command Line Interface - CLI:
here…).
To that extent, the best way of configuring a React environment depends upon the recommended sequential execution:
<sudo> npm uninstall -g create-react-app
<sudo> npm i -g create-react-app
create-react-app
that installs all the desired stuff:
npx create-react-app currencies.react
create-react-app
(here…) that focuses
on the dev. process and related configuration instead of React
inner workings, i.e., JSX, MVC support, etc.
// '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. */}
);
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 */
…
}
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...
}
}
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>
…
}
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 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!
}
}
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
that are called by React at key instants of components' lifecycle. Overriding is in essence an option…
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'";
}
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 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.
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...
}
…
}
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*/
);
}
}
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()
}
}));
}
…
}
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 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 { …
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>
);
}
}
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...
});
…
}
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'...
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>
);
}
}
Two-way binding in
React just relies on the setState
(inherited from
React.Component
) primitive.
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