Як правильно працювати з React, щоб уникнути деяких загальних підводних каменів

Клавіатура Macbook Pro

Одне, що я досить часто чую, це “ Давайте підемо за Redux ” у нашому новому додатку React. Це допомагає масштабувати, а дані програми не повинні знаходитись у локальному стані React, оскільки вони неефективні. Або коли ви викликаєте API і поки обіцянка очікує, компонент демонтується, і ви отримуєте наступну красиву помилку.

Попередження: Не вдається викликати setState (або forceUpdate) для немонтованого компонента. Це заборона, але це свідчить про витік пам’яті у вашому додатку. Щоб виправити, скасуйте всі підписки та асинхронні завдання в методі componentWillUnmount.

Тож рішення, яке люди зазвичай отримують, - використання Redux .Я люблю Redux, і робота, яку виконує Ден Абрамов , просто неймовірна! Цей чувак дуже важкий - я б хотів, щоб я був настільки ж талановитим, як він.

Але я впевнений, що коли Ден робив Redux, він просто давав нам інструмент у нашому поясі інструментів як помічник. Це не гніздо всіх інструментів. Ви не використовуєте молоток, коли можете закрутити болт за допомогою викрутки.

Ден навіть погоджується .

Я люблю React, і я працюю над цим вже майже два роки. Поки що не шкодую. Найкраще рішення. Мені подобається Vue та вся крута бібліотека / фреймворки. Але React займає особливе місце в моєму серці. Це допомагає мені зосередитись на роботі, яку я повинен робити, а не забирати весь свій час на маніпуляції DOM. І робить це найкращим та найефективнішим способом. з його ефективним примиренням.

За останні кілька років я багато чому навчився, і помітив поширену проблему серед нових і досвідчених розробників React: неправильне використання React при роботі з передплатою чи асинхронними завданнями. Я відчуваю, що документація, яка там розміщена, у цьому випадку погано оформлена, і тому я вирішив написати цю статтю.

Спочатку я поговорю про підписки, а потім ми перейдемо до обробки асинхронного скасування завдань, щоб уникнути витоків пам’яті в React (основна мета цієї статті). Якщо з цим не впоратись, це уповільнює роботу нашої програми.

А тепер повернімось до того прекрасного повідомлення про помилку, про яке ми спочатку говорили:

Попередження: Не вдається викликати setState (або forceUpdate) для немонтованого компонента. Це заборона, але це свідчить про витік пам’яті у вашому додатку. Щоб виправити, скасуйте всі підписки та асинхронні завдання в методі componentWillUnmount.

Моя мета цієї статті - переконатись, що ніхто ніколи не стикається з цією помилкою і не знає, що з цим робити знову.

Що ми розглянемо

  • Очистити підписки, такі як setTimeout / setInterval
  • Очистити асинхронні дії, коли ви викликаєте запит XHR за допомогою fetchабо таких бібліотек, якaxios
  • Альтернативні методи, деякі сумнівні інші застаріли.

Перш ніж я почну, величезний вигук до Кента С Доддса , найкрутішої людини в Інтернеті зараз. Дякуємо, що знайшли час і віддали громаді. Його подкасти на YoutubeіКурс яєчного голови з розширених моделей компонентів React вражає. Перевірте ці ресурси, якщо хочете зробити наступний крок у своїх навичках React.

Я запитав Кента про кращий підхід, щоб уникнути setState на демонтажі компонента, щоб я міг краще оптимізувати продуктивність React. Він пішов вище і далі і зробив на це відео. Якщо ви людина типу відео, перевірте це нижче. Це дасть вам покрокову прогулянку з докладним поясненням.

Отже, давайте заскочимо, щоб розпочати.

1: Очистити підписки

Почнемо з прикладу:

Давайте поговоримо, що тут щойно сталося. Я хочу, щоб ви зосередили увагу на counter.jsфайлі, який в основному збільшує лічильник через 3 секунди.

Це призводить до помилки через 5 секунд, оскільки я відключив передплату, не очистивши її. Якщо ви хочете побачити помилку ще раз, просто натисніть кнопку оновлення в редакторі CodeSandbox, щоб побачити помилку в консолі.

У мене є файл контейнера, index.jsякий просто перемикає компонент лічильника після перших п’яти секунд.

Тому

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

У своєму Index.js я викликаю Counter.js і просто роблю це у своєму візуалізації:

{showCounter ?  : null}

Це showCounterлогічне значення стану, яке встановлює значення false після перших 5 секунд, як тільки компонент монтується (componentDidMount).

Справжнє, що ілюструє нашу проблему тут, - це counter.jsфайл, який збільшує відлік через кожні 3 секунди. Отже, після перших 3 секунд лічильник оновлюється. Але як тільки доходить до другого оновлення, яке відбувається о 6-мупо-друге, index.jsфайл уже демонтував компонент лічильника о 5-йдруге. На час, коли компонент лічильника досягає 6-гопо-друге, він оновлює лічильник вдруге.

Він оновлює свій стан, але тут проблема. Немає DOM для компонента лічильника для оновлення стану, і саме тоді React видає помилку. Цю прекрасну помилку ми обговорили вище:

Попередження: Не вдається викликати setState (або forceUpdate) для немонтованого компонента. Це заборона, але це свідчить про витік пам’яті у вашому додатку. Щоб виправити, скасуйте всі підписки та асинхронні завдання в методі componentWillUnmount.

Тепер, якщо ви новачок у React, ви можете сказати: “ну, Adeel ... так, але хіба ми не просто демонтували компонент Counter на 5-й секунді? Якщо немає компонента для лічильника, як його стан все ще може оновлюватися на шостій секунді? "

Так, ти правий. Але коли ми робимо щось подібне setTimeoutабо setIntervalв наших компонентах React, це не залежить і не пов’язане з нашим класом React, як ви думаєте. Він буде продовжувати працювати після встановленого стану, якщо або поки ви не скасуєте його підписку.

Тепер ви, можливо, вже робите це, коли ваша умова виконана. Але що, якщо ваша умова ще не виконана, і користувач вирішує змінити сторінки, де ця дія все ще відбувається?

Найкращий спосіб очистити такі підписки - у вашому componentWillUnmountжиттєвому циклі. Ось приклад, як ви можете це зробити. Ознайомтеся з методом componentWillUnmount файлу counter.js:

І це майже все для setTimout& setInterval.

2: Аборти API (XHR)

  • Потворний старий підхід (застарілий)
  • Хороший новий підхід (основна мета цієї статті)

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()

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:

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('//randomuser.me/api/') if (this._isMounted) { this.setState({ user: response.data }) } } componentDidMount() { this._isMounted = true } componentWillUnmount() { this._isMounted = false } render() { return ( Click Me 
{JSON.stringify(this.state.user, null, 2)}
) } }

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

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.

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.

Цей підхід дійсно робить роботу, але, як кажуть документи React:

Основним випадком використання isMounted()є уникнення виклику setState()після того, як компонент відключений, оскільки виклик setState()після відключення компонента видасть попередження. "Попередження setState" існує, щоб допомогти вам ловити помилки, оскільки виклик setState()немонтованого компонента є ознакою того, що ваш додаток / компонент якось не вдалося очистити належним чином. Зокрема, виклик setState()немонтованого компонента означає, що у вашій програмі все ще є посилання на компонент після того, як компонент був відключений - що часто свідчить про витік пам'яті! Детальніше ...

Це означає, що хоча ми і уникли непотрібного setState, пам’ять все ще не очистилася. Все ще відбувається асинхронна дія, яка не знає, що життєвий цикл компонента закінчився і він більше не потрібен.

Давайте поговоримо про правильний шлях

Тут, щоб врятувати день, є AbortControllers . Відповідно до документації MDN, в ній зазначено:

AbortControllerІнтерфейс являє об'єкт контролера , який дозволяє перервати один або кілька запитів DOM , як і коли це необхідно. Читати далі ..

Давайте розглянемо трохи глибше тут. З кодом, звичайно, тому що кожен ❤ код.

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.

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.

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.

Якщо ви хочете прочитати ще більш обширний приклад AbortController, круті люди з MDN мають цей справді приємний та елегантний приклад на своєму Github. Ви можете перевірити це тут.

Я хотів поговорити про ці запити на переривання, тому що не багато людей знають про них. Запит на переривання отримання було розпочато в 2015 році. Ось оригінальний випуск GitHub On Abort - він нарешті отримав підтримку приблизно в жовтні 2017 року. Це розрив у два роки. Оце Так! Є кілька бібліотек, таких як axios, які надають підтримку AbortController. Я обговорю, як ви можете використовувати його з axios, але спочатку я хотів показати поглиблену версію того, як працює AbortController.

Скасування запиту XHR в Axios

“Роби чи ні. Спроби немає ». - Йода

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!

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('//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 ( 
{JSON.stringify(this.state.user, null, 2)}
) } }

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

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.

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.

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.

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.

Final details

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

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?

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 ( 
{JSON.stringify(this.state.user, null, 2)}
) } }; }
export const onLoadUser = async myCancelToken => { try { const { data } = await axios.get('//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.

Дякуємо, що знайшли час для читання. Накричіть мого дуже талановитого колегу Кінана за допомогу в доведенні прочитання цієї статті. Дякуємо Кенту Доддсу за натхнення у спільноті JavaScript OSS.

Знову ж таки, я хотів би почути ваш відгук про це. Ви завжди можете зв’язатися зі мною у Twitter .

Також є ще одне дивовижне прочитання про Abort Controller, яке я знайшов у документації MDN Джейком Арчібальдом . Я пропоную вам прочитати його, якщо у вас є такий цікавий характер, як у мене.