当前位置: 首页 > 工具软件 > react-bolt > 使用案例 >

react避免子组件渲染_如何与React一起使用正确的方法来避免一些常见的陷阱

师赤岩
2023-12-01

react避免子组件渲染

One thing I hear quite often is “Let’s go for Redux” in our new React app. It helps you scale, and the App data shouldn’t be in React local state because it is inefficient. Or when you call an API and while the promise is pending, the component get unmounted and you get the following beautiful error.

我经常听到的一件事是我们新的React应用程序中的“ Letux去吧 ”。 它可以帮助您扩展,并且App数据不应该处于React本地状态,因为它效率低下。 或者,当您调用API且未完成Promise时,该组件将被卸载,并出现以下错误。

Warning: Can’t call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
警告:无法在已卸载的组件上调用setState(或forceUpdate)。 这是空操作,但它表明应用程序中发生内存泄漏。 要解决此问题,请在componentWillUnmount方法中取消所有订阅和异步任务。

So the solution people usually arrive at is using Redux. I love Redux and the work that Dan Abramov is doing is simply incredible! That dude rocks big time — I wish I was as half talented as he is.

因此,人们通常会想到的解决方案是使用Redux 我喜欢Redux, Dan Abramov所做的工作简直令人难以置信! 那家伙花了很大的时间-我希望我像他一样才华横溢。

But I am sure that when Dan made Redux, he was just giving us a tool in our tool-belt as a helper. It’s not the Jack of all tools. You don’t use a hammer when you can screw the bolt with a screw driver.

但是我敢肯定,当Dan制作Redux时,他只是在给我们提供工具,作为我们的助手。 不是所有工具的杰克。 使用螺丝刀拧紧螺栓时,请勿使用锤子。

Dan even agrees.

丹甚至同意

I love React, and I have been working on it for almost two years now. So far, no regrets. Best decision ever. I like Vue and all the cool library/frameworks out there. But React holds a special place in my heart. It helps me focus on the work that I am suppose to do rather then taking up all my time in DOM manipulations. And it does this in the best and most efficient way possible. with its effective reconciliation.

我喜欢React,并且我已经从事了将近两年的工作。 到目前为止,没有任何遗憾。 有史以来最好的决定。 我喜欢Vue和所有不错的库/框架。 但是React在我心中占有特殊的位置。 它可以帮助我专注于我应该做的工作,而不是花所有时间从事DOM操作。 它以最佳和最有效的方式做到这一点。 有效的和解

I have learned a lot over these past few years, and I’ve noticed a common problem among new and experienced React developers alike: not using React the right way when dealing with subscription or asynchronous tasks. I feel that the documentation out there isn’t well put up in this case, and so I decided to write this article.

在过去的几年中,我学到了很多东西,而且我注意到新的和经验丰富的React开发人员之间都存在一个共同的问题:在处理订阅或异步任务时,没有以正确的方式使用React。 我觉得这种情况下的文档不够完善,所以我决定写这篇文章。

I’ll talk about subscriptions first, and then we’ll move on to handling asynchronous task cancellation to avoid memory leaks in React (the main purpose of this article). If not handled, this slows our app down.

我将首先讨论订阅,然后我们将继续处理异步任务取消以避免React中的内存泄漏(本文的主要目的)。 如果不处理,这会使我们的应用程序变慢。

Now let’s get back to that beautiful error message that we initially talked about:

现在,让我们回到最初讨论的漂亮错误消息:

Warning: Can’t call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
警告:无法在已卸载的组件上调用setState(或forceUpdate)。 这是空操作,但它表明应用程序中发生内存泄漏。 要解决此问题,请在componentWillUnmount方法中取消所有订阅和异步任务。

My goal for this article is to make sure that no one ever has to face this error and not know what to do about it again.

我在本文中的目标是确保没有人必须面对这个错误并且不知道该怎么办。

我们将介绍的内容 (What we’ll cover)

  • Clear subscriptions like setTimeout/setInterval

    清除订阅,例如setTimeout / setInterval
  • Clear asynchronous actions when you call an XHR request using fetch or libraries like axios

    当您使用fetchaxios库调用XHR请求时,清除异步操作

  • Alternate methods, some opinionated others deprecated.

    替代方法,有些人认为不赞成使用。

Before I start, a huge shout out to Kent C Dodds, the coolest person on the internet right now. Thank you for taking the time & giving back to the community. His Youtube podcasts and egghead course on Advanced React Component Patterns are amazing. Check these resources out if you want to take the next step in your React skills.

在我开始之前,向互联网上最酷的人Kent C Dodds大喊大叫。 感谢您抽出宝贵时间并回馈社区。 他的YouTube 播客 高级React组件模式的 Egghead课程很棒。 如果您想进一步提高React技能,请查阅这些资源。

I asked Kent about a better approach to avoid setState on component unmount so I could better optimize React’s performance. He went above and beyond and made a video on it. If you are a video kind of person, check it out below. It’ll give you a step by step walk through with a detailed explanation.

我问肯特一个更好的方法来避免setState在组件卸载上,这样我可以更好地优化React的性能。 他越过了上面,并制作了一个视频。 如果您是视频类人,请在下面查看。 它会逐步为您提供详细的说明。

So now let’s jump in get started.

现在让我们开始吧。

1:清除订阅 (1: Clear Subscriptions)

Let’s start off with the example:

让我们从示例开始:

Let’s talk what just happened here. What I want you to focus on is the counter.js file which basically increments the counter after 3 seconds.

让我们说说这里发生了什么。 我想让您关注的是counter.js文件,该文件基本上会在3秒后增加计数器的数量。

This gives an error in 5 seconds, because I unmounted a subscription without clearing it. If you want to see the error again, just hit the refresh button in the CodeSandbox editor to see the error in the console.

这会在5秒钟内出现错误,因为我卸载了订阅但未清除它。 如果您想再次看到该错误,只需在CodeSandbox编辑器中单击“刷新”按钮即可在控制台中查看该错误。

I have my container file index.js which simply toggle’s the counter component after the first five seconds.

我有我的容器文件index.js ,它在前五秒后简单地切换了计数器组件。

So

所以

— — — →Index.js
— — —→Index.js
— — — — → Counter.js
— — — —→Counter.js

In my Index.js, I call Counter.js and simply do this in my render:

在我的Index.js中,我调用Counter.js并简单地在渲染器中执行此操作:

{showCounter ? <Counter /> : null}

The showCounter is a state boolean which set’s itself to false after the first 5 seconds as soon as the component mounts (componentDidMount).

showCounter是一个状态布尔值,一旦安装了组件(componentDidMount),就会在最初的5秒内将自身设置为false。

The real thing which illustrates our problem here is the counter.js file which increments the count after every 3 seconds. So after the first 3 seconds, the counter updates. But as soon as it gets to the second update, which happens at the 6th second, the index.js file has already unmounted the counter component at the 5th second. By the time the counter component reaches it’s 6th second, it updates the counter for the second time.

在这里说明我们问题的真正问题是counter.js文件,该文件每3秒增加一次计数。 因此,在开始的3秒后,计数器会更新。 但是,一旦它到达第二次更新,即发生在第六次 其次, index.js文件已经在第5个卸载了计数器组件 第二。 当计数器部分达到第六位时 第二,它第二次更新计数器。

It updates its state, but then here is the problem. There is no DOM for the counter component to update the state to, and that is when React throws an error. That beautiful error we discussed above:

它更新其状态,但是这就是问题所在。 计数器组件没有将状态更新为的DOM,也就是React引发错误时。 我们上面讨论的那个美丽的错误:

Warning: Can’t call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
警告:无法在已卸载的组件上调用setState(或forceUpdate)。 这是空操作,但它表明应用程序中发生内存泄漏。 要解决此问题,请在componentWillUnmount方法中取消所有订阅和异步任务。

Now if you are new to React, you might say, “well Adeel … yeah but didn’t we just unmount the Counter component at the 5th second? If there is no component for counter, how can it’s state still update at the sixth second?”

现在,如果您是React的新手,您可能会说:“好吧Adeel…是的,但是我们不是只是在第5秒钟卸下了Counter组件吗? 如果没有计数器的组件,它的状态如何仍可以在第六秒更新?”

Yes, you are right. But when we do something like setTimeout or setInterval in our React components, it is not dependent on or linked with our React class like you think it may be. It will keep on running after its specified condition unless or until you cancel it’s subscription.

是的,你是对的。 但是,当我们在React组件中执行诸如setTimeoutsetInterval时,它并不像您认为的那样依赖或链接到我们的React类。 它会在指定条件后继续运行,除非或直到您取消订阅为止。

Now you might already be doing this when your condition is met. But what if your condition hasn’t been met yet and the user decides to change pages where this action is still happening?

现在,当满足您的条件时,您可能已经在执行此操作。 但是,如果尚未满足您的条件并且用户决定更改仍在执行此操作的页面该怎么办?

The best way to clear these kinds of subscriptions is in your componentWillUnmount life cycle. Here is an example how you can do it. Check out the counter.js file’s componentWillUnmount method:

清除此类订阅的最佳方法是在componentWillUnmount生命周期中。 这是一个示例,您可以如何做。 查看counter.js文件的componentWillUnmount方法:

And that is pretty much it for setTimout & setInterval.

而对于setTimoutsetInterval来说, setTimout

2:API(XHR)中止 (2: API (XHR) Aborts)

  • The Ugly Old Approach (Deprecated)

    丑陋的旧方法(不建议使用)
  • The Good Newer Approach (The main purpose for this article)

    较新的方法(本文的主要目的)

So, we’ve discussed subscriptions. But what if you make an asynchronous request? How do you cancel it?

因此,我们已经讨论了订阅。 但是,如果您发出异步请求怎么办? 您如何取消呢?

旧的方式 (The old way)

Before I talk about that, I want to talk about a deprecated method in React called isMounted()

在谈论这个之前,我想谈谈React中一个不赞成使用的方法isMounted()

Before December 2015, there was a method called isMounted in React. You can read more about it in the React blog. What it did was something like this:

在2015年12月之前,React中有一种名为isMounted的方法。 您可以在React 博客中阅读更多有关它的信息 它所做的是这样的:

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

class RandomUser extends React.Component {
  state = {user: null}
  _isMounted = false
  handleButtonClick = async () => {
    const response = await axios.get('https://randomuser.me/api/')
    if (this._isMounted) {
      this.setState({ user: response.data })
    }
  }
  componentDidMount() {
    this._isMounted = true
  }
  componentWillUnmount() {
    this._isMounted = false
  }
  render() {
    return (
      <div>
        <button onClick={this.handleButtonClick}>Click Me</button>
        <pre>{JSON.stringify(this.state.user, null, 2)}</pre>
      </div>
    )
  }
}

For the purpose of this example, I am using a library called axios for making an XHR request.

出于本示例的目的,我使用一个名为axios的库来发出XHR请求。

Let’s go through it. I initially set this_isMounted to false right next to where I initialized my state. As soon as the life cycle componentDidMount gets called, I set this._isMounted to true. During that time, if an end user clicks the button, an XHR request is made. I am using randomuser.me. As soon as the promise gets resolved, I check if the component is still mounted with this_isMounted. If it’s true, I update my state, otherwise I ignore it.

让我们经历一下。 最初,我在初始化状态的位置旁边将this_isMounted设置为false 。 一旦生命周期componentDidMount被调用,我将this._isMounted设置为true。 在此期间,如果最终用户单击该按钮,则会发出XHR请求。 我正在使用randomuser.me 。 诺言得到解决后,我将检查组件是否仍通过this_isMounted安装。 如果为真,则更新状态,否则忽略它。

The user might clicked on the button while the asynchronous call was being resolved. This would result in the user switching pages. So to avoid an unnecessary state update, we can simply handle it in our life cycle method componentWillUnmount. I simply set this._isMounted to false. So whenever the asynchronous API call gets resolved, it will check if this_isMounted is false and then it will not update the state.

解决异步调用时,用户可能单击了按钮。 这将导致用户切换页面。 因此,为避免不必要的状态更新,我们可以在生命周期方法componentWillUnmount 。 我只是将this._isMounted设置为false。 因此,每当异步API调用得到解决时,它将检查this_isMounted是否为false,然后将不更新状态。

This approach does get the job done, but as the React docs say:

这种方法确实可以完成工作,但是正如React文档所说:

The primary use case for isMounted() is to avoid calling setState() after a component has unmounted, because calling setState() after a component has unmounted will emit a warning. The “setState warning” exists to help you catch bugs, because calling setState() on an unmounted component is an indication that your app/component has somehow failed to clean up properly. Specifically, calling setState() in an unmounted component means that your app is still holding a reference to the component after the component has been unmounted - which often indicates a memory leak! Read More …

isMounted()的主要用例是避免在组件卸载后调用setState() ,因为在组件卸载后调用setState()会发出警告。 存在“ setState警告”可帮助您捕获错误,因为在已卸载的组件上调用setState()表示您的应用程序/组件无法正确清理。 具体来说,在已卸载的组件中调用setState()意味着您的应用在卸载该组件后仍会保留对该组件的引用-这通常表示内存泄漏! 阅读更多 …

This means that although we have avoided an unnecessary setState, the memory still hasn’t cleared up. There is still an asynchronous action happening which doesn’t know that the component life cycle has ended and it is not needed anymore.

这意味着尽管我们避免了不必要的setState,但内存仍未清除。 仍然有一个异步操作正在发生,它不知道组件的生命周期已经结束并且不再需要它。

让我们谈谈正确的方法 (Let’s Talk About The Right Way)

Here to save the day are AbortControllers. As per the MDN documentation it states:

为了节省一天,这里是AbortControllers 。 根据MDN文档,它指出:

The AbortController interface represents a controller object that allows you to abort one or more DOM requests as and when desired. Read more ..

AbortController接口表示一个控制器对象,允许您根据需要中止一个或多个DOM请求。 阅读更多 ..

Let’s look a bit more in depth here. With code, of course, because everyone ❤ code.

让我们在这里更深入一些。 有了代码,当然是因为每个人都有❤代码。

var myController = new AbortController();
var mySignal = myController.signal;

var downloadBtn = document.querySelector('.download');
var abortBtn = document.querySelector('.abort');

downloadBtn.addEventListener('click', fetchVideo);

abortBtn.addEventListener('click', function() {
  myController.abort();
  console.log('Download aborted');
});

function fetchVideo() {
  ...
  fetch(url, { signal: mySignal }).then(function(response) {
    ...
  }).catch(function(e) {
    reports.textContent = 'Download error: ' + e.message;
  })
}

First we create a new AbortController and assign it to a variable called myController. Then we make a signal for that AbortController. Think of the signal as an indicator to tell our XHR requests when it’s time to abort the request.

首先,我们创建一个新的AbortController并将其分配给名为myController的变量。 然后,我们为该AbortController发出信号 。 可以将信号视为指示符,以告知我们何时终止请求。

Assume that we have 2 buttons, Download and Abort . The download button downloads a video, but what if, while downloading, we want to cancel that download request? We simply need to call myController.abort(). Now this controller will abort all requests associated with it.

假设我们有两个按钮, DownloadAbort 。 下载按钮会下载视频,但是如果我们在下载时要取消该下载请求怎么办? 我们只需要调用myController.abort() 。 现在,此控制器将中止与其关联的所有请求。

How, you might ask?

您可能会问如何?

After we did var myController = new AbortController() we did this var mySignal = myController.signal . Now in my fetch request, where I tell it the URL and the payload, I just need to pass in mySignal to link/signal that FETCh request with my awesome AbortController.

完成var myController = new AbortController()我们执行了var mySignal = myController.signal 。 现在在我的获取请求中,在其中告诉我URL和有效负载的地方,我只需要传入mySignal即可将FETCh请求与我的真棒AbortController链接/信号AbortController

If you want to read an even more extensive example about AbortController, the cool folks at MDN have this really nice and elegant example on their Github. You can check it out here.

如果您想阅读有关AbortController更广泛的示例,则MDN上的好人在其Github上都有这个非常漂亮而优雅的示例。 您可以在这里查看

I wanted to talk about these abort requests was because not many people are aware of them. The request for an abort in fetch started in 2015. Here’s the Original GitHub Issue On Abort — it finally got support around October 2017. That is a gap of two years. Wow! There are a few libraries like axios that give support for AbortController. I will discuss how you can use it with axios, but I first wanted to show the in-depth under-the-hood version of how AbortController works.

我想谈谈这些中止请求是因为没有多少人知道它们。 要求中止获取的请求始于2015 。这是Abort上的原始GitHub问题 -它终于在2017年10月左右获得了支持。这是两年的时间。 哇! 有一些像axios这样的库提供对AbortController的支持。 我将讨论如何在axios上使用它,但是我首先想展示AbortController的工作原理的深入的幕后版本。

在Axios中终止XHR请求 (Aborting An XHR Request In Axios)

“Do, or do not. There is no try.” — Yoda
“做还是不做。 没有尝试。” - 尤达

The implementation I talked about above isn’t specific to React, but that’s what we’ll discuss here. The main purpose of this article is to show you how to clear unnecessary DOM manipulations in React when an XHR request is made and the component is unmounted while the request is in pending state. Whew!

我上面讨论的实现并非特定于React,但这就是我们将在此处讨论的实现。 本文的主要目的是向您展示如何在发出XHR请求并在请求处于挂起状态时卸载组件时在React中清除不必要的DOM操作。 ew!

So without further ado, here we go.

因此,事不宜迟,我们开始吧。

import React, { Component } from 'react';
import axios from 'axios';

class Example extends Component {
  signal = axios.CancelToken.source();

  state = {
    isLoading: false,
    user: {},
  }
  
  componentDidMount() {
    this.onLoadUser();
  }
  
  componentWillUnmount() {
    this.signal.cancel('Api is being canceled');
  }
  
  onLoadUser = async () => {
    try {
      this.setState({ isLoading: true });
      const response = await axios.get('https://randomuser.me/api/', {
        cancelToken: this.signal.token,
      })
      this.setState({ user: response.data, isLoading: true });
    } catch (err) {
      if (axios.isCancel(err)) {
        console.log('Error: ', err.message); // => prints: Api is being canceled
      } else {
        this.setState({ isLoading: false });
      }
    }
   } 
   
    
    render() {
      return (
        <div>
          <pre>{JSON.stringify(this.state.user, null, 2)}</pre>
        </div>
      )
    }
 
}

Let’s walk through this code

让我们看一下这段代码

I set this.signal to axios.CancelToken.source()which basically instantiates a new AbortController and assigns the signal of that AbortController to this.signal. Next I call a method in componentDidMount called this.onLoadUser() which calls a random user information from a third party API randomuser.me. When I call that API, I also pass the signal to a property in axios called cancelToken

我设置this.signalaxios.CancelToken.source()基本上实例化新的AbortController和受让人的,该信号AbortControllerthis.signal 。 接下来,我在componentDidMount调用一个名为this.onLoadUser()的方法,该方法从第三方API randomuser.me调用随机用户信息。 当我调用该API时,我还将信号传递给axios中名为cancelToken的属性

The next thing I do is in my componentWillUnmount where I call the abort method which is linked to that signal. Now let’s assume that as soon as the component was loaded, the API was called and the XHR request went in a pending state.

接下来,我要做的是在我的componentWillUnmount中调用与该signal链接的中止方法。 现在,我们假设一旦加载了组件,就会调用API,并且XHR request went in a pending state

Now, the request was pending (that is, it wasn’t resolved or rejected but the user decided to go to another page. As soon as the life cycle method componentWillUnmount gets called up, we will abort our API request. As soon as the API get’s aborted/cancelled, the promise will get rejected and it will land in the catch block of that try/catch statement, particularly in the if (axios.isCancel(err) {} block.

现在,该请求处于待处理状态(也就是说,该请求尚未解决或拒绝,但用户决定转到另一个页面。生命周期方法componentWillUnmount被调用时,我们将中止我们的API请求。 API get被中止/取消,承诺将被拒绝,它将if (axios.isCancel(err) {}try/catch语句的catch块中,特别是在if (axios.isCancel(err) {}块中。

Now we know explicitly that the API was aborted, because the component was unmounted and therefore logs an error. But we know that we no longer need to update that state since it is no longer required.

现在,我们明确知道该API已中止,因为该组件已卸载,因此记录了一个错误。 但是我们知道我们不再需要更新该状态,因为它不再需要。

P.S: You can use the same signal and pass it as many XHR requests in your component as you like. When the component gets un mounted, all those XHR requests that are in a pending state will get cancelled when componentWillUnmount is called.

PS:您可以使用相同的信号,并根据需要在组件中传递尽可能多的XHR请求。 卸载组件时,调用componentWillUnmount时,所有处于挂起状态的XHR请求都将被取消。

最终细节 (Final details)

Congratulations! :) If you have read this far, you’ve just learned how to abort an XHR request on your own terms.

恭喜你! :)如果您已经阅读了本文,那么您已经学会了如何根据自己的条件中止XHR请求。

Let’s carry on just a little bit more. Normally, your XHR requests are in one file, and your main container component is in another (from which you call that API method). How do you pass that signal to another file and still get that XHR request cancelled?

让我们继续多一点。 通常,您的XHR请求位于一个文件中,而主容器组件位于另一个文件中(从中调用该API方法)。 您如何将该信号传递到另一个文件并仍然取消该XHR请求?

Here is how you do it:

这是您的操作方式:

import React, { Component } from 'react';
import axios from 'axios';

// API
import { onLoadUser } from './UserAPI';

class Example extends Component {
  signal = axios.CancelToken.source();

  state = {
    isLoading: false,
    user: {},
  }
  
  componentDidMount() {
    this.onLoadUser();
  }
  
  componentWillUnmount() {
    this.signal.cancel('Api is being canceled');
  }
  
  onLoadUser = async () => {
    try {
      this.setState({ isLoading: true });
      const data = await onLoadUser(this.signal.token);
      this.setState({ user: data, isLoading: true });
    } catch (error) {
      if (axios.isCancel(err)) {
        console.log('Error: ', err.message); // => prints: Api is being canceled
      } else {
        this.setState({ isLoading: false });
      }
    }
  }
    
    render() {
      return (
        <div>
          <pre>{JSON.stringify(this.state.user, null, 2)}</pre>
        </div>
      )
    }
  };
 
}
export const onLoadUser = async myCancelToken => {
  try {
    const { data } = await axios.get('https://randomuser.me/api/', {
      cancelToken: myCancelToken,
    })
    return data;
  } catch (error) {
    throw error;
  }
};

I hope this has helped you and I hope you’ve learned something. If you liked it, please give it some claps.

希望这对您有所帮助,也希望您学到了一些东西。 如果您喜欢它,请给它鼓掌。

Thank you for taking the time out to read. Shout out to my very talented colleague Kinan for helping me proof read this article. Thanks to Kent C Dodds for being an inspiration in the JavaScript OSS community.

感谢您抽出宝贵的时间阅读。 向我很有才华的同事Kinan大喊大叫,以帮助我证明阅读本文的内容。 感谢Kent C Dodds在JavaScript OSS社区中的启发。

Again, I’d love to hear your feedback on it. You can always reach me out on Twitter.

同样,我很想听听您对此的反馈。 您可以随时在Twitter上与我联系

Also there is another amazing read on Abort Controller that I found through the MDN documentation by Jake Archibald. I suggest you read it, if you have a curios nature like mine.

另外,我在Jake ArchibaldMDN文档中找到了有关Abort Controller的 另一个惊人的读物 。 如果您像我一样有好奇心,我建议您阅读。

翻译自: https://www.freecodecamp.org/news/how-to-work-with-react-the-right-way-to-avoid-some-common-pitfalls-fc9eb5e34d9e/

react避免子组件渲染

 类似资料: