Skip to content

50.003 - Frontend Development using React.js

Learning Outcomes

By the end of this unit, you should be able to

  1. Explain React Components
  2. Explain React States
  3. Explain React Lifecycle
  4. Explain React Effect
  5. Develop a frontend app using React JS

Issues with the ExpressJS + AJAX approach

  1. routes / controllers for view rendering and AJAX calls are intertwined
  2. 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

npx create-react-app my_react_app
cd my_react_app

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,

  1. We are using ES6 syntax
  2. 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.
  3. index.js is creating a page by referencing to the root element and render the App element inside.
  4. the App element is imported from src/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

npm start

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

PORT=3001

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 and onSubmitClick. message stores the text value in the text field and onSubmitClick is a callback when the Submit button is clicked. The function returns a text input field and a button Submit.
  • 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 to MessageList 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:

import { useState } from "react";

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 incorporate submitNewMessage().

When the button is clicked, a sequence of operations were carried out in the following order.

  1. handleSubmitClick() invokes submitNewMessage()
  2. submitNewMessage() make the api call
  3. when the api call returns the result, the body text is extracted and parsed as JSON.
  4. setMessages() update the messages state, which triggers the re-rendering of the Echo() component and all of its children (NewMessageBar and MessageList).

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.

res.set("Access-Control-Allow-Origin", "http://localhost:3001");

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.

useEffect(() => {
  console.log("from effect");
}, [msgTxt]);

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

  1. Thinking in React https://react.dev/learn/thinking-in-react
  2. React JS and Express JS https://www.freecodecamp.org/news/create-a-react-frontend-a-node-express-backend-and-connect-them-together-c5798926047c/
  3. React JS life cycle https://levelup.gitconnected.com/componentdidmakesense-react-lifecycle-explanation-393dcb19e459
  4. Multi page React https://www.geeksforgeeks.org/how-to-create-a-multi-page-website-using-react-js/
  5. Multi page with state https://www.microverse.org/blog/how-to-get-set-up-with-react-redux-in-your-next-multi-page-project
  6. React JS and Express JS with Login and Token authentication https://www.digitalocean.com/community/tutorials/how-to-add-login-authentication-to-react-applications