50.003 - Frontend Development using React.js
Learning Outcomes
By the end of this unit, you should be able to
- Explain React Components
- Explain React States
- Explain React Lifecycle
- Explain React Effect
- Develop a frontend app using React JS
Issues with the ExpressJS + AJAX approach
- routes / controllers for view rendering and AJAX calls are intertwined
- client side JS in the views are not so reusable and not so testable
Front End Web App
A front end web app detachs the view components from a monolith web app.
For example, the following diagram shows that a react.js
app runs separately from
the express.js
web app.
In this settings, the express.js
becomes a backend API server. The views are now hosted in the react.js
app in a separate host address and port. When the client first accesses a page, it visits the react.js
app for the desired web page. The React.js
app renders the web page by making an API call to the backend server. When it has the JSON data, it inserts the data into the result rendered. In addition, it bundles all the client side JS codes and other static content and sends them to the client browser as the response.
The bundle JS codes becomes the client app which runs in the client browser. In the subsequent requests (initiated by UI control), the AJAX calls are made directly from the client browser through the client app. This frees up some of the processing time from the web server as more codes are executed within the client browser.
Furthermore, the client side JS codes are hosted and executed (at least during the first request) on the react.js
app server. This allows JS codes to be better modularized as a separate project, thus testing can be conducted systematically.
Building a ReactJS App
To initiate the project, we need to execute
We find a project folder with the following content.
.
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ ├── logo512.png
│ └── logo192.png
└── src
├── index.css
├── index.js
├── App.css
├── App.js
├── App.test.js
├── setupTests.js
├── reportWebVitals.js
└── logo.svg
src/index.js
is the main function (application entry point).
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
From the above we noted a few points,
- We are using ES6 syntax
- We mix JavaScript syntax with HTML (XHTML) syntax (as well as CSS). The syntax is called JSX. Using JSX is not mandatory but it is recommended. You can still use pure JS and React syntax to create browser components.
index.js
is creating a page by referencing to theroot
element and render theApp
element inside.- the
App
element is imported fromsrc/App.js
.
Let's take a look at the App.js
import logo from "./logo.svg";
import "./App.css";
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
To run the app, we run
which will start the reach app at https://localhost:3000
. To stop it, we press control-C in the terminal.
As we are going to run both react server and express server simultanously, we need to run the react app in a different port.
In the project root folder, create a file .env
with the following content
Re-run npm start
, it will re-start the react web app at https://localhost:3001
.
App.js
Let's modify src/App.js
to see how the web app change.
import logo from "./logo.svg";
import "./App.css";
function App() {
return (
<div>
<h1> Echo App </h1>
<div> This is a test app for ESC. </div>
</div>
);
}
export default App;
As we save the file, the browser with the web app running will automatically refresh. We see
(Can you modify the code so it appears similar to the above screenshot?) If we check the source of the HTML page in the browser,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/manifest.json" />
<!--
Notice the use of in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<script defer src="/static/js/bundle.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Nothing from the above reflects the change that we made in App.js
.
If we follow the link /static/js/bundle.js
in the browser, we find the following
code snippet in the JavaScript file.
function App() {
return /*#__PURE__*/ (0,
react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(
"div",
{
children: [
/*#__PURE__*/ (0,
react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(
"h1",
{
children: " Echo App ",
},
void 0,
false,
{
fileName: _jsxFileName,
lineNumber: 10,
columnNumber: 9,
},
this
),
/*#__PURE__*/ (0,
react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__.jsxDEV)(
"div",
{
children: " This is a test app for ESC. ",
},
void 0,
false,
{
fileName: _jsxFileName,
lineNumber: 11,
columnNumber: 11,
},
this
),
],
},
void 0,
true,
{
fileName: _jsxFileName,
lineNumber: 9,
columnNumber: 7,
},
this
);
}
_c = App;
React JS Compilation
React frameworks focus in building applications which can be targeted for multiple platforms, i.e. web browser, windows application, mobile app and etc. It takes a single stack of source languages, i.e. JSX and CSS, compiles them into a single output. In this case, since we are using React JS to build a web app, the output will be in a single JavaScript file.
Image credits https://nextjs.org/learn/foundations/how-nextjs-works/compiling
Components
Unlike Web Application developed using other frameworks, e.g. express.js, React JS develops applications with a single page in mind. Component
is the building block of React app. Component
in react can be written using JSX or pure React syntax React.createElement
. We will use JSX syntax in this module. With the control of basic components, the React JS app is able to dynamically update and replace components and sub-components within the same page in the event of user interaction and data update.
In our running example, function App
constructs a component which is used by the index.js
at the root level. Function App
returns the static HTML code as result.
Let's make some change to App.js
as follows,
import logo from "./logo.svg";
import "./App.css";
import Echo from "./Echo";
function App() {
return (
<div>
<h1> Echo App </h1>
<Echo />
</div>
);
}
export default App;
In the above we import and include an Echo
component from ./src/Echo.js
, which is defined as as follows,
function Echo() {
return <div>This message is defined in the Echo component.</div>;
}
export default Echo;
After reloading the app in the browser, we have
Top Down design process (static)
From the earlier example we see a hierachical structures in the components and elements used in our app.
- index.js
- App
- div
- h1
- Echo
- div
- text
When we design the UI using React JS we should follow and exted the same structure.
Suppose we would like to build a frontend app to interact with the express.js app by submitting new messages and displaying the list of submitted messages. By breaking down the UI components we might have the following
By following the UI structural breakdown, we modify the Echo.js
as follows,
function NewMessageBar({ message, onSubmitClick }) {
return (
<div>
<input type="text" placeholder="" value={message}></input>
<button onClick={onSubmitClick}> Submit </button>
</div>
);
}
function MessageList({ messages }) {
let rows = [];
for (let i in messages) {
rows.push(
<tr>
<td>{messages[i].time}</td>
<td>{messages[i].msg}</td>
</tr>
);
}
return (
<table>
<tbody>
<tr>
<th>Date Time</th>
<th>Message</th>
</tr>
{rows}
</tbody>
</table>
);
}
function Echo() {
const messages = [
{ time: new Date().toDateString(), msg: "hello" },
{ time: new Date().toDateString(), msg: "bye" },
];
return (
<div>
<NewMessageBar message="" onSubmitClick={() => {}} />
<MessageList messages={messages} />
</div>
);
}
export default Echo;
In the above version of Echo.js
, we define two sub components, namely NewMessageBar
and MessageList
. Note that both functions take some objects as the arguments, these objects arguments are referred as props
in React's terminologies. Refer here for more details about props
in react.
NewMessageBar()
takes an object with two attributes,message
andonSubmitClick
.message
stores the text value in the text field andonSubmitClick
is a callback when theSubmit
button is clicked. The function returns a text input field and a buttonSubmit
.MessageList()
takes an object with an attribute,messages
which is a list of message objects. It returns an HTML table whose rows are constructed by mapping each message in the list to a row of the table.Echo()
, we hard code the data (which is supposed to be returned by the AJAX call to the backend, will be implemented later), and pass them toMessageList
component during the component construction.
Note that we might re-write the component definition using class instead of function. Notice the difference on how props are accessed here.
class NewMessageBar extends React.Component {
constructor(props) {
this.message = props.message;
this.onSubmitClick = props.onSubmitClick;
}
render() {
return (
<div>
<input type="text" placeholder="" value={this.message}></input>
<button onClick={this.onSubmitClick}> Submit </button>
</div>
);
}
}
It is up to you to use either class
or function
syntax in creating React components.
Props
As shown in the earlier example, props are the objects arguments being passed to the constructor of a React Component or function argument enclosed with curly brackets. Props carried data and information passed down from the use-site, (or the parent component).
Make it interactive (dynamic)
Now we need to make the echo app functional. First let's make the button click to respond to the text entered in the input text field.
We modify NewMessageBar
as follows,
function NewMessageBar({ message, onMessageChange, onSubmitClick }) {
return (
<div>
<input
type="text"
placeholder=""
value={message}
onChange={(e) => {
onMessageChange(e.target.value);
}}
></input>
<button onClick={onSubmitClick}> Submit </button>
</div>
);
}
We introduce a new props attribtue onMessageChange
which is used as the handler for the text field onChange
event. Without this, the text field is read-only.
Next we modify the Echo
function as follows,
function Echo() {
const [msgTxt, setMsgTxt] = useState("");
function handleSubmitClick() {
alert("clicked " + msgTxt);
}
const messages = [
{ time: new Date().toDateString(), msg: "hello" },
{ time: new Date().toDateString(), msg: "bye" },
];
return (
<div>
<NewMessageBar
message={msgTxt}
onMessageChange={setMsgTxt}
onSubmitClick={handleSubmitClick}
/>
<MessageList messages={messages} />
</div>
);
}
and import the following:
State
The useState(initialState)
is one of React Hooks which allows you to have a getter and setter without writing a class. This function returns two values (in the previous example, the two values are captured in msgTxt
and setMsgTxt
). The first value is the current state which in the first render will have the initialState
value. The second "value" is the setter function that lets you update the state and trigger a re-render phase of React lifecycle (lifecycle will be discussed later).
In addition, we define a function handleSubmitClick
which is used as the event handler of the Submit
button click. Now we can interact with the app by entering the text in the message text field and pressing the button to trigger a pop up.
Interfacing with the backend app
Lastly we need to remove the hard-coded data and make use the data returned by the backend API end-point. Adjust the Echo
function as follows:
function Echo() {
const [msgTxt, setMsgTxt] = useState("");
const [messages, setMessages] = useState([]);
function handleSubmitClick() {
alert("clicked " + msgTxt);
submitNewMessage();
}
async function submitNewMessage() {
const response = await fetch(`http://localhost:3000/echo/submit`, {
method: "POST",
body: `msg=${msgTxt}`,
headers: {
"Content-type": "application/x-www-form-urlencoded",
},
});
const text = await response.text();
const json = JSON.parse(text);
setMessages(json);
}
return (
<div>
<NewMessageBar
message={msgTxt}
onMessageChange={setMsgTxt}
onSubmitClick={handleSubmitClick}
/>
<MessageList messages={messages} />
</div>
);
}
The changes we made include
- remove the hard coded messages.
- introduce a new state
[messages, setMessages]
. - define a function
submitNewMessage()
which make the HTTP POST api request to the backend app and update the message. - update the body of
handleSubmitClick()
to incorporatesubmitNewMessage()
.
When the button is clicked, a sequence of operations were carried out in the following order.
handleSubmitClick()
invokessubmitNewMessage()
submitNewMessage()
make the api call- when the api call returns the result, the body text is extracted and parsed as JSON.
setMessages()
update the messages state, which triggers the re-rendering of theEcho()
component and all of its children (NewMessageBar
andMessageList
).
Next, we can reuse our previous AJAX demo project my_mysql_app
as the backend of our react frontend. In separate terminal, run npm start
for my_mysql_app
for the backend and npm start
for the react app (make sure that they run in separate port, 3001 for react and 3000 for express if you follow this note).
We will encounter an error of the api Call. By checking the console in the browser, we find that it is related to the Cross Origin Resource Sharing (CORS) restriction, which prevents the JS from React app to pull data from an exernal app.
To lift the restriction, we modify the router code at the backend app, i.e. my_mysql_app
.
For all the AJAX call end-points to be accessed by our react app (which is running on port 3001), we add the following statement before the res.send()
is called.
Lastly, let's make the app to displays all the existing messages when it is firstly loaded. Naively, we might added the following function definitions and function call statements before the return
statement in the Echo()
function.
async function submitNewMessage() { ... } // existing
async function initMessages() { // newly added
const response = await fetch(`http://localhost:3000/echo/all`);
const text = await response.text();
const json = JSON.parse(text);
setMessages(json);
}
initMessages(); // newly added, causing loop
return ( ... ); // existing
With this fix, our App seems to get into an infinite loop. This is because initMessages()
is now part of the render phase routine of the Echo
component. When the Echo
is rendered, the setMessages
is invoked, which causes the component to re-render.
Life Cycle of React Components
In order to fix this issue, we need to understand the life cycle of React Component.
image credits https://levelup.gitconnected.com/componentdidmakesense-react-lifecycle-explanation-393dcb19e459
When a React Component is spawned, server methods are called at the server end (i.e. when our react app is hosted). After which, it calls componentDidMount
, then goes into a kind of loop driven by the changes of states or props of react component. Each change of states or props will re-renders the component in the browser. The loop goes on until it unmount, (i.e. the component is destroyed).
To access each of the stages of the React Component, let's rewrite the Echo
component in class style
import { Component } from "react";
class Echo extends Component {
constructor(props) {
super(props);
this.state = { msgTxt: "", messages: [] };
}
componentDidMount() {}
componentDidUpdate() {}
async submitNewMessage() {
const response = await fetch(`http://localhost:3000/echo/submit`, {
method: "POST",
body: `msg=${this.state.msgTxt}`,
headers: {
"Content-type": "application/x-www-form-urlencoded",
},
});
const text = await response.text();
const json = JSON.parse(text);
this.setMessages(json);
}
setMsgTxt(s) {
this.setState({ msgTxt: s, messages: this.state.messages });
}
setMessages(l) {
this.setState({ msgTxt: this.state.msgTxt, messages: l });
}
handleSubmitClick() {
this.submitNewMessage();
}
render() {
return (
<div>
<NewMessageBar
message={this.state.msgTxt}
onMessageChange={(s) => this.setMsgTxt(s)}
onSubmitClick={() => this.handleSubmitClick()}
/>
<MessageList messages={this.state.messages} />
</div>
);
}
}
In the above version, we refactor the useState()
parts by using the inherited attribute of the Component
class: state
and setState()
. In the above example, we put msgTxt
and messages
in this.state
and write setMsgTxt
and setMessages
methods using this.setState
. Since functions are different from method internally, we need to turn the props arguments into lambdas, e.g. onMessageChange={(s) => this.setMsgTxt(s)}
instead of onMessageChange={setMsgTxt}
. Finally we override the componentDidMount
and componentDidUpdate
methods.
Exercise (Non Graded)
Add some log messages to the constructor
, componentDidUpdate
, componentDidMount
and render
.
Run the program to observe the order of the log messages being printed. Can you identify which message is printed from which part of the life cycle?
As suggested by the life cycle, it is better to initilize the state outside the loop, i.e. componentDidMount
.
Hence we change the componentDidMount
as follows, and introduce a new function to retrieve all existing messages from the API and update the state.
async initMessages() {
const response = await fetch(`http://localhost:3000/echo/all`);
const text = await response.text();
const json = JSON.parse(text);
this.setMessages(json);
}
componentDidMount() {
this.initMessages();
}
With this change, the App behaves as what we want.
Effect
Some of us might argue, rewriting the component in the class form allows us to access the different stage of the life-cycle. The code could be too verbose.
To achieve the same result in the function-form component, we need to use the useEffect
hook.
Recall the earlier version in function-form.
function Echo() {
const [msgTxt, setMsgTxt] = useState("");
function handleSubmitClick() {
submitNewMessage();
}
const [messages, setMessages] = useState([]);
async function submitNewMessage() {
const response = await fetch(`http://localhost:3000/echo/submit`, {
method: "POST",
body: `msg=${msgTxt}`,
headers: {
"Content-type": "application/x-www-form-urlencoded",
},
});
const text = await response.text();
const json = JSON.parse(text);
setMessages(json);
}
useEffect(() => {
// useEffect
console.log("from effect");
});
return (
<div>
<NewMessageBar
message={msgTxt}
onMessageChange={setMsgTxt}
onSubmitClick={handleSubmitClick}
/>
<MessageList messages={messages} />
</div>
);
}
The statement calling useEffect
defines a call-back to be called every time
the component re-renders. We can restrict the call-back to be triggered only when certain states have updated, e.g.
will be triggered whenever msgTxt
state changes.
If the 2nd argument is an empty list []
. the call-back will only be triggered when the component is mounted. Hence to achieve what we want, i.e. to get all the existing messages and assign them to the state messages
, we need to include the following in the body of the Echo
function.
async function initMessages() {
const response = await fetch(`http://localhost:3000/echo/all`);
const text = await response.text();
const json = JSON.parse(text);
setMessages(json);
}
useEffect(() => {
initMessages();
}, []);
We could think of useEffect
with only 1 argument is behaving like componentDidUpdate
. useEffect
with a 2nd argument as a non empty list is behaving like componentDidUpdate
with conditional update (depending on whether the given state has changed). When useEffect
is used with a 2nd argument as an empty list it is behaving like componentDidMount
.
Further Reading
- Thinking in React
https://react.dev/learn/thinking-in-react
- React JS and Express JS
https://www.freecodecamp.org/news/create-a-react-frontend-a-node-express-backend-and-connect-them-together-c5798926047c/
- React JS life cycle
https://levelup.gitconnected.com/componentdidmakesense-react-lifecycle-explanation-393dcb19e459
- Multi page React
https://www.geeksforgeeks.org/how-to-create-a-multi-page-website-using-react-js/
- Multi page with state
https://www.microverse.org/blog/how-to-get-set-up-with-react-redux-in-your-next-multi-page-project
- React JS and Express JS with Login and Token authentication
https://www.digitalocean.com/community/tutorials/how-to-add-login-authentication-to-react-applications