React (see reactjs.org and react.dev) is a JavaScript user interface library, originally developed by Facebook, which allows complex JavaScript user interfaces 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 item.
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.
React is so-called because it allows us to develop reactive UIs, which automatically update when the application's data changes without us having to implement logic to re-build the UI when that data changes. It does this through the use of state, which will be described below.
Here is the React Hello World
application:
import React from 'react'; import ReactDOM from 'react-dom/client'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<h1>Hello World!</h1>);
If you look at this code, you should notice that the render()
method, on the last line, takes a string of HTML which is not in quotes. What is this? It's 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 - see below.
So what's the code example above doing?
First of all we create a root element for our React app. This is done with the createRoot()
method, part of ReactDOM
. This takes an existing HTML element as an argument - here, the element with the ID of root
We then call the render()
method of our root elementand specify something which looks like a string of HTML, but does not have the quotes. This is JSX. So here the React code will render an H1 heading reading Hello World inside the element with the ID of root
.
Formerly, the recommended way of creating a React app was with create-react-app, which automatically downloads dependencies and writes build scripts for you. However create-react-app
is no longer maintained and therefore its use is not recommended. It still works, but it may break in the future.
Instead, you should create and build it from scratch, making use of npm
to install dependencies, Webpack to build, and a third tool, Babel, which we will look at below.
The Hello World React app configuration is available on GitHub at https://github.com/nwcourses/react-starter. We will go through each of its components.
Babel is a transpiler: a piece of software which converts between different JavaScript versions. Why is this useful? First of all it allows the newest versions of JavaScript, which may not be supported by older browsers, to be transpiled into older versions which browsers do support. Formerly, Babel was used extensively to transpile ECMAScript 6 upwards into the older version of JavaScript, ECMAScript 5, when the former was not widely supported. Now it is less used for this purpose as ECMAScript 6 and higher are widely supported by browsers.
In the context of React, Babel is important. JSX is not natively supported by browsers so we need to transpile it using Babel into standard JavaScript.
We will start our examination of the starter repository with the package.json
file, used by npm
to download dependencies and build the app.
{ "name": "react-starter", "version": "0.0.1", "scripts": { "build": "npx webpack" }, "devDependencies": { "@babel/core": "^7.23.0", "@babel/preset-env": "^7.22.20", "@babel/preset-react": "^7.22.15", "webpack": "^5.88.2", "webpack-cli": "^5.1.4", "babel-loader": "^9.1.3" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" } }
Hopefully some of this will be familiar to you from last week:
scripts
section specifies the scripts we can run with npm
: you can see there is a build
script to build it with Webpack. You can execute this script with:
npm run build
devDependencies
. They include:
@babel/core
: the core part of Babel.@babel/preset-env
: for transpiling ES6 to ES5 (for maximum compatibility the output is in ES5).@babel/preset-react
: for transpiling React JSX code.webpack
, webpack-cli
and webpack-dev-server
, as last week.babel-loader
: Webpack "plugin" for Babel, linking the two together and allowing you to specify which files will be handled by Babel in webpack.config.js
.Finally the dependencies
include React itself, and react-dom
which allows DOM manipulation with React.
Next we will discuss the Webpack configuration file, webpack.config.js
, which we also saw last week. Here is the webpack.config.js
for this project:
const path = require('path'); module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'public/dist'), filename: 'bundle.js' }, optimization: { minimize: false }, module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: { loader: "babel-loader" } } ] } }
We went through most of this last week. However note the rules
section within module
. We looked at this last week in the context of including CSS files and images with Webpack, and specifying custom loaders to handle these and include them in the bundle. Hopefully you can see that this is testing for files with extension .js
or .jsx
, and if they occur, handle them with the babel-loader
(i.e. they will be transpiled with Babel). We also exclude the node_modules
folder as we do not want to wastefully run Babel on any third-party libraries we are using, in which the JavaScript will already be suitable for bundling without the use of Babel.
Note how the index.js
is now placed within a src
folder. This is a convention commonly used in Webpack-built JavaScript applications. The raw source code does not need to be delivered to the client (the browser), only the built bundle. So there is no need to place the source code inside public
, whose role should be primarily to contain publicly-accessible code.
Finally we have a third configuration file: .babelrc
. This is the Babel configuration file.
{ "presets": ["@babel/env", "@babel/react"] }
This tells Babel which transformations to enable. The env
transformation is the standard newer-to-older JavaScript transformation, while the react
transformation allows transformation of React (JSX) code into standard JavaScript.
One thing we didn't cover last time when looking at Webpack is how to automatically regenerate the bundle if any of the client-side code changes. You can do this by running Webpack in watch mode, e.g.:
npx webpack --watchSee the documentation. This is similar, in some ways, to PM2 in watch mode. However if you run Webpack in watch mode from the command prompt, you will be unable to enter any other commands as Webpack will be continuously running. So if you want to run any other commands you will need to open a separate command prompt.
As we've already briefly discussed above, React applications consist of a series of reusable components. A component is a user-interface element focused on one particular item, which can be reused in other applications. So we could for example have a clock component, or a login component, or a component which searches for music (via AJAX) and displays the results within the component.
React components are written using functions which return JSX. Here is an example:
import React from "react"; function Greeting() { return <h1>Hello from the Greeting component!</h1>; } export default Greeting;Note how we write a function called
Greeting()
which returns JSX to display a paragraph containing the text "Hello!". Ideally, each React component should go in its own file (so this could be saved in greeting.js
) and then imported into our application. To better organise our code, we can store it in a folder called components
within our src
folder. This allows us to reuse the component across multiple applications. Therefore, we are exporting the function so it can then be imported into other code.
To use a component, we incorporate it into JSX by writing a tag corresponding to the functio name. So we could incorporate the above component into JSX with the tag <Greeting>. Here is an example of an index.js
making use of the component:
import React from "react"; import ReactDOM from "react-dom/client"; import Greeting from "./components/greeting.js"; const root = ReactDOM.createRoot( document.getElementById("root") ); root.render(<Greeting />);Note how we import the component from the appropriate module (
greeting.js
within the components
folder) and pass in the <Greeting> tag into the render()
function.
In most cases, we need to customise the component. For example, we might want a greeting component to greet the user by name. How can we pass the name into the component? The answer is to use props - or properties. Props are specified in the same way as HTML attributes using key/value pairs in the tag, e.g.:
root.render(<Greeting person="John" />);Here, the
<Greeting>
component is being passed a person
prop with a value of "John"
.
Here is how we could use the prop in the component:
function Greeting(props) { return <p>Hello {props.person}!</p>; } export default Greeting;Note how we specify the props as a parameter to the function. This is an object containing all the props that were passed in; we access the individual prop using its name i.e.
props.person
and then include it in our JSX. Note how we can embed JavaScript expressions within JSX by surrounding them with curly brackets { and }. Unlike with backtick syntax, we do not need a dollar sign outside the curly brackets.
Here is another example with two props:
function Greeting(props) { return <p>Hello {props.firstname} {props.lastname}!</p>; } export default Greeting;which would be rendered, for example, as follows:
root.render(<Greeting firstname="John" lastname="Brown" />);
We can use an alternative syntax for props as parameters using object destructuring This is a general JavaScript (not React specific) feature which allows us to allocate properties of a JavaScript object into variables of the same name. Here is a simple non-React example:
function displaySong({title, artist}) { console.log(`${title} ${artist}`); } const songObject = { title: 'Wonderwall', artist: 'Oasis' }; displaySong(songObject);In this simpler example we create an object
songObject
containing two properties title
and artist
. We then pass this to the displaySong()
function. However, note that displaySong()
destructures the object into two variables, title
and artist
, corresponding to the properties of the same name. Likewise with React props, the props to a component become an object, which is then destructured in the component function.
So we could write the above React example using destructuring as follows:
function Greeting({firstname, lastname}) { return <p>Hello {firstname} {lastname}!</p>; } export default Greeting;Note how the two props
firstname
and lastname
are destructured into the respective individual variables.
An important point about JSX is that inline styles are specified with a different syntax.
<p style={{ backgroundColor: 'yellow' }}>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-colorbecomes
backgroundColor.
React components use the concept of state. A React component can store data it needs to do its job in a group of state variables, which can be retrieved when methods of the component are called. This is central to how React works. We store the application's data in its state, and describe the components to be rendered using a mix of HTML and state variables. The result will be that when we update the state, the application will be automatically re-rendered to use the current values stored in the state. Thus, we do not have to manually update the UI each time the application's data changes.
For example here is a simple component which allows the user to enter a name and updates a greeting message with the most recent name that was entered. The name is stored in state.
import React from 'react'; function InteractiveGreeting() { const [name,setName] = React.useState("No name"); return( <div> <h2>Enter your name</h2> <input id='txtName' /> <div>Hello {name}</div> <input type='button' value='update' onClick={updateStateName} /> </div> ); function updateStateName() { setName(document.getElementById('txtName').value); } } export default InteractiveGreeting;To explore this in detail: The line:
const[name, setName] = React.useState("No name");sets up a state property called name and sets it up to initially contain the value "No name". It also creates a function setName() which is used to update the state; this function is created for you automatically in the background. React.useState() always returns an array containing a state variable and a matching function to update it. Note the destructuring syntax:
const [name, setName] = ....;This causes the array returned by React.useState() to be destructured into two individual variables,
name
and setName
.
Multiple state variables can be setup in this way, and each state variable has a corresponding set method to update that state variable. Note how the content of the div
is set to the state variable name. The interesting thing here is that whenever the name
property of the state changes, the div
will be automatically updated, as name
always refers to the current state. This shows how state can be more useful than just storing the name in a regular variable, in which case this auto-update would not happen. Essentially we are binding the div
to the state variable name
, ensuring that the div
is always displaying the current name.
But how do we update the state? Hopefully you can see that we handle a click event on the button within the component, and call a custom function updateStateName()
when it's clicked. If you look at this function (within our component), you can see that it uses setName()
- the setter function we obtained from React.useState()
- to update the state to the contents of the form field with the ID txtName
.
So the end result will be that the div
keeps in sync with whatever the user enters in the text field. This automatic update of the UI to reflect current state is possibly the most fundamental principle of React.
The above example included code to handle a click event on a button. In React, the convention used for event handling is to set up an event handler in the HTML rather than using addEventListener()
. 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.
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.
The handler for onClick
is the updateStateName()
function: as updateStateName
is a variable within JSX, we have to surround it with braces.
<input type='button' value='update' onClick={updateStateName} />
Consider this new version of the InteractiveGreeting example. As well as displaying the name in the div it also displays it in the input box.
import React from 'react'; function InteractiveGreeting2() { const [name,setName] = React.useState("No name"); return( <div> <h2>Enter your name</h2> <input id='txtName' type='text' value={name} /> <div>Hello {name}</div> <input type='button' value='update' onClick={updateStateName} /> </div> ); function updateStateName() { setName(document.getElementById('txtName').value); } } export default InteractiveGreeting2;
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:
import React from 'react'; function InteractiveGreeting3() { const [name,setName] = React.useState("No name"); return( <div> <h2>Enter your name</h2> <input id='txtName' type='text' value={name} onChange={updateStateName} /> <div>Hello {name}</div> <input type='button' value='update' onClick={updateStateName} /> </div> ); function updateStateName() { setName(document.getElementById('txtName').value); } } export default InteractiveGreeting3;
Note how the text field now has an onChange event handler, which is the updateStateName() function of the component. In updateStateName() we use the setName() method, remember that when we create the hook we obtain a series of variables, the state variable and a setter to update the state with.
Note that you must use the setter method to update the state; do not modify the state variable directly.
Often in React, you have to loop through an array or list of data, and generate JSX from the array (for example, create an HTML list from the array). The example below shows how to do this:
.... function ListComponent() { const modules = ["WAD", "OODD", "MAD", "Databases"]; const modulesList = modules.map(module => <li key={module}>{module}</li> ); return <ul>{modulesList}</ul>; } ....
Here we are performing a mapping to convert an input array into an output array using a transformation defined with an arrow function. See the Mozilla documentation for more.
Here we are mapping an array of modules into an array of <li>
list items containing the module. This mapped array is stored in the variable modulesList
. We are then returning a <ul>
containing the list items from the component.
Note also how each list item has a unique key, equal to the current module. React uses this to keep track of which item in the list is which, if items in the list are modified or deleted. We don't do this here, but nonetheless you should always give items in a rendered list of items a unique key.
The next example combines the discussion on state with the discussion on loops and arrays above, to implement a shopping cart by storing an array of items in state.
import React from 'react';
let itemId = 1;
function ShoppingCart({store}) {
const [cart, setCart] = React.useState([]);
const cartHtml = cart.map ( item => <li key={item.id}>{item.name}</li>);
return (
<div>
<h1>{store} Shopping Cart</h1>
<div>
<h2>Add something to your shopping cart</h2>
<fieldset>
<input type='text' id='item' />
<input type='button' value='Go!' onClick={addItem} />
</fieldset>
</div>
Here is your shopping cart:
<ul>
{cartHtml}
</ul>
</div>
);
function addItem() {
const items = structuredClone(cart);
const newItem = {
id: itemId++,
name: document.getElementById('item').value
};
items.push(newItem);
setCart(items);
}
}
export default ShoppingCart;
This is quite a bit more complex.
Note how we:
addItem()
. This runs when the user clicks the "add item" button.
structuredClone()
(we cannot add directly to an array stored in state, we must make a clone, add to that, and reset the state to the clone).newItem
), containing fields for its ID and its name. The former is an auto-incrementing integer, the latter is what the user entered.items.push(newItem)
), before updating the cart variable in the state to the clone (with the new item added) with setCart()
.map()
method of cart to convert it to an array of JSX, ready for rendering, as in the last example. Each cart item is converted to an HTML list item (li
), containing the item's name.Note that the JSX returned by a component function must have a
single root element. Here is an example, note that MappingAppComponent
contains a MapComponent
and a PointsOfInterestListComponent
but also contains a div
to contain them both as a root element for this component.
function MappingAppComponent() { return ( <div> <MapComponent /> <PointOfInterestListComponent /> </div> ); } export default MappingAppComponent;
You should allow up to an hour to do Exercises 1-3 and two hours for Exercise 4, which requires you to create much more code from scratch.
Clone the repository:
https://github.com/nwcourses/react-starterBuild via Webpack and test the Hello World example out by starting the provided Express server.
Greeting
component above (the version with first name and last name props). Save it in a components
folder within your src
folder. Add a third prop, age
, to the above component and pass in an age from index.js
. Display the age in the component, e.g:
Hello James, your age is 18!Ensure that you import the component from your
index.js
!
age
state variable is 18+ or not. The ternary operator allows you to write a ternary expression, which is a shorthand version of an if
statement. Here is a non-React example which displays a name, or an error if the user enters a blank string:
const name = document.getElementById("txtName").value; document.getElementById("message").innerHTML = (name == "" ? "ERROR: Please enter a name!" : `Hello ${name}`);In your case, however, you should include the ternary expression as an expression within your JSX. Note the general syntax of a ternary expression:
condition ? result if condition is true: result if condition is false
In your Greeting
component from Exercise 2, add code to style the div according to a background colour defined in a prop colour
. Modify your index.js
so you pass the colour
prop into the component.
Build a version of your HitTastic! AJAX site using React, step-by-step as follows:
react-starter
project, as above.InteractiveGreeting3
example above. Also, include a state variable containing the current artist, and a message within a <div> telling the user what artist the user entered. In this message, display the artist from the state.JSON.stringify()
.map()
to convert your array of objects to JSX in your component and return it, as shown in the shopping cart example above.