React technical principles and code development practice: from PWA to Web Components

1. Background Introduction

React is a JavaScript library for building user interfaces. It was originally open sourced by Facebook in 2013. Its powerful functions, excellent performance, simplicity and ease of use make it one of the most popular front-end frameworks currently. In recent years, the React community has also undergone some changes, including the rise of componentization, the emergence of data management tools such as Redux and Mobx, and the application of WebAssembly. This book will introduce the basic theoretical knowledge of the React technology stack, the interactive relationships between various components, programming models and programming techniques. In addition, starting from actual projects, practical cases will be used to demonstrate how to develop React applications, WebComponents components and Progressive Web App applications. Readers can follow the guidance in the book to learn React step by step, and use their own way to combine programming skills, theoretical knowledge and practical experience to achieve more meaningful and innovative products.

2. Core concepts and connections

The core concept of React is the component design pattern. It divides the interface into independent and reusable modules, and each module is called a component. Components can communicate with each other through props and state, and can be freely combined and assembled into complex view structures, thereby improving code reuse and development efficiency. As shown in the figure above, a typical React application consists of three main parts, namely Root Component, Child Component and Parent Component. The relationship between them is as follows:

  • Root component: Responsible for rendering the entire page. It serves as the ancestor of all other components and is the only root node, there can only be one. It defines a top-level routing table that determines which components need to be rendered on the screen and their respective URLs.
  • Subcomponent: Responsible for a specific function or business logic. They are generally nested inside parent components and can only be controlled by the parent component. The state of child components is controlled by Props, which can be passed directly to child components or received from parent components.
  • Parent component: Responsible for managing the life cycle, status, event handling, etc. of child components. It usually only needs to receive Props and State, and respond to user interaction by calling the child component’s methods. Parent components can also trigger commands to child components to let them perform specific tasks. React’s rendering mechanism is based on virtual DOM, which stores the actual DOM object in memory, then calculates the changes through the diff algorithm, and only updates the corresponding DOM elements to avoid unnecessary rendering. In addition, React supports JSX syntax, allowing JavaScript expressions to be embedded in the template language. In this way, when writing components, you only need to focus on the UI template, without having to consider cumbersome processes such as DOM operations and event binding. React provides a series of APIs to help developers develop components more efficiently. For example, useState() function is used to declare a state variable, useEffect() function is used to declare a side effect function, and useContext() function is used to declare a shared context environment. React Router is a routing management tool officially provided by React. It provides a unified API to allow applications to have a complete URL access experience. React Native is a cross-platform mobile application development framework launched by Facebook, which can run on multiple platforms such as iOS, Android, and Web. It is closely related to the componentization idea of React, and the same code can be used to develop iOS and Android versions of the application.

    3. Detailed explanation of core algorithm principles, specific operation steps, and mathematical model formulas

    There are two main algorithms of the React technology stack One – Fiber algorithm and virtual DOM. Among them, the Fiber algorithm is one of the most important algorithms in React. Its main idea is to use the characteristics of virtual DOM to divide the component tree into different levels of subtrees, and then only re-render the changed subtrees instead of rendering the entire component tree for each update. This can effectively reduce rendering overhead and improve application performance.

Below I will introduce the specific principles and operation steps of the Fiber algorithm in detail. First, let’s take a look at the rendering process of a typical React component.

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 0};
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <h1>{this.state.count}</h1>
        <button onClick={this.handleClick}> + </button>
        <Child />
      </div>
    );
  }
}

function Child() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`I am child and my count is ${count}`);
  }, [count]);

  const handleClick = () => {
    setCount((prevState) => prevState + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}> + </button>
    </div>
  );
}

In the above example, the Parent component is responsible for rendering the parent element, button and child component Child. The useState() function in the subcomponent is used to maintain the value of the state count; the useEffect() function is used to register a side effect function, which will be executed whenever the count value changes. The handleClick function is used to modify the count value when the button is clicked. Note that in class components, subcomponents can be returned directly in the render() method without wrapping them in an extra tag. Therefore, in the sample code, the rendering of the Child component occurs in the render() method of the Parent component.

  1. Overview of Fiber algorithm The Fiber algorithm is a React algorithm that supports incremental rendering. Its main idea is to divide the component tree into different subtrees and render the subtrees separately, instead of rendering the entire component tree at once like traditional rendering. This approach can minimize the rendering workload and improve application performance.

  2. The structure of Fiber Fiber is actually a linked list data structure. Each node represents a task, including components to be rendered, attributes, location information, etc. The Fiber node is called “fiber”, which means fiber. It is actually a small note and can be understood as a linked list of recursive structure. As shown in the figure below, each node also has a pointer to the next node, thus forming a one-way linked list. In addition to saving component, attribute and position information, Fiber also records the type of the current node (such as ordinary node, head node, etc.), as well as information about child nodes and sibling nodes. In addition, Fiber also saves additional information, such as the list of updates to be submitted, reconciliation procedures, pause status, etc.

  3. Creation and update of Fiber When a component’s render() method is called, React will create a Fiber node, record the component’s type, properties and location information, etc., and add it to the end of the Fiber tree. For child components, React will create the child Fiber node and add it to the child node queue of the parent Fiber node.

When the application state changes or other situations require re-rendering, React will create a new Fiber tree and update the original Fiber tree. In order to determine whether a Fiber node needs to be updated, React compares the root nodes of the two trees before and after. If the root node is the same, determine whether the remaining child nodes are the same. If they are different, it is considered that the node has changed and needs to be updated.

In the React source code, you can see that there is a function called reconcileSingleElement(), which is used to compare two Fiber nodes and determine whether they need to be updated. If it is found that an update is needed, the old and new nodes are marked as requiring replacement. React does not delete the old node immediately, but marks it as deleted. The next time there is free time, React will iterate through all deleted nodes and destroy them.

  1. Reconciliation and rearrangement of Fiber When multiple Fiber nodes are marked as needing update, React needs to determine the relative order of these nodes. Because React’s algorithm requires that the order of each Fiber node must be consistent, otherwise rendering cannot be completed. So when encountering a Fiber node that needs to be updated, React will try to figure out a suitable insertion location.

The Fiber algorithm converts the component tree into an updated list, which is called workInProgress tree. Traverse the workInProgress tree starting from the root node. For each node, React checks the number of its child nodes. If the number of child nodes of the current node is the same as the number of child nodes on current tree, then traversal is entered. Otherwise, React will decide whether to create, delete, or move the node based on the difference in the number of child nodes.

Assume that five nodes A, B, C, D, and E form a component tree. The relationship between the nodes is as follows: A->B, A->C, B->D, C->D, C->E. Now, if the number of child nodes of C changes, for example, from 2 to 3, then React will create a new Fiber node and add the three nodes C, D, and E to the end of the node’s child node queue. Similarly, if the number of child nodes of A and C changes, for example, from 2 to 3, React will create a new Fiber node and add the four nodes A, C, D, and E to the node. The end of the child node queue.

This creation, deletion, or move operation is called reconciliation. Whenever a component needs to be updated, React calls the reconciliation algorithm. However, the reconciliation algorithm is not real-time, it only determines the structure of the updated workInProgress tree. Therefore, React cannot generate the real rendering result immediately, but continues to maintain the original current tree. React will generate real rendering results only after workInProgress tree is stable. Therefore, there may be a conflict between the structure of workInProgress tree and current tree.

The way to resolve this conflict is through the reconciliation process. The reconciliation program will change the marking of nodes that need to be replaced to “conflict” or “missing” based on the difference between workInProgress tree and current tree. The next time it needs to be rendered, React looks for all “colliding” nodes and tries to find a suitable place to insert them. If the insertion is successful, clear the original node; otherwise, roll back to the last stable current tree.

  1. Pause and resume of Fiber The Fiber algorithm is asynchronous by default, that is, the rendering process is performed in a background thread and will not affect the display of the UI. However, in some cases, it is necessary to ensure the order of rendering, such as in event callback functions. At this point, we can pause rendering, wait for the event callback to end, and then resume rendering. This requires the introduction of an updateQueue to record the Fiber nodes that need to be updated. Whenever a component needs to be updated, React will put it into the updateQueue and will not start rendering until the next refresh.

4. Specific code examples and detailed explanations

PWA application practice

Introduction to Progressive Web Application (PWA)

Progressive Web Applications (hereinafter referred to as PWA) are web applications that can run normally like web pages, but at the same time have the immersive and interactive experience of desktop applications. PWA is a type of web application that is given a Webapp Manifest file and can be installed on the device through browsers such as Chrome, Firefox, and Opera. Unique features of PWA include:

  • Offline browsing: PWA can realize offline browsing of application data through the caching mechanism, ensuring that the application can run normally even without a network connection.
  • Immersive experience: PWA gives applications an immersive running experience through the native application interface of the mobile phone system (such as notification bar, quick launch, etc.).
  • Fast startup: Since PWA does not need to download complete resources every time it is loaded, it can achieve fast startup of the application.
  • Website link: PWA can open a website on the mobile phone through a registration agreement, or even launch the website within the application. The concept of PWA originates from Google’s PWA white paper released at the Google I/O conference, which defines five major characteristics that PWA should have. Below we elaborate on these characteristics and discuss the meaning behind them.

Offline browsing This is the basic feature of PWA. In the absence of a network, PWA can still run normally without loading failure. PWA uses Service Worker technology to implement offline browsing. When the browser requests an HTML document, Service Worker takes over and can obtain resources from the cache, thereby enabling offline browsing of the application. In addition to HTML documents, PWA can also cache images, CSS, JavaScript files, etc.

Immersive experience This is another core feature of PWA. PWA can obtain the native immersive running experience of the mobile phone system just like native applications. For example, when a notification is received, the system will pop up a prompt to allow the user to quickly switch to the main interface of the application; when the user enters a search keyword, the application can automatically associate query suggestions to improve the user experience.

Quick Start This is another core feature of PWA. In terms of achieving fast startup, PWA can save users’ time during the loading process through the subcontracting strategy of application resources. In fact, many application resources can be split into multiple packages and loaded on demand to achieve quick startup of the application. For example, the application homepage only has a few key resources, but through the subcontracting strategy, other resources (such as login pages and registration pages) can be packaged together to achieve quick startup.

Website link This is the third core feature of PWA. PWA can use URL schemes to allow users to open websites within the application, or even open specific pages within the application. For example, users can open the WeChat web version on the WeChat client to view messages, chat records, etc. PWA can also set shortcuts, which can be opened by clicking on the desktop icon.

Taken together, these features make PWA a brand new application form. By combining web technology, network, device capabilities and other capabilities, PWA applications can create a user experience like native applications.

Create a simple PWA application

Let’s take a look at how to create a simple PWA application. We will create a simple counter application that has features such as offline browsing and immersive experience, allowing users to use it normally even when offline. This application is a Vue.js single-page application, packaged using Webpack, and used with ServiceWorker to achieve offline browsing.

Installation dependencies

First, we need to install Vue scaffolding, webpack and webpack-cli.

npm install -g @vue/cli webpack webpack-cli
Initialization project

Then, we initialize a Vue project.

vue create counter-pwa
cd counter-pwa
Configure webpack

We need to configure Webpack to support PWA features. Edit build/webpack.config.js and add the following content.

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
 ...
  plugins: [
    new HtmlWebpackPlugin({
      template: 'public/index.html',
      filename: 'index.html',
      injection: true
    }),
    new WorkboxPlugin.GenerateSW({
      swDest:'sw.js'
    })
  ]
}

Here, we configured the HtmlWebpackPlugin so that the HTML files output by Webpack are automatically injected into the PWA’s Service Worker script. In addition, we also use the WorkboxPlugin.GenerateSW plug-in to automatically generate the Service Worker script and output it to the dist/sw.js directory.

Configure Service Worker

We also need to configure the Service Worker script so that it can respond to offline requests. Edit src/registerServiceWorker.js and add the following content.

importScripts('https://storage.googleapis.com/workbox-cdn/releases/latest/workbox-sw.js');

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    if (navigator.onLine) {
      // When there is a network, register Service Worker
      registerValidatingServiceWorker();
    } else {
      // When there is no network, register the cached Service Worker
      navigator.serviceWorker.register('./static/precache-manifest.json')
         .then(registration => {
            console.log('Service Worker successfully registered', registration);
          }).catch(error => {
            console.error('Error:', error);
          });
    }
  });

  //Verify whether the Service Worker is valid
  async function registerValidatingServiceWorker() {
    try {
      await navigator.serviceWorker.register('./sw.js', {scope: './'});

      // If verification is successful, reload the page
      location.reload();
    } catch (e) {
      console.warn(e);
    }
  }
}

Here, we use Workbox’s register() method to register the Service Worker. If the browser is online, the local Service Worker script is used; if the browser is offline, the pre-cached Service Worker script is used.

Add cache policy

We also need to add a cache strategy to the configuration file src/assets/precache-manifest.[hash].js. Edit the .gitignore file and add src/assets/*.gz.

{
  "version": "[hash]",
  "files": [
    "/",
    "/favicon.ico",
    "/manifest.webmanifest"
  ],
  "globPatterns": [
    "**/*.html",
    "**/*.js",
    "**/*.css",
    "**/*.svg",
    "**/*.jpeg",
    "**/*.webp",
    "**/*.woff",
    "**/*.woff2",
    "**/*.ttf",
    "!**/*.gz"
  ]
}

Here, we define the resource caching strategy. The globPatterns field specifies the file types and paths to be cached. In order to speed up access, we also compressed the static resources and uploaded the .gz file to the server.

Writing Application

Edit the src/App.vue file and add the following content.

<template>
  <div id="app">
    <header>
      <h1>{<!-- -->{ title }}</h1>
      <p>{<!-- -->{ message }} {<!-- -->{ count }}</p>
      <button v-on:click="increment">{<!-- -->{ buttonLabel }}</button>
    </header>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      title: 'Counter App',
      message: 'You clicked ',
      count: 0,
      buttonLabel: 'Increment',
    };
  },
  methods: {
    increment() {
      this.count + + ;
    }
  }
}
</script>

<style scoped>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
}

header {
  margin: 2em auto;
  max-width: 600px;
}

h1 {
  font-weight: normal;
  margin: 0 0 0.5em;
}

p {
  margin: 0;
}

button {
  display: block;
  margin: 1em auto;
  padding: 0.5em 1em;
  border: none;
  background-color: #4CAF50;
  color: white;
  cursor: pointer;
}
</style>

This application is a very simple counter application that demonstrates the basic usage of Vue.js. We can modify the initial value in the data option, adjust button text and color, etc.

Test application

Finally, let’s test the application. Run the following commands to build the application and start the server.

npm run build
serve dist --single

We need to add --single to the server’s startup parameters to make it run in single-page mode instead of starting the default multi-page application server. Visit http://localhost:5000 with the browser, open the application, and ensure that the Service Worker has been activated. Then, close the network connection and refresh the page to make sure the app is still working properly.

When we open the browser debugging tool and switch to the Application tab, we can see the status of the Service Worker.

As shown in the figure above, the Service Worker has been activated and is available. At this point, we have successfully created a simple PWA application.

Web Components practice

Web Components is a technology that can be used to create custom HTML tags. It is a collection of loosely coupled, reusable Web components that provides features such as encapsulation, inheritance, isolation, and composability of Web components. Using Web Components can effectively improve code reusability and maintainability.

Why use Web Components?

The core idea of Web Components is to treat HTML tags as code snippets and define their behavior and style through custom elements. It enables web developers to create reusable, customizable elements that can be intelligently combined to build complex applications.

Web Components can bring the following benefits:

  • Reuse: The same code can be encapsulated into a custom element and reused in multiple web pages.
  • Customizable: Web pages can be easily customized by modifying the properties and methods of custom elements.
  • Isolation: Web Components provides a componentization solution that is completely independent of external web page styles, ensuring the security and stability of components.
  • Maintainability: Web Components allow web developers to easily modify the behavior and style of components without changing the structure of the web page.

Use Web Components component library

In the actual development process, we may refer to third-party component libraries, or write our own components that meet our own needs. This section will introduce how to use the Web Components component library in React projects.

Installation dependencies

First, we need to install react, react-dom and @webcomponents/webcomponentsjs.

npm i react react-dom @webcomponents/webcomponentsjs
Import components in React project

Then, we edit the App.js file and import the components we need.

import '@webcomponents/webcomponentsjs';
import './my-component';

function App() {
  return (
    <div className="App">
      <my-component></my-component>
    </div>
  );
}

export default App;

Here, we imported @webcomponents/webcomponentsjs to use Web Components in the browser. We also imported ./my-component, which is a custom component. Note that we don’t need to manually import the component’s JS file.

Use custom elements

We edit the my-component.js file to define and use custom elements.

class MyComponent extends HTMLElement {

  connectedCallback() {
    this.innerHTML = `
      <label>Input:</label><br/>
      <input type="text"><br/>
      <button οnclick="${this._handleClick}">Submit</button>`;

    setTimeout(() => {
      this.$input = this.querySelector('input');
      this._initEventListener();
    }, 0);
  }

  _initEventListener() {
    this.$input.addEventListener('keyup', this._handleChange.bind(this));
  }

  _handleChange() {
    const value = this.$input.value;
    console.log('New input value:', value);
  }

  _handleClick() {
    alert('Clicked!');
  }

}

customElements.define('my-component', MyComponent);

Here, we define a class named MyComponent, which inherits from HTMLElement. When the component is added to the DOM tree, the connectedCallback() method will be called. We used the innerHTML attribute to dynamically generate a form and bound a click event to the button.

Then, we use the setTimeout() method to delay the initialization of this.$input because the connectedCallback() method is executed synchronously. It means that we can’t do any operation on the child elements inside it.

We use the _initEventListener() method to initialize the listening event of the input box. When the value of the input box changes, we call the _handleChange() method to print the log and display the warning box.

Finally, we define a global method called customElements.define() to define a custom element.

Test custom element

We can use custom elements in React components and modify their properties, methods, etc. Edit the App.js file and modify the code as follows:

class App extends React.Component {
  componentDidMount() {
    document.getElementById('root').appendChild(document.createElement('my-component'));
  }
  render() {
    return (
        <div className="App"></div>
    );
  }
}

export default App;

Here, we use the componentDidMount() life cycle method to dynamically append the element to id=" after the component is mounted. under the root" element.

We can see the alert box pop up in the browser, proving that our custom element is working correctly.

At this point, we have successfully used Web Components in our React project.

Summary

This book mainly introduces the basic theoretical knowledge of the React technology stack, the interactive relationships between various components, programming models and programming techniques. In addition, it also starts from actual projects and demonstrates how to develop React applications, WebComponents components and Progressive Web App applications through actual cases. Readers can follow the guidance in this book to learn React step by step, and use their own way to combine programming skills, theoretical knowledge, and practical experience to achieve more meaningful and innovative products.