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.
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.
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.
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-domYour 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.jsNote 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.
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
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.
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 (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.
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:
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') )
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.
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.
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.
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?
<div style={{border: '1px solid black'}}>The outer curly brackets indicate that we are embedding JavaScript within HTML within JSX, as before. The inner brackets indicate that we are defining a JavaScript object to represent the style, rather than ordinary CSS. The same rules as normal for accessing styles within JavaScript apply, i.e. style properties with dashes become camel case, for example background-color becomes backgroundColor.
<InputWidget passBackUserInput={this.updateState.bind(this)} name={this.state.name}/>What we are doing here is passing a callback method, this.updateState(), into the InputWidget as a prop named passBackUserInput. You can do this; methods can be passed as props as well as simple data. updateState() updates the name property of the parent's state to the name passed in as a parameter.
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.
Build a version of your HT-Tracks AJAX site using React. To do this:
var output = this.props.json.map( song=> Title: {song.title}<br /> ) return output;What is happening here? If this.props.json is an array containing the JSON results, map returns a new array with each member of the array (each song) mapped to the resulting JSX string, i.e. Title: {song.title}<br />. Thus, the array output will be an array of JSX for each song. We return this from render() as normal.