Topic 20: React

Note: You should only look at this topic when you have completed all previous exercises from the majority of previous topics (Topics 1-5, 6-8 and 11-12). Additionally, you should at least read Topic 9 and Topic 15, as these discuss classes and bind(), which you need to understand to develop with React.

What is React?

React (see reactjs.org) is a JavaScript framework, originally developed by Facebook, which allows complex JavaScript applications to be written in modular, high-level, easy-to-read code. React revolves around the concept of a component. A component is a subsection of a web page, consisting of multiple related HTML elements (e.g. <div>s, form fields and buttons), which all work together to perform a particular task.

For example, we could have a component representing a clock, which consists of a <div> containing the current time. Or we could have a component for an AJAX search, consisting of the text field to enter the search term, the search button and the <div> to display the search results.

JSX

With React, we use an extension of JavaScript called JSX (see the React documentation). JSX allows us to embed HTML elements within JavaScript code which can simplify coding a component if it contains large amounts of static HTML and only a small amount of variable data. You will see examples of JSX below. Because JSX is not native JavaScript, it needs to be converted to standard JavaScript using Babel (we saw this earlier; see here), which must be linked to by your web page.

A simple example

Here is an initial example, this would be saved in a separate JavaScript file:

ReactDOM.render(
<h1>Hello World</h1>,
document.getElementById("div1")
)
What's this doing? Note how the render() method of ReactDOM takes as its first argument something which looks like a string of HTML, but does not have the quotes. This is JSX. The second argument is the DOM node representing the page element we want to place that string in, so here the React code will render an H1 heading reading Hello World inside the element with the ID of div1.

Here are the required links for React and Babel, which need to go in your accompanying HTML page:

<script type='text/javascript' src='https://unpkg.com/react@16/umd/react.development.js' crossorigin></script>
<script type='text/javascript' src='https://unpkg.com/react-dom@16/umd/react-dom.development.js' crossorigin></script>
<script type='text/javascript' src='https://unpkg.com/babel-standalone@6/babel.min.js'></script>
<script type='text/babel' src='react1.js'></script>
The fourth script here (react1.js) is your JavaScript file, but note that it has a type of text/babel because it uses JSX rather than pure JavaScript.

Or, alternatively, we can adopt JavaScript modules to build our React app. Here is an example:

const React = require('react');
const ReactDOM = require('react-dom');

ReactDOM.render(
<h1>Hello World</h1>,
document.getElementById("div1")
)
This is assuming React is installed with npm, e.g:
sudo npm install -g react
sudo npm install -g react-dom
Your code, plus React itself, needs to be converted to a bundle using Browserify and Babel (via babelify), we use the Babel React preset.

Here is an example of a build command:

browserify index.js -t [ /usr/local/lib/node_modules/babelify --presets [ @babel/preset-react ] ] > bundle.js

You could then uglifyify the output to make the bundle more compact:

browserify index.js -t [ /usr/local/lib/node_modules/babelify --presets [ @babel/preset-react ] ] -g [ /usr/local/lib/node_modules/uglifyify ] > bundle.js
Note the -g indicates a global transform, i.e. it will be applied to the React code as well as your own. If you used -t here, the React code would never be uglified.

Using a package.json to automate the build process

With JavaScript projects you can automate the build process with npm and the package.json file. Here is a brief example:

{
    "name": "ReactApp01",
    "devDependencies": {
        "react": "latest",
        "react-dom": "latest"
    },
    "scripts": {
        "build" : "browserify.... as above"
    }
}
The package.json includes the project's name, its dependencies (here, react and react-dom), and a collection of build scripts. Here, there is just one script, build, which performs the browserify/babel sequences used above.

To build the project, we'd just use:

npm run build

Defining components

Specifying components with classes

We can now start looking at how to create individual components. Each component can be represented as a custom tag in JSX. Consider this example:

// If using commonjs
const React = require('react');
const ReactDOM = require('react-dom');
// end if using commonjs

class InputWidget extends React.Component{
    render() {
        return (
            <div>
            <h1>Input your name</h1>
            <fieldset>
            <input type='text' id='name' />
            <input type='button' value='Go!' />
            </fieldset>
            </div>
        );
    }
}

ReactDOM.render(
    <InputWidget />,
    document.getElementById('div1')
)
Note how we create a class called InputWidget. This class defines how the component InputWidget, will be rendered. Note how the render() method of the class returns JSX describing how the component will be rendered, i.e. it will be rendered as a div containing an H1 heading, and a text box and button within a fieldset. We can then include the component as a tag in ReactDOM.render() to actually render it on the web page. Note how we render an <InputWidget> to the element with the ID of div1.

Using CommonJS - make each widget a module

If using CommonJS, it makes sense to place each widget (component) in its own module, export it and then require it from other files. For example:

const React = require('react');

class InputWidget extends React.Component{
    render() {
        return (
            <div>
            <h1>Input your name</h1>
            <fieldset>
            <input type='text' id='name' />
            <input type='button' value='Go!' />
            </fieldset>
            </div>
        );
    }
}
module.exports = InputWidget;
and index.js:
const ReactDOM = require('react-dom');
const InputWidget = require('./InputWidget');

ReactDOM.render(
    <InputWidget />,
    document.getElementById('div1')
)

Props

Props (short for properties) are a key feature in React (see the React documentation). Props allow you to pass parameters to the component by specifying them as attributes on your custom component tag. Here is a modification of the above example to use props:

class InputWidget extends React.Component{
    render() {
        return (
            <div>
            <h1>{this.props.title}</h1>
            <fieldset>
            <input type='text' id='name' value={this.props.defaultName}/>
            <input type='button' value='Go!' />
            </fieldset>
            </div>
        );
    }
}

ReactDOM.render(
    <InputWidget title='Please enter your name:' defaultName='Tim Berners-Lee'/>,
    document.getElementById('div1')
)
Note how in this example we pass two properties title and defaultName to the component:
<InputWidget title='Please enter your name:' defaultName='Tim Berners-Lee'/>
We can then refer to them within the class by referencing the props property of our class. Hence we use this.props.title and this.props.defaultName to refer to each. Note how we can embed variables within JSX using the braces { and }:
<input type='text' id='name' value={this.props.defaultName}/>
Anything between braces will be treated as a JavaScript variable or expression. As well as simple variables, more complex expressions can be embedded within the braces. Thus, here, the title prop will be rendered as an H1 heading, and the defaultName prop will be inserted into the text field.

Lifecycle methods

The React.Component class has a number of lifecycle methods. These are methods which run at various points within the lifecycle of the component, from setting up to shutting down. See the React docs. Two are of particular interest:

Thus, componentDidMount() is a good place to add code to set up such things as event handlers. Here is an example which uses it to attach an event handler to the button within the component. When the button is clicked, an alert box greeting the user will appear.
class InputWidget extends React.Component{

    componentDidMount() {
        document.getElementById("btn1").addEventListener('click',
                e=> {
                    alert(`Hello ${document.getElementById('name').value}`)
                });
    }
    
    render() {
        return (
            <div>
            <h1>{this.props.title}</h1>
            <fieldset>
            <input type='text' id='name' value={this.props.defaultName}/>
            <input type='button' id='btn1' value='Go!' />
            </fieldset>
            </div>
        );
    }
}

ReactDOM.render(
    <InputWidget title='Please enter your name:' defaultName='Tim Berners-Lee'/>,
    document.getElementById('div1')
)

State

React components use the concept of state. A React component can store data it needs to do its job in a state object, which can be retrieved when methods of the component are called. We initialise the state in the constructor. See the React docs.

class InputWidget extends React.Component{

    constructor(props) {
        super(props);
        this.state = { name: this.props.defaultName };
    }
    
    componentDidMount() {
        document.getElementById("btn1").addEventListener('click',
                e=> {
                    alert(`Hello ${this.state.name}`)
                });
    }
    
    render() {
        return (
            <div>
            <h1>{this.props.title}</h1>
            <fieldset>
            <input type='text' id='name' value={this.state.name}/>
            <input type='button' id='btn1' value='Go!' />
            </fieldset>
            </div>
        );
    }
}

ReactDOM.render(
    <InputWidget title='Please enter your name:' defaultName='Tim Berners-Lee'/>,
    document.getElementById('div1')
)
Note how this differs from the previous example.

The general rule is, always store data associated with a component in its state.

Event handling the React way

Our example includes code to handle a click event on a button, attaching the event listener in componentDidMount(). This works, but is not following React conventions for event listening (see the React docs). In React, the convention used for event handling is actually the older-style "set up handlers in HTML" approach (onClick, etc); but this is done in a component's JSX, not the HTML of the web page, so the actual main HTML page remains free of JavaScript.

Here is the previous example rewritten this way:

class InputWidget extends React.Component{

    constructor(props) {
        super(props);
        this.state = { name: this.props.defaultName };
    }
        
    render() {
        return (
            <div>
            <h1>{this.props.title}</h1>
            <fieldset>
            <input type='text' id='name' value={this.state.name}/>
            <input type='button' id='btn1' value='Go!' onClick={()=> alert(`Hello ${this.state.name}`) } />
            </fieldset>
            </div>
        );
    }
}

ReactDOM.render(
    <InputWidget title='Please enter your name:' defaultName='Tim Berners-Lee'/>,
    document.getElementById('div1')
)
Note how we set up an onClick event for the button directly in the JSX code. The event handler is an arrow function that uses the state to alert the name entered. Note how an arrow function with only one statement does not require braces { } surrounding the one statement; the braces following the onClick are for embedding JavaScript within our HTML as we have seen before.

Note also how the event is onClick, not onclick; React uses the convention for event handling of capitalising the first letter of the event type.

Implementing an onChange() event handler

If you try this out, you'll notice that it is impossible to change the text in the text field. Why is this? The text field is tightly bound to the name property of the state, so unless the state changes, the text field will not change. More generally, any element within the component which references the state is tightly bound to the state and will not change unless the state changes. To implement an editable text field, you need to implement an onChange() event handler on the text field to update the state. The next example shows this:

class InputWidget extends React.Component{

    constructor(props) {
        super(props);
        this.state = { name: this.props.defaultName };
    }
        
    render() {
        return (
            <div>
            <h1>{this.props.title}</h1>
            <fieldset>
            <input type='text' id='name' value={this.state.name} onChange={this.updateState.bind(this)}/>
            <input type='button' id='btn1' value='Go!' onClick={()=> alert(`Hello ${this.state.name}`) }  />
            </fieldset>
            </div>
        );
    }
    
    updateState() {
        this.setState({name: document.getElementById('name').value})
    }
}

ReactDOM.render(
    <InputWidget title='Please enter your name:' defaultName='Tim Berners-Lee'/>,
    document.getElementById('div1')
)
Note how the text field now has an onChange event handler, which is the updateState() method of the component. Note how we use bind() to preserve the context of this; we looked at this concept last time. In updateState() we use the setState() method of the component to update the state. setState() takes as an argument an object containing state properties to update; other state properties will remain the same.

Note that you must use setState() to update the state; do not modify the state object directly.

Components containing other components, and sharing state

More complex components will contain sub-components. For example, imagine an extended version of the previous example in which the greeting message appears in a <div> as the user enters their name. You can imagine that there might be one component for the text box, another component for the <div> and an overall "parent" component containing them both.

What you are probably asking is: how can the contents of the first sub-component (the text field) be passed to the second (the <div> ?) The recommended approach (see the React documentation) is to store information that needs to be shared between the two components as state of the parent. We can then pass that state to the sub-components via props. The following example illustrates this:


class AppWidget extends React.Component {
    constructor(props) {
        super(props);
        this.state = { name : props.name };
    }

    updateState(name) {
        this.setState({name: name});
    }

    render() {
        return (
        <div>
        <InputWidget passBackUserInput={this.updateState.bind(this)} name={this.state.name}/>
        <ResultsWidget name={this.state.name} />
        </div>
        )
    }
}

class InputWidget extends React.Component {


    updateName() {
        var n =  document.getElementById('name').value;
        this.props.passBackUserInput(n);
    }

    render() {
        return (
        <div>
        <h1>Enter your name</h1>
        <fieldset>
        <input type='text' id='name' value={this.props.name} onChange={this.updateName.bind(this)} />
        </fieldset>
        </div>
        )
    }
}

class ResultsWidget extends React.Component {
    render() {
    return (
        <div style={{border: '1px solid black'}}>{this.props.name}</div>
    )
    }
}

ReactDOM.render(
    <AppWidget name='Fred'  />,
    document.getElementById('div1')
)
What is happening here?

Further Reading on React website

Arranging multiple widgets

Here is an example of how you might arrange multiple widgets in one parent widget. You'd place them inside a given <div> or other page component:

class MappingAppWidget extends React.Component {
    render() {
        return (
        <div id='map'>
        <MapWidget />
        </div>
        <div id='poiList'>
        <PointOfInterestListWidget />
        </div>
        );
    }
}
The MappingAppWidget contains two sub-components: MapWidget, and PointOfInterestListWidget, each placed in their own separate divs. This could be used in a mapping app in which a user can search for points of interest, which appear in a list next to the map.

Exercise

Build a version of your HT-Tracks AJAX site using React. To do this:

  1. Create a web page which links in the appropriate React and Babel libraries, and contains a single <div>. Your React components should be rendered to this <div>.
  2. Now start working on your React code. Initially, create a single component with a search box for the artist, a button, and a results <div>
  3. Next, connect the button to an event handler function within the component. The event handler should initially display an alert box with a message of "You entered the artist " plus the artist name.
  4. Once you've got this working, perform an AJAX request to your Session 1 web service and update the results <div> to contain the results from the server. You should display title, artist, year and quantity.
  5. Once you've completed this, try using a parent component and two sub-components, one containing the text field and button, and another containing the results <div>. By storing the JSON returned from the AJAX in the parent component's state, update the search <div> every time the text field changes. Also, use the JSON returned from the web service as a prop of the results component. For this to work, you need to ensure that your web service uses LIKE for pattern matching.
  6. Alter your code so that each result has an Order button. When the Order button is clicked, simply alert the song ID for now.
  7. Advanced, for strong programmers only: have a go at performing a live update of the quantity in stock for each search result when you click on the Order button. You do not need to actually update the quantity in stock in the database, just the Quantity figure in the search results on the front-end. To do this, create an array within the search results component's state. This array should contain the quantity in stock for each song and should be indexed using the song ID. Whenever the JSON prop changes in the results div, you should repopulate this array. Hint: there is a lifecycle method, componentWillReceiveProps(), which runs every time the props of a component update. It takes the updated props as a parameter. Initialise the array in this method, so that each song in the JSON has a corresponding quantity entry in the array, indexed by song ID. When a user clicks on the Order button, increase the corresponding entry of the array by one. If the array is stored as state and this state is referenced when you output the quantity, you will find the quantity in stock automatically updates.