This article is not a clicker. I learned some of the React syntax by reading the React component library source code. I have compiled some of the React syntax first

Batch update multiple renders, you may want to know about unstable_batchedUpdates

1. Take 🌰 for example

Imagine a scenario where a page has both a form and a table, as shown in the figure below

We want the table to change the current page number to the first page and load the table’s data when the user clicks the query button, as shown in the code below

import React, { useState, useEffect } from "react";

const Test = () = > {
  const [page, setPage] = useState(1);
  const [filterParams, setFilterParams] = useState({});
  const [data, setData] = useState([]);
  const form = useRef();

  useEffect(() = > {
    // Rerequest table data when page or search criteria change
    fetch("/loadData", {
      method: "get".params: {
        page: page, ... filterParams, }, }).then((data) = > {
      setData(data);
    });
  }, [page, filterParams]);

  function handleSearch() {
    // Get the form value asynchronously through validateFields
    form.current.validateFields().then((formData) = > {
      // Set the search criteria
      setFilterParams(formData);
      // Set page number to 1
      setPage(1);
    });
  }

  return (
    <>
      <Form ref={form}>
        <span>The form elements</span>
        <Button onClick={handleSearch}>search</Button>
      </Form>
      <Table data={data} pagination={{ page: page}} ></Table>
    </>
  );
};

Copy the code

In the code above, when the user clicks the search button, we do three things

  1. Call the form’s validateFields method to get the form’s data asynchronously
  2. Setting search Criteria
  3. Reset the page number to the first page

However, we find that there are two requests to load the table data, and the page number of the first request is not set to 1. What’s going on?

2. Problem analysis

As we know, in the React event loop, multiple setStates are merged into one to trigger an update, so we usually don’t have a problem writing React to batch state updates. However, there is an exception here: React does not merge multiple state updates in asynchronous code. React does not merge states when performing asynchronous operations such as setTimeout,Promise, etc

3. Unstable_batchedUpdates

React provides a temporary API called unstable_batchedUpdates for batch updates.

  function handleSearch() {
    // Get the form value asynchronously through validateFields
    form.current.validateFields().then((formData) = > {
      unstable_batchedUpdates(() = > {
        // Set the search criteria
        setFilterParams(formData);
        // Set page number to 1
        setPage(1); })}); }Copy the code

As shown in the code above, just wrap the batch status update code with unstable_batchedUpdates.

4. All asynchronous states need to be usedunstable_batchedUpdatesTo the parcel?

I don’t think it is necessary, only when the batch status update results in repeated requests, page rendering lag, etc., which affects the user experience

Publish a subscriber model to help you write a high performance Hook

1. Take 🌰 for example

In order to reuse logic in multiple components, we packaged a HOOK of useLayoutReisze, as shown in the code below

2. General implementation ideas

  const useLayoutResize = () = > {
  const [size, setSize] = useState({
    width: 0.height: 0}); useEffect(() = > {
    function handleResize() {
      const layout = document.querySelector(".layout");
      const bound = layout.getBoundingClientRect();
      setSize({
        width: bound.width,
        height: bound.height,
      });
    }
    window.addEventListener("resize", handleResize);
    return () = > {
      window.removeEventListener("resize", handleResize); }; } []);return [size];
};

export default useLayoutResize;

Copy the code

In the above code we implemented a useLayoutResize hook. We only needed to use the following code in the component

  const [size] = useLayoutResize();
  useEffect(() = > {
    console.log("The width has changed", size.width);
  }, [size.width]);
Copy the code

UseLayoutResize already meets the requirements, but what if we had 10 components on a page that all used useLayoutResize? Looking closely at the implementation of useLayoutResize, we needed to listen for ten resize events in ten components, and then when the browser window changed, We need to call getBoundingClientRect ten times, and each call to getBoundingClientRect will cause the browser to redraw, which may cause performance problems, so we need to find a way to solve this problem

3. Optimize with the published subscriber model

In essence, we only need to listen for layout container size changes. Listening once is enough, so can we extract the listening logic and inform each useLayoutReisze in turn when size changes, then we need to use publish subscriber mode

Publish the implementation of the subscriber


const subscribes = new Map(a);let subId = -1;
let size = {};
let layout = undefined;

const layoutResponsiveObserve = {
  // Triggers the subscription event
  dispatch(currentSize) {
    size = currentSize;
    subscribes.forEach((func) = > func(size));
    return subscribes.size > 0;
  },
  // Subscribe to events
  subscribe(func) {
    // If the listener event is not already registered, the listener event is registered
    if(! subscribes.size) {this.register();
    }
    subId += 1;
    subscribes.set(subId, func);
    func(size);
    return subId;
  },
  unSubscribe(id) {
    subscribes.delete(id);
    if(! subscribes.size) {this.unRegister(); }},register() {
    if(! layout) {// Initialize the layout
      layout = document.querySelector(".layout");
      // addEventListener causes the handleListener's this pointer to be problematic, so bind it here
      this.handleListener = this.handleListener.bind(this);
    }
    window.addEventListener("resize".this.handleListener);
    this.handleListener();
  },
  unRegister() {
    window.removeEventListener("resize".this.handleListener);
    subscribes.clear();
  },
  handleListener() {
    const bound = layout.getBoundingClientRect();
    const size = {
      width: bound.width,
      height: bound.height,
    };
    this.dispatch(size); }};export default layoutResponsiveObserve;
Copy the code

inuseLayoutResizeSubscribe to the event

This code implements layout resize, so how to use it in useLayoutResize?

import React, { useEffect, useMemo, useState } from "react";
import layoutResponsiveObserve from "./layoutResponsiveObserve";
const useLayoutResize = () = > {
  const [size, setSize] = useState({
    width: 0.height: 0}); useEffect(() = > {
    / / to monitor
    const token = layoutResponsiveObserve.subscribe((size) = > {
      setSize(size);
    });
    // Cancel listening when the component is destroyed
    return () = >{ layoutResponsiveObserve.unSubscribe(token); }; } []);return [size];
};

export default useLayoutResize;
Copy the code

Using the code above, no matter how many components we used useLayoutResize, we only listened once for layout, so we didn’t have to worry about performance issues caused by multiple listeners and multiple size calculations

Abnormal boundary

1. Take 🌰 for example

We can’t guarantee that our code will be bug-free, so we need to consider what to do if our component code fails. Take the following code for example

const Test = () = > {
  useEffect(() = > {
    const array = "";
    // This is obviously wrong
    array.push(3); } []);return <div>This is an 🌰</div>;
};

export default Test;
Copy the code

One obvious bug is calling push on strings. Execute the code, and at development time the page will display:

In a production environment, the entire page crashes, showing a blank page, and a component error causes the entire page to crash, which is a serious problembugSo how can we reduce the impact of code errors?

2. Take a look at the exception boundary

For our part, we hope that when an error occurs in one component of the page, it will not affect the display of other components, such as the pattern shown below

As you can see in the figure above, one of the components reported an error, but the rest of the page displayed normally and was not affected. To do this, you need to use exception boundaries.

What is the anomaly boundary?

Exception boundaries are a new feature introduced after React 16. Exception components are used to catch errors in child js components and display class components of alternate UI.

How are exception boundaries implemented

The following code implements a simple exception bound component. It is important to note that exception bound components must use class components, not functional components

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false}; }static getDerivedStateFromError(error) {
    return {
      hasError: true}; }componentDidCatch(error, errorInfo) {
    // Error logs can be reported here
  }

  render() {
    if (this.state.hasError) {
      return (
        <Result
          status="error"
          title="Wow, there's a mistake."
          subTitle="Please contact the administrator"
        ></Result>
      );
    }
    return this.props.children; }}export default ErrorBoundary;

Copy the code

How do I use exception components

Just use the component as a child of ErrorBoundary, as shown below

// Define a component
const Child = () = > {
  return <div>Child components</div>
}

// Use the exception boundary component in the parent page
const Parent = () = > {

  return <>
    <ErrorBoundary><Child/></ErrorBoundary>
    <ErrorBoundary><Child2/></ErrorBoundary>
    <ErrorBoundary><Child3/></ErrorBoundary>
  </>
} 
Copy the code

Through the above code, no matter which component error occurs, it will not affect the normal display of other components.

3. What are the restrictions

Although exception catching can catch errors in child components, it has some limitations

  1. Asynchronous code is not captured (e.gsetTimeout.Promise).
  2. Unable to catch server rendering errors
  3. If the exception boundary component itself reports an error, it cannot be caught
  4. Error reported in event

Operate sub-components to implement the shower function

1. Take 🌰 for example

In page development, radio buttons are written like this:

<input type="radio" name="colors" id="red">red<br>
<input type="radio" name="colors" id="blue">blue<br>
<input type="radio" name="colors" id="yellow">yellow<br>
Copy the code

In order for multiple radio buttons to form a radio button group, we need to give multiple radio buttons the same name, but in fact the native radio button style is not very good, by using the encapsulated radio button group, the UI looks like the following

Once wrapped, the code used on the page should look something like the following figure

  <Radio.Group>
    <Radio value="red">red</Radio>
    <Radio value="blue">blue</Radio>
    <Radio value="yellow">yellow</Radio>
  </Radio.Group>
Copy the code

There is a problem here, how to specify a name for each Radio? Radio.group = radio.group = radio.group = radio.group = radio.group = radio.group = radio.group It would be more convenient to give radio.group a name.

2. Let’s implement a radio component group first

Radio button code

import React from "react";

export interfaceIProps { name? :string;
  value: any;
}

const Radio: React.FunctionComponent<React.PropsWithChildren<IProps>> = ({ name, value, children, }) = > {
  // Sample code, undefined style
  return (
    <label>
      <span>
        <input type="radio" name={name} value={value}></input>
      </span>
      <span>{children}</span>
    </label>
  );
};

export default Radio;
Copy the code

Group of radio buttons

import React from "react";

export interfaceIProps { name? :string;
}

const Group: React.FunctionComponent<React.PropsWithChildren<IProps>> = ({ name, children, }) = > {
  return <div>{children}</div>;
};

export default Group;

Copy the code

Components export

import InnerRadio, { IProps as RadioProps } from "./Radio";
import Group from "./Group";
import React from "react";

export interface CompoundedComponent
  extends React.FunctionComponent<RadioProps> {
  Group: typeof Group;
}

const Radio = InnerRadio as CompoundedComponent;
Radio.Group = Group;
export default Radio;
Copy the code

With the above three pieces of code, we have developed a group of radio buttons that would look like this if used at the top of the page

 <Radio.Group>
    <Radio name="a" value="red">red</Radio>
    <Radio name="a" value="blue">blue</Radio>
  </Radio.Group>
Copy the code

This obviously doesn’t match the idea of putting the name in radio.group. To meet our requirements, we introduced React.children

Use 3.React.ChildrenTo solvenameLocation problem

Next, make some changes to the radio.group code

export interface IProps {
  name: string;
  children: React.ReactNode;
}

const Group: React.FC<IProps> = ({ name, children }) = > {
  return (
    <div>{React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child, { name: name, }); } throw new Error(" Child component must be of type react. ReactElement "); })}</div>
  );
};
Copy the code

In the code above, we introduced the React. Children, the React. IsValidElement, React. CloneElement three API, will be the realization of the function of the we want to come out, so, what do these three apis, have what use?

4. The React. Children is introduced

React.Children

React.Children provides utilities for dealing with the this.props.children opaque data structure.

The react props. Children is an opaque data structure for developers, which can be JSX, arrays, functions, strings, Boolean, etc. For example, we want to know how long the props. I can’t write props. Children. Length because a Boolean doesn’t have length and a string like ‘react’. Use React.Children. Count (props. Children) to calculate the number

React.children. Map, react.children. ForEach, react.children. Count, react.children. React.Children.only

React.Children.map

React.children. Map (Children, function[(thisArg)])

React.children.map is used to traverse each node in props. Children and call a callback function for each node, in which the new node is returned. If the props. Children value is undefined or null, undefined/null will be returned

React.Children.forEach

Syntax: reace.children. ForEach (Children, function[(thisArg)])

React.children. ForEach is used exactly the same as react.children. Map, except that there is no return value

React.Children.count

Grammar: the React. Children. Count (Children)

Because the props. Children data structure is opaque, we need to know how many nodes there are in the props. Children

  <List>
    <List.Item key="1">1</Listm.Item>
    <List.Item key="2">2</Listm.Item>
    <List.Item key="3">3</Listm.Item>
    <List.Item key="4">4</Listm.Item>
    <List.Item key="5">5</Listm.Item>
    <List.Item key="6">6</Listm.Item>
  </List>
Copy the code

I now want to display only 10 items when the number of list. items exceeds 10, and then add a “View More” button at the bottom of the List. Click “View more” to display the items

const List: React.FC<IProps> = ({ children }) = > {
  const length = useMemo(() = > {
    return React.Children.count(children);
  }, [children]);

  / /...
};
Copy the code

Now that we have the number, how do we get the first ten items of the List? Then we can use React.Children

React.Children.toArray

Grammar: the React. Children. ToArray (Children)

ToArray is used to expose the props. Children data structure to us as a flat Array structure, usually used to reorder or filter partial Children scenarios. In the previous scenario where list. Item fetched the first ten items, we converted children to Array and then used the Array’s slice method to fetch the first ten items of the Array

  const list = useMemo(() = > {
    return React.Children.toArray(children).slice(0.10)
  },[children])
Copy the code

React.Children.only

Grammar: the React. Children. Only (Children)

Verify that the child element has only one child (reac.reactelement) and return it. Otherwise, this method throws an error. Note: React.children. only does not accept the value returned by react.children. map because it is an array and not a React element.

5. React.isValidElement 与 React.cloneElementintroduce

CloneElement (Element,[config],[…children])

React. IsValidElement is used to verify that a React Element is passed. We used this API in radio.group because the props. So when we need to do something with a component that only exists with React Element, we need to call this API to verify that the React. CloneElement clones an Element and returns a new Element. We used this API in radio.group earlier. So when is this API going to be used? We can use this API when we want to modify the properties of props. Children.