Поиск по сайту:

Исправление ошибок React «Called SetState() on an Unmounted Component»


Увидеть Called setState() в несмонтированном компоненте в вашей консоли — одна из наиболее частых проблем, с которыми сталкиваются новички в React. Когда вы работаете с компонентами класса, вы можете легко создать ситуации, в которых вы сталкиваетесь с этой ошибкой.

Обновление состояния компонентов, которые были удалены из DOM, обычно сигнализирует об утечке памяти в вашем приложении. Это тратит впустую аппаратные ресурсы вашего пользователя и приведет к постепенному снижению производительности, если его не остановить. Вот почему возникает ошибка и что вы можете сделать, чтобы ее устранить.

Простой компонент

Вот базовый компонент React, который извлекает некоторые данные по сети:

import React from "react";
 
class NewsList extends React.Component {
 
    state = {
 
        news: null
 
    };
 
 
    componentDidMount() {
        fetch("http://example.com/news.json").then(res => {
            this.setState({news: response.json()});
        }).catch(e => {
            alert("Error!");
        });
    }
 
 
    render() {
        if (!this.state.news) return "Loading...";
        else return news.map((story, key) => <h1 key={key}>{story.Headline}</h1>);
    }
 
}

Этот компонент может генерировать ошибки вызываемого setState() для несмонтированного компонента. Чтобы понять почему, рассмотрим, что происходит, когда вы посещаете страницу, отображающую NewsList. Компонент сделает асинхронный сетевой запрос, а затем добавит полученный список новостей в свое состояние.

Проблема возникает, когда для разрешения сетевого запроса требуется некоторое время. Если новостей много, а у пользователя нестабильное 3G-соединение, это может занять несколько секунд. Тем временем пользователь, возможно, сдался и перешел на другую страницу. Это размонтирует NewsList и удалит его из DOM.

Несмотря на навигацию, сетевой запрос все еще выполняется. В конце концов он разрешится, и будет запущен обратный вызов .then(). Согласно сообщению об ошибке, setState() вызывается для размонтированного экземпляра NewsList.

Теперь вы зря тратите память, сохраняя список новостей в состоянии несмонтированного компонента. Пользователь никогда не увидит данные — если он вернется на страницу со списком новостей, новый компонент NewsList смонтирует и получит свои собственные данные.

Решение проблемы

Проблема с этим примером легко решается. Вот действительно простой подход:

class NewsList extends React.Component {
 
    mounted: false;
 
    state = {
 
        news: null
 
    };
 
 
    componentDidMount() {
 
        this.mounted = true;
 
        fetch("http://example.com/news.json").then(res => {
            if (this.mounted) {
                this.setState({news: response.json()});
            }
        }).catch(e => {
            alert("Error!");
        });
 
    }
 
 
    componentWillUnmount() {
        this.mounted = false;
    }
 
}

Теперь результаты вызова API игнорируются, если только компонент еще не смонтирован. Когда компонент собирается размонтироваться, React вызывает componentWillUnmounted(). Переменная экземпляра mounted компонента получает значение false, позволяя обратному вызову fetch узнать, подключен ли он к DOM.

Это работает, но добавляет шаблонный код для отслеживания состояния mounted. Сетевой вызов также будет выполняться до завершения, потенциально тратя полосу пропускания. Вот лучшая альтернатива, которая использует AbortController для отмены вызова fetch на полпути:

class NewsList extends React.Component {
 
    abortController = new AbortController();
 
    state = {
 
        news: null
 
    };
 
 
    componentDidMount() {
        fetch("http://example.com/news.json", {signal: this.abortController.signal}).then(res => {
            this.setState({news: response.json()});
        }).catch(e => {
            if (e.name === "AbortError") {
                // Aborted as unmounting
            }
            else alert("Error!");
        });
    }
 
 
    componentWillUnmount() {
        this.abortController.abort();
    }
 
}

Теперь вызов fetch получает AbortSignal, который можно использовать для отмены запроса. Когда React собирается размонтировать компонент, вызывается метод abort() контроллера прерывания. Это будет отражено в сигнале, переданном fetch, и браузер обработает отмену сетевого запроса. Обратный вызов .then() не будет выполняться, поэтому ваш компонент не будет пытаться обновить свое состояние после размонтирования.

Другие возможные причины

Другая распространенная причина этой ошибки — когда вы добавляете прослушиватели событий или таймеры в свой компонент, но не очищаете их перед размонтированием:

class OfflineWarning extends React.Component {
 
    state = {online: navigator.onLine};
 
    handleOnline = () => this.setState({online: true});
 
    handleOffline = () => this.setState({online: false});
 
    componentDidMount() {
        window.addEventListener("online", this.handleOnline);
        window.addEventListener("offline", this.handleOffline);
    }
 
    render() {
        return (!this.state.online ? "You're offline!" : null);
    }
 
}

Если пользователь переходит на экран, на котором не отображается OfflineWarning, вы получите сообщение об ошибке вызванный setState() при изменении условий его сети. Хотя компонент больше не монтируется, настроенные им прослушиватели событий браузера по-прежнему будут активны.

Пользователь может несколько раз переключаться между экраном с OfflineWarning и экраном без него. Это привело бы к появлению нескольких экземпляров невидимых компонентов, каждый из которых избыточно прослушивал бы сетевые события.

Вы можете решить эту проблему, просто изменив операции при размонтировании вашего компонента:

class OfflineWarning extends React.Component {
 
    componentDidMount() {
        window.addEventListener("online", this.handleOnline);
        window.addEventListener("offline", this.handleOffline);
    }
 
    componentWillUnmount() {
        window.removeEventListener("online", this.handleOnline);
        window.removeEventListener("offline", this.handleOffline);
    }
 
}

Используйте ту же модель при работе с таймерами и интервалами. Если вы используете setTimeout() или setInterval() в любом месте вашего компонента, вы должны использовать clearTimeout() и clearInterval() до того, как он размонтируется.

Переопределение функции setState()

Другой вариант — создать собственный базовый компонент, который переопределяет setState():

class SafeComponent extends React.PureComponent {
 
    mounted = false;
 
    componentDidMount() {
        this.mounted = true;
    }
 
    componentWillUnmount() {
        this.mounted = false;
    }
 
    setState(state, callback) {
        if (this.mounted) {
            super.setState(state, callback);
        }
    }
 
}

Любые компоненты, которые вызывают setState() в асинхронном обратном вызове, могут затем расширяться от SafeComponent вместо React.PureComponent. Родительский элемент SafeComponent отслеживает, смонтирован ли ваш компонент. Вызовы setState() будут игнорироваться, если они получены в размонтированном состоянии.

Этот подход на самом деле не устраняет корень ваших проблем. Он эффективен для подавления ошибки консоли и предотвращения несмонтированных обновлений состояния, но его не следует использовать вместо правильной очистки таймеров и прослушивателей событий.

Тем не менее, этот вариант может быть полезен в качестве временной меры, когда вы новичок в кодовой базе с большим количеством проблем. Это также может быть приемлемым решением для ошибок, вызванных незначительными сетевыми запросами, которые вы не чувствуете необходимости должным образом прерывать. Если вас устраивает получение и отбрасывание данных после того, как пользователь уходит с экрана, использование пользовательского базового компонента позволяет избежать добавления повторяющейся логики «подключено» к каждому из ваших компонентов.

Если вы выберете этот маршрут, не забудьте вызвать super.componentDidMount() и super.componentWillUnmount() в своих дочерних компонентах при переопределении этих методов. В противном случае свойство mounted не будет установлено правильно. Если вы забудете вызвать super.componentDidMount(), это будет означать, что mounted всегда false, в результате чего каждое обновление состояния будет игнорироваться!

Краткое содержание

Если вы видите вызванный setState() для несмонтированного компонента в консоли вашего браузера, это означает, что обратный вызов для асинхронной операции все еще выполняется после удаления компонента из DOM. Это указывает на утечку памяти, вызванную выполнением избыточной работы, от которой пользователь никогда не получит выгоды.

Вы можете решить эти проблемы, внедрив componentWillUnmounted() и правильно очистив свой компонент. Отмените незавершенные сетевые запросы, удалите прослушиватели событий и отмените все созданные вами таймеры. Это гарантирует, что в будущем не останется кода для запуска, поэтому вашему компоненту не нужно будет оставаться и пытаться обновить свое состояние после того, как он ушел из DOM.




Все права защищены. © Linux-Console.net • 2019-2024