React technical principles and code development practice: from Webpack to Parcel

Author: Zen and the Art of Computer Programming

1. Background Introduction

What is React?

React is a JavaScript library for building user interfaces that was first released in September 2013. It was invented by Facebook engineer Jean Preact and open sourced in 2017. Its main features include declarative programming, component-based design, one-way data flow, etc., and it provides a lightweight virtual DOM and update mechanism to improve page rendering efficiency.

Why should we learn React technology?

There are two significant advantages to learning React technology:

  1. Complex interface effects can be easily achieved using React;
  2. It can quickly respond to changes in business requirements, shorten the development cycle, and improve product quality.

By learning React technology, you can master the basic architecture of web applications, component development models, inter-component communication, routing management, state management, asynchronous data requests, testing tools, release processes and other technical skills.

2. Core concepts and connections

JSX syntax

JSX (JavaScript XML) is a syntax extension for describing XML markup language in JavaScript. Introduced by React creator Eli Snow in 2013, JSX simplifies writing views by allowing you to define your React components using HTML/CSS-like template syntax. JSX syntax can directly appear HTML tags in JavaScript code, so if you are used to HTML, your proficiency in JSX will be higher.

The following are the syntax rules for JSX:

<div className="container">
  <h1>Hello World!</h1>
</div>

// Convert JSX to JavaScript object
var element = React.createElement(
  'div',
  {className: 'container'},
  React.createElement('h1', null, 'Hello World!')
);

Virtual DOM and Diff algorithm

Virtual DOM (also called virtual tree or lightweight DOM) is a technology that represents real DOM as objects. It reduces the number of actual DOM updates and improves performance by comparing the differences between two virtual DOM trees and then applying the minimal changes to the real DOM. The following is a simple flow chart of Virtual DOM:

The Diff algorithm (also called collaboration algorithm) is used to calculate the minimum operating steps when comparing old and new virtual nodes to make the two virtual nodes consistent. The following is a diagram of how the Diff algorithm works:

In addition to JSX and Virtual DOM, React also provides some other important features such as PropTypes, State, Refs, etc. Among them, PropTypes can help check whether the incoming props meet the requirements, State provides the internal data storage mechanism of the component, and Refs provides a method to obtain DOM elements. However, this article only covers content related to JSX, Virtual DOM and Diff algorithm, and does not cover the specific usage of these features.

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

JSX description syntax rules

The JSX description syntax rules are as follows:

  1. <> represents JSX Element and {} represents expression interpolation.
  2. Use className for the class attribute.
  3. JSX supports all JavaScript expressions and supports statements such as if else.
  4. There can be only one root element in JSX.

createElement() method

The React.createElement() method is used to create React elements. This method accepts three parameters:

  1. tag – Creates a string representation of a DOM node or React component.
  2. props – Set the properties of this node.
  3. children – Set the children of this node.
const element = React.createElement(
  'div',
  {id: 'example'},
  'Hello World!'
);

console.log(element); // Output: {type: "div", key: null, ref: null, props: {…}, _owner: null, …}

render() method

The render() method is a global variable defined in the ReactDOM module. Its function is to convert JSX into a virtual DOM tree. After calling the render() method, React renders JSX into a complete component, including all sub-components defined by JSX.

import React from'react';
import ReactDOM from'react-dom';

function App() {
  return (
    <div>
      <h1>Hello World!</h1>
      <p>{Math.random()}</p>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

Class components and function components

React has two types of components: class components and function components. The biggest difference between them is whether they have their own state and lifecycle hooks.

Class components inherit from the Component base class and provide state, lifecycle hooks and other auxiliary functions. This can be achieved using the extends keyword:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

Function components do not have their own this and can only receive and provide data through props. Since they cannot have state or lifecycle hooks, they are just pure UI rendering components. You can return JSX directly:

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

function App() {
  return (
    <div>
      <Greeting name="World" />
    </div>
  );
}

data flow

React implements a two-way binding mechanism for data through JSX, Virtual DOM, Diff algorithm, class components, and function components, that is, when the data changes, the view will automatically update.

When the component’s state or props change, React will re-render the component and generate a new Virtual DOM tree based on the new Props and State. React then compares the differences between the new Virtual DOM tree and the old Virtual DOM tree, and generates a set of minimum operation steps, which are applied to the Real DOM, thus completing the view update.

React’s data flow can be summarized as parent component -> props -> child component -> state. The direction of data flow is one-way, that is, the parent component passes Props to the child component, and the child component can only modify its own State.

componentDidUpdate() life cycle function

The componentDidUpdate() function is executed immediately after the component is rendered, and is generally used to handle the logic after the dom is updated.

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

  handleClick = () => {
    console.log("Clicked");
    this.setState((prevState) => ({count: prevState.count + 1}));
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count!== this.state.count & amp; & amp; this.state.count % 2 === 0) {
      alert(`Count is even now!`);
    }
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>
          Click me
        </button>
        <p>You clicked {this.state.count} times</p>
      </div>
    )
  }
}

The componentDidUpdate() function in the above example pops up a window to prompt the user when count changes and is currently an even number.

useReducer() hook

The useReducer() hook allows you to customize the reducer and set it as local state. You can use reducers to merge multiple state updates into a single action, and you can also put side-effect behaviors together without breaking the encapsulation of the component.

import React, { useState, useReducer } from'react';

function counter(state, action) {
  switch (action.type) {
    case 'increment':
      return {...state, value: state.value + 1 };
    case 'decrement':
      return {...state, value: state.value - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counter, { value: 0 });

  return (
    <>
      Count: {state.value}
      <button onClick={() => dispatch({ type: 'increment' })}> + </button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}

export default Counter;

The Counter component in the above example uses a reducer named counter. This reducer accepts two parameters: the current state and the action to be executed. It determines the value of action.type through switch-case and performs addition and subtraction operations on value respectively. The Counter component obtains the state and dispatcher through useReducer(), and triggers the dispatcher by clicking the button to send the action to the reducer for execution. The reducer returns the updated state, and the Counter updates the display based on the state.

4. Specific code examples and detailed explanations

setState() method

React’s setState() method can conveniently set the state of a component. It can accept object parameters or function parameters. If the parameter is a function, it will be merged into the current state. The setState() method is asynchronous, so do not rely on the value set by this method to update the next render, otherwise you may see incorrect rendering.

this.setState(previousState => {
  return { counter: previousState.counter + 1 };
});

Conditional rendering

React conditional rendering can use three methods: if…else, conditional operator, and map method.

{condition? true : false} // condition operator

{array.map(item => condition? item : null)} // map method

{condition & amp; & amp; (<div>Render if condition is truthy</div>)} // if...else

{(typeof array!== 'undefined' || array!== null) & amp; & amp;
  array.length > 0? (
  <ul>
    {array.map(item => (
      <li>{item}</li>
    ))}
  </ul>)
: ('No items to display')} // Array judgment

Context API

Context provides a way to pass props down to each layer without manually passing them down so that certain data can be shared between components. Context needs to specify the context object through the contextType attribute of createClass or functional component.

class App extends React.Component {
  static childContextTypes = {
    theme: PropTypes.string
  };

  getChildContext() {
    return { theme: this.state.theme };
  }

  constructor(props) {
    super(props);
    this.state = { theme: "light" };
  }

  changeTheme = () => {
    this.setState(prevState => ({ theme: prevState.theme === "dark"? "light" : "dark" }));
  }

  render() {
    return (
      <div>
        <Button onClick={this.changeTheme}>{this.state.theme === "light"? "Dark Mode" : "Light Mode"}</Button>
        <hr/>
        <Content />
      </div>
    );
  }
}

class Content extends React.Component {
  static contextTypes = {
    theme: PropTypes.string
  };

  render() {
    let color = this.context.theme === "light"? "#fff" : "#000";

    return (
      <div style={<!-- -->{backgroundColor: color}}>
        This content has a background color based on the theme selected in the parent component.
      </div>
    );
  }
}

The Content component in the above example specifies the context object theme through contextType, and reads this value through this.context. The App component obtains the context object through getChildContext() and passes it to the child component. The Content component obtains the theme and sets the backgroundColor by reading this.context. The onClick function of the switch theme button calls this.changeTheme() to change the theme state, and the Content component automatically responds to the update.

error boundary

Error boundaries are a technique used to catch exceptions from child components and render alternative UI. You can use the ErrorBoundary component to wrap components that need to be error-tolerant and render fallback UI. ErrorBoundary’s componentDidCatch() function can be used to log error information and then render the fallback UI.

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // log error message and stack trace in the console
    console.log(error, info);
    this.setState({ hasError: true });
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

<ErrorBoundary>
  <MyComponentThatMayThrowAnError />
</ErrorBoundary>

The MyComponentThatMayThrowAnError component in the above example may throw an error, such as missing required parameters. This error will be recorded in the componentDidCatch() function of ErrorBoundary. When a component fails to render, the fallback UI is rendered instead of the component itself.

React has been on a stable path in recent years. With the rapid development of web technology and the gradual popularity of front-end frameworks, React will continue to be popular among developers. However, there is no silver bullet in React, and it is difficult to completely solve React’s problems. The only way to improve React’s capabilities and performance is by improving its own technology.

Here are some of the main problems faced by React:

  1. Performance optimization of large applications: Currently, React is only suitable for small applications. For large applications, the computing overhead of Virtual DOM is very high. Large-scale data rendering will consume a lot of memory and CPU resources, causing the application to freeze or even crash.
  2. Embrace the latest technology stack: React was originally born to work with Facebook’s products and services, but now React can be used in conjunction with other technology stacks, such as GraphQL, Relay, Apollo, MobX, etc.
  3. Consideration of browser compatibility: React is based on the browser’s own event loop mechanism and runs in the browser environment. However, the design concept of React components is cross-platform, and the interaction between components should avoid using browser features as much as possible to maintain consistency between platforms.
  4. Asynchronous rendering: React’s diff algorithm uses depth-first traversal, which is relatively slow. Asynchronous rendering is beneficial to user experience, such as animation effects, but it also increases the complexity of components.

6. Appendix Frequently Asked Questions and Answers