This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

preface

The next few articles will focus on some ‘curious’ scenarios that will revolutionize how React works in principle. The React principle is behind every scenario,

I can seriously say that after reading this article, you will have mastered:

  • 1 componentDidCatch principle
  • 2 susponse principle
  • 3 Principles of Asynchronous Components.

Impossible

I can write whatever I want in my function component. When many of you read this sentence, the four words that should come to your mind are: How can that be? Function components, as we remember them, cannot use asynchrony directly and must return a piece of Jsx code.

So today I’m going to break that rule and do something unexpected in functions that we think of as components. Let’s follow my lead here.

React JSX

represents a DOM element, while

represents a component.

<div />
<Index />
Copy the code

React Element JSX is a representation of the React Element. The JSX syntax is compiled into React Element objects by Babel.

  • <div />Not reallyDOMElement, which is the type attribute ofdivElement object.
  • componentIndexIs an Element object whose Type attribute is the class or component itself.

So, using function components as a reference, Index is conventionally something like this:

function Index(){
    /* Asynchronous operations cannot be performed directly */
    /* return JSX code */
    return <div></div>
}
Copy the code

If you do not write strictly to this format and mount it via JSX
, an error will be reported. Look at the following example 🌰 :

/* Index is not in strict component form */
function Index(){
    return {
       name:'React Advanced Practice Guide'}}/* Properly mount Index component */
export default class App extends React.Component{
    render(){
        return <div>
            hello world , let us learn React! 
            <Index />
        </div>}}Copy the code

Children should be a React Element object, but Index returns a normal object.

Since it cannot be a normal object, it is even more unlikely that an Index will have an asynchronous operation, as in the following case:

 /* Example 2 */
function Index(){
    return new Promise((resolve) = >{
        setTimeout(() = >{
            resolve({ name:'React Advanced Practice Guide'})},1000)})}Copy the code

The React component can also report errors, so under a standard React component specification:

  • A JSX object structure must be returned, not a normal object.
  • Render cannot perform asynchronous operations during execution.

The impossible becomes possible

So how to break the game and make the impossible possible. The first problem to be solved is the error problem. As long as no error is reported, the App can render normally. It is not hard to see that the error timing is all in the render process. You can use the two render error bounds lifecycle componentDidCatch and getDerivedStateFromError provided by React.

I’m going to select componentDidCatch because we’re going to do something nasty after catching render errors. So let’s modify our App with componentDidCatch.

export default class App extends React.Component{
    state = {
       isError:false
    }
    componentDidCatch(e){
         this.setState({ isError:true})}render(){
        return <div>hello world , let us learn React! {! this.state.isError &&<Index />}
        </div>}}Copy the code
  • withcomponentDidCatchCatch exceptions, render exceptions

As you can see, the error is still reported, but at least the page is rendering normally. Now that’s not enough. Let’s say the first Index returns a normal object. We want to mount this component and get the data that Index returns.

It occurs to me that componentDidCatch catches render exceptions, so inside it it should catch exceptions like try{}catch(){}. Something like this:

try{
    // Try rendering
}catch(e){
     // Render failed, execute componentDidCatch(e)
     componentDidCatch(e) 
}
Copy the code

So if an error is thrown in Index, is it also received at componentDidCatch. So he did it. We change Index from return to throw, and then we print error on componentDidCatch.

function Index(){
    throw {
       name:'React Advanced Practice Guide'}}Copy the code
  • Return the throw object.
componentDidCatch(e){
    console.log('error:',e)
    this.setState({ isError:true})}Copy the code
  • Catch errors with componentDidCatch. So e is IndexthrowThe object. Next render with objects thrown by the child component.

export default class App extends React.Component{
    state = {
       isError:false.childThrowMes:{}
    }
    componentDidCatch(e){
          console.log('error:',e)
         this.setState({ isError:true , childThrowMes:e })
    }
    render(){
        return <div>hello world , let us learn React! {! this.state.isError ?<Index /> : <div> {this.state.childThrowMes.name} </div>}
        </div>}}Copy the code
  • Catch the exception object thrown by Index and re-render it with the data inside the object.

Effect:

ComponentDidCatch accepts and renders. Isn’t that a little bit…

But do all objects of the throw get caught normally? So we catch the Promise object thrown by the second Index with componentDidCatch. Shall we see what it is?

As you can see, the Promise object is not caught properly, but instead is caught with an exception. In Suspense, you can find the word Suspense. There must be a connection between throw Promises and Suspense, in other words Suspense can catch Promise objects. Suspense components cannot be found in Suspense after React.

So far, it can be concluded that:

  • ComponentDidCatch throughtry{}catch(e){}Catch an exception, and if we throw a normal object during rendering, it gets caught as well. butPromiseObject that will be raised the second time by the React base.
  • Suspense can accept thrown Promise objects inside Suspense, so there is one inside SuspensecomponentDidCatchSpecializes in exception catching.

Gorgon – My component can write asynchronously

Even though direct throw promises are intercepted at the React level, how do you implement normal writing of asynchronous operations inside components? React intercepts Promise objects thrown by components, so what if we wrapped Promise objects in a layer? So let’s change the Index content.

function Index(){
    throw {
        current:new Promise((resolve) = >{
            setTimeout(() = >{
                resolve({ name:'React Advanced Practice Guide'})},1000)}}}Copy the code
  • As above, instead of throwing a Promise directly, you wrap a layer of objects around the Promise. Let’s take a look at the typo.

Now that we can receive the Promise directly, we execute the Promise object, simulate the asynchronous request, and render it with the requested data. So you modify the App component.

export default class App extends React.Component{
    state = {
       isError:false.childThrowMes:{}
    }
    componentDidCatch(e){
         const errorPromise = e.current
         Promise.resolve(errorPromise).then(res= >{
            this.setState({ isError:true , childThrowMes:res })
         })
    }
    render(){
        return <div>hello world , let us learn React! {! this.state.isError ?<Index /> : <div> {this.state.childThrowMes.name} </div>}
        </div>}}Copy the code
  • Get the Promise in parameter E to componentDidCatch,Promise.resolveExecute the Promise to fetch the data and render.

Effect:

You can see that the data is rendering properly, but there is a new problem: the current Index is not really a component, but a function, so next, modify the Index to become a normal component by retrieving data asynchronously.

function Index({ isResolve = false , data }){
    const [ likeNumber , setLikeNumber ] = useState(0)
    if(isResolve){
        return <div>
            <p>Name: {data. The name}</p>
            <p>Star: {likeNumber}</p>
            <button onClick={()= >SetLikeNumber (likeNumber + 1)} > thumb up</button>
        </div>
    }else{
        throw {
            current:new Promise((resolve) = >{
                setTimeout(() = >{
                    resolve({ name:'React Advanced Practice Guide'})},1000)})}}}Copy the code
  • The Index throughisResolveDetermine whether the component is added to the completion, the first timeisResolve = falsesothrow Promise.
  • The parent App accepts the Promise, gets the data, changes the state isResolve, renders the second time, and the second Index will render normally. Let’s look at how the App writes:
export default class App extends React.Component{
    state = {
       isResolve:false.data:{}
    }
    componentDidCatch(e){
         const errorPromise = e.current
         Promise.resolve(errorPromise).then(res= >{
            this.setState({ data:res,isResolve:true})})}render(){
        const {  isResolve ,data } = this.state
        return <div>
            hello world , let us learn React!
            <Index data={data} isResolve={isResolve} />
        </div>}}Copy the code
  • The error is caught with componentDidCatch and then rendered a second time.

Effect:

It served its purpose. This is a brief introduction to asynchronous components. That introduces the concept of ‘Susponse’, so let’s move on to ‘Susponse’.

Suspense For Flying – Implement a simple Suspense

What is Susponse? Susponse English translation suspended. What is’ React Susponse ‘? So normally the component would be all in one step, so with ‘Susponse’ the component rendering would be able to be suspended first.

First explain why you hover?

Susponse’s position in the React ecosystem is highlighted in the following aspects.

  • Code splitting (code) : which component loading, loading which component code, sounds pretty man, can be really hard to solve the problems of the master file size is too large, the indirect optimization the first screen load time of the project, we know that after the browser to load resources are time consuming, the impact of time to the user’s bad effect.

  • Spinner decoupling: Under normal circumstances, page display requires interaction between front and back ends. Data loading process is not expected to see no data state -> flash data scene, but rather a spinner data loading state -> display page state after loading. For example:

<List1 />
<List2 />
Copy the code

List1 and List2 both use the server to request data, so in the process of loading data, we need a Spin effect to elegantly display the UI, so we need a Spin component, but the Spin component needs to be inside List1 and List2, resulting in a coupling relationship. Now to take on Spin by Basil Basil, put it in the business code:

<Suspense fallback={ <Spin /> }  >
    <List1 />
    <List2 />
</Suspense>
Copy the code

Spin loading of List1 and List2 data. Decouple the Spin, just like watching a movie. If the movie gets stuck loading the video stream, instead of expecting to show the user a black screen, fill the screen with a poster, and the poster is the Spin.

  • Render data: The entire render process is executed synchronously in one go, so component render => request data => component reRender, but in Suspense asynchronous components it is allowed to call render => find asynchronous requests => hover, Wait until the asynchronous request is complete => Render and display data again. This definitely reduces one render.

Now explain how to hover

Suspense is understood above, then analyze the principle of wave. First, through the above, we have explained the principle of Suspense, how to hover, which is very simple and crude, and throw an exception directly.

What is an exception? A Promise, and that Promise also comes in two cases:

  • The first is to request data asynchronously, this onePromiseInternally encapsulates the request method. Request data for rendering.
  • The second is to load components asynchronouslywebpackThe require() API is provided to achieve code segmentation.

Hover and render again

Rerender will do if you want to resume rendering after Suspense.

Suspense is introduced in detail above. Suspense is not Suspense provided by React. Here we just simulate the general implementation details.

In essence, Suspense implementation bottlenecks encapsulate request functions. Suspense mainly accepts promises and resolves them, so that successful states are passed back to asynchronous components that are unknown to developers. For Promise and createFetcher, the state-passing function, the following conditions should be met.

const fetch = createFetcher(function getData(){
    return new Promise((resolve) = >{
       setTimeout(() = >{
            resolve({
                name:'React Advanced Practice Guide'.author:'alien'})},1000)})})function Text(){
    const data = fetch()
    return <div>
        name: {data.name}
        author:{data.author}
    </div>
}
Copy the code
  • throughcreateFetcherEncapsulate the requester function. The request function getData returns a Promise whose mission is to complete the data interaction.
  • A mock asynchronous component that internally requests data using a request function created by createFetcher.

The next step is to write the createFetcher function.

function createFetcher(fn){
    const fetcher = {
        status:'pedding'.result:null.p:null
    }
    return function (){
        const getDataPromise = fn()
        fetcher.p = getDataPromise
        getDataPromise.then(result= >{ /* Succeeded in obtaining data */
            fetcher.result = result
            fetcher.status = 'resolve'
        })
        if(fetcher.status === 'pedding') {/* The first time to interrupt rendering, the second time */
            throw fetcher
        }
        /* Execute */ for the second time
        if(fetcher.status==='resolve')
        return fetcher.result
    }
}
Copy the code
  • The thing to notice herefnisgetDataGetDataPromise isgetDataReturn the Promise.
  • Return a functionfetchIn theTextInternal execution, the first component render, because status = pedding so throws the exception fetcher to Susponse, render abort.
  • ‘Susponse’ will process the fetcher internally at componentDidCatch, ‘getDataPromise.then’, at which point ‘Status’ will be’ resolve ‘and the data will be returned normally.
  • Then Susponse will render the component again and the data will be retrieved as normal.

Now that you have the createFetcher function, the next step is to simulate the upstream component ‘Susponse’.

class MySusponse extends React.Component{
    state={
        isResolve:true
    }
    componentDidCatch(fetcher){
        const p = fetcher.p
        this.setState({ isResolve:false })
        Promise.resolve(p).then(() = >{
            this.setState({ isResolve:true})})}render(){
        const { fallback, children  } = this.props
        const { isResolve } = this.state
        return isResolve ? children : fallback
    }
}

Copy the code

The name that we wrote ‘Basil Basil’ was MySusponse.

  • MySusponse internal componentDidCatch throughPromise.resolveCapture the successful state of the Promise. Once successful, disable fallback UI effects.

With that done, it’s time for the experience. Let’s try the MySusponse effect.

export default function Index(){
    return <div>Hello, world<MySusponse fallback={<div>loading...</div>} >
            <Text />
       </MySusponse>
    </div>
}
Copy the code

Effect:

Although it worked, it was far from being a real basil. The first problem that was exposed was that the data could be changed. The MySusponse data written above is loaded only once, but in general, the data interaction is variable and the data is mutable.

Derivative – Implements an error exception handling component

Anyway, we’re not going to do that in the function component, and we’re not going to write ‘createFetcher’ and ‘Susponse’ ourselves. However, one scenario that is useful is the handling of render errors and degradation of the UI. This situation usually occurs in scenarios where server data is uncertain. For example, we render through server data, such as the following scenario:

<div>{ data.name }</div>
Copy the code

If data is an object, it will render normally, but if data is null, an error will be reported. Without rendering error bounds, a small problem will cause the entire page to not render.

So in this case, if you add componentDidCatch to every page component to catch errors and degrade the UI, then the code is too redundant to reuse, to decouple the degraded UI from the business component.

Therefore, it is possible to write a RenderControlError component in order to uniformly display degraded UI in the case of component exceptions, which also ensures that the whole front-end application does not crash, and also greatly improves the server data format tolerance rate. Let’s look at the implementation.

class RenderControlError extends React.Component{
    state={
        isError:false
    }
    componentDidCatch(){
        this.setState({ isError:true})}render(){
        return !this.state.isError ?
             this.props.children :
             <div style={styles.errorBox} >
                 <img url={require('././assets/img/error.png')}
                     style={styles.erroImage}
                 />
                 <span style={styles.errorText}  >There is an error</span>
             </div>}}Copy the code
  • If children makes an error, degrade the UI.

use

<RenderControlError>
    <Index />
</RenderControlError>
Copy the code

conclusion

This article through some imaginative, strange operation, let everyone understand the principle of basil basil, componentDidCatch and so on. I’m sure soon, with React 18, basil Basil will come to the fore and have a future.

Finally, send rose, hand left lingering fragrance, feel a harvest of friends can give the author praise, pay attention to a wave, update the front end of the super core article.

First come, first served! Here are a few copies of the React Advanced Practice Guide. 30% off the coupon code

Refer to the article

  • React Advanced discusses the React asynchronous component