Хорошо ли использовать async componentDidMount ()?


139

Является ли использование componentDidMount()асинхронной функции хорошей практикой в ​​React Native или мне следует избегать этого?

Мне нужно получить некоторую информацию о том, AsyncStorageкогда компонент монтируется, но единственный известный мне способ сделать это возможным - сделать componentDidMount()функцию асинхронной.

async componentDidMount() {
    let auth = await this.getAuth();
    if (auth) 
        this.checkAuth(auth);
}

Есть ли здесь какие-либо проблемы и есть ли другие решения этой проблемы?


2
«Хорошая практика» - это вопрос мнения. Это работает? да.
Крайлог

2
Вот хорошая статья, которая показывает, почему async await - хороший вариант по сравнению с обещаниями hackernoon.com/…
Хатри

просто используйте redux-thunk, это решит проблему
Тилак Мэдди

@TilakMaddy Почему вы предполагаете, что каждое приложение React использует redux?
Миракурун

@Mirakurun, почему все переполнение стека предполагало, что я использую jQuery, когда я раньше задавал простые вопросы javascript?
Тилак Мэдди

Ответы:


162

Начнем с того, что укажем на различия и определим, как это может вызвать проблемы.

Вот код метода componentDidMount()жизненного цикла async и "sync" :

// This is typescript code
componentDidMount(): void { /* do something */ }

async componentDidMount(): Promise<void> {
    /* do something */
    /* You can use "await" here */
}

Посмотрев на код, я могу отметить следующие отличия:

  1. В asyncключевые слова: В машинописи, это просто код маркера. Он делает 2 вещи:
    • Заставить возвращаемый тип быть Promise<void>вместо void. Если вы явно укажете, что тип возвращаемого значения не является обещанием (например, void), машинописный текст выдаст вам ошибку.
    • Позвольте вам использовать awaitключевые слова внутри метода.
  2. Тип возврата изменен с voidнаPromise<void>
    • Это означает, что теперь вы можете сделать это:
      async someMethod(): Promise<void> { await componentDidMount(); }
  3. Теперь вы можете использовать awaitключевое слово внутри метода и временно приостановить его выполнение. Как это:

    async componentDidMount(): Promise<void> {
        const users = await axios.get<string>("http://localhost:9001/users");
        const questions = await axios.get<string>("http://localhost:9001/questions");
    
        // Sleep for 10 seconds
        await new Promise(resolve => { setTimeout(resolve, 10000); });
    
        // This line of code will be executed after 10+ seconds
        this.setState({users, questions});
        return Promise.resolve();
    }

Итак, как они могли вызвать проблемы?

  1. asyncКлючевое слово является абсолютно безвредным.
  2. Я не могу представить себе ситуацию, в которой вам нужно вызвать componentDidMount()метод, поэтому возвращаемый тип Promise<void>тоже безвреден.

    Вызов метода с типом возврата Promise<void>без awaitключевого слова не будет иметь никакого значения от вызова метода с типом возврата void.

  3. Поскольку нет методов жизненного цикла после componentDidMount() отсрочка его выполнения кажется довольно безопасной. Но тут есть подводный камень.

    Скажем, вышеуказанное this.setState({users, questions});будет выполнено через 10 секунд. Посреди времени задержки еще один ...

    this.setState({users: newerUsers, questions: newerQuestions});

    ... были успешно выполнены, и DOM обновлен. Результат был виден пользователям. Часы продолжали идти, и прошло 10 секунд. Затем this.setState(...)будет выполнено отложенное выполнение, и DOM будет обновляться снова, на этот раз с учетом старых пользователей и старых вопросов. Результат также будет виден пользователям.

=> Это довольно безопасно (я не уверен на 100%) использовать asyncс componentDidMount()методом. Я его большой поклонник и пока не сталкивался с проблемами, которые вызывают у меня слишком много головной боли.


Когда вы говорите о проблеме, когда другой setState произошел перед ожидающим Promise, разве это не то же самое с Promise без синтаксического сахара async / await или даже классических обратных вызовов?
Clafou

3
Да! Промедление setState()всегда связано с небольшим риском. Мы должны действовать осторожно.
Cù Đức Hiếu

Думаю, один из способов избежать проблем - использовать что-то вроде isFetching: trueсостояния компонента. Я использовал это только с redux, но я полагаю, что это полностью справедливо с управлением состоянием только для реакции. Хотя на самом деле это не решает проблему обновления одного и того же состояния где-то еще в коде ...
Clafou,

1
Я согласен с этим. Фактически, isFetchingфлаговое решение довольно распространено, особенно когда мы хотим воспроизвести некоторые анимации во внешнем интерфейсе, ожидая ответа ( isFetching: true).
Cù Đức Hiếu

3
Вы можете столкнуться с проблемами, если выполните setState после размонтирования компонента
Элиэзер Штайнбок

18

Обновление от апреля 2020 года: проблема, похоже, исправлена ​​в последней версии React 16.13.1, см. Этот пример песочницы . Спасибо @abernier за указание на это.


Я провел небольшое исследование и обнаружил одно важное отличие: React не обрабатывает ошибки асинхронных методов жизненного цикла.

Итак, если вы напишете что-то вроде этого:

componentDidMount()
{
    throw new Error('I crashed!');
}

тогда ваша ошибка будет поймана границей ошибки , и вы сможете обработать ее и отобразить изящное сообщение.

Если мы изменим код так:

async componentDidMount()
{
    throw new Error('I crashed!');
}

что эквивалентно этому:

componentDidMount()
{
    return Promise.reject(new Error('I crashed!'));
}

тогда ваша ошибка будет молча проглочена . Позор тебе, Реагировать ...

Итак, как мы обрабатываем ошибки? Кажется, единственный способ - это явный улов:

async componentDidMount()
{
    try
    {
         await myAsyncFunction();
    }
    catch(error)
    {
        //...
    }
}

или вот так:

componentDidMount()
{
    myAsyncFunction()
    .catch(()=>
    {
        //...
    });
}

Если мы по-прежнему хотим, чтобы наша ошибка достигла границы ошибки, я могу подумать о следующем трюке:

  1. Поймайте ошибку, заставьте обработчик ошибок изменить состояние компонента
  2. Если состояние указывает на ошибку, выбросить ее из renderметода

Пример:

class BuggyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }

  buggyAsyncfunction(){ return Promise.reject(new Error('I crashed async!'));}

  async componentDidMount() {
    try
    {
      await this.buggyAsyncfunction();
    }
    catch(error)
    {
        this.setState({error: error});
    }
  }

  render() {
    if(this.state.error)
        throw this.state.error;

    return <h1>I am OK</h1>;
  }
}

есть ли проблема, связанная с этим? Было бы полезно сообщить об этом, если все еще так ...
спасибо

@abernier Я думаю, что из соизволения ... Хотя, наверное, они могли бы это улучшить. Я не подавал никаких вопросов по этому поводу ...
CF

1
похоже, что это уже не так, по крайней мере, с React 16.13.1, как это было здесь проверено: codeandbox.io/s/bold-ellis-n1cid?file=/src/App.js
abernier

9

Ваш код прекрасен и очень удобочитаем для меня. См. Эту статью Дейла Джефферсона, где он показывает пример асинхронности componentDidMountи тоже выглядит очень хорошо.

Но некоторые люди скажут, что человек, читающий код, может предположить, что React что-то делает с возвращенным обещанием.

Так что интерпретация этого кода и то, является ли это хорошей практикой, очень личное.

Если вам нужно другое решение, вы можете использовать обещания . Например:

componentDidMount() {
    fetch(this.getAuth())
      .then(auth => {
          if (auth) this.checkAuth(auth)
      })
}

3
... или просто использовать встроенную asyncфункцию с awaits внутри ...?
Эрик Каплун,

также вариант @ErikAllik :)
Tiago Alves

@ErikAllik, а у вас есть пример?
Пабло Ринкон

1
@PabloRincon что-то вроде (async () => { const data = await fetch('foo'); const result = await submitRequest({data}); console.log(result) })()where fetchи submitRequest- это функции, возвращающие обещания.
Эрик Каплун

Этот код определенно плохой, потому что он проглатывает любую ошибку, возникшую в функции getAuth. И если функция что-то делает с сетью (например), следует ожидать ошибок.
CF

6

Когда вы используете componentDidMountбез asyncключевого слова, документ говорит следующее:

Вы можете немедленно вызвать setState () в componentDidMount (). Это вызовет дополнительный рендеринг, но это произойдет до того, как браузер обновит экран.

Если вы используете, async componentDidMountвы потеряете эту способность: другой рендеринг произойдет ПОСЛЕ обновления экрана браузером. Но я думаю, если вы думаете об использовании асинхронного режима, например о получении данных, вы не можете избежать, что браузер обновит экран дважды. В другом мире невозможно ПРИОСТАНОВИТЬ componentDidMount до того, как браузер обновит экран


1
Мне нравится этот ответ, потому что он краток и поддерживается документами. Не могли бы вы добавить ссылку на документы, на которые вы ссылаетесь.
theUtherSide

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

3

Обновить:

(Моя сборка: React 16, Webpack 4, Babel 7):

При использовании Babel 7 вы обнаружите:

Используя этот шаблон ...

async componentDidMount() {
    try {
        const res = await fetch(config.discover.url);
        const data = await res.json();
        console.log(data);
    } catch(e) {
        console.error(e);
    }
}

вы столкнетесь со следующей ошибкой ...

Uncaught ReferenceError: регенератор Время выполнения не определено

В этом случае вам нужно будет установить babel-plugin-transform-runtime.

https://babeljs.io/docs/en/babel-plugin-transform-runtime.html

Если по какой-то причине вы не хотите устанавливать указанный выше пакет (babel-plugin-transform-runtime), вам следует придерживаться шаблона Promise ...

componentDidMount() {
    fetch(config.discover.url)
    .then(res => res.json())
    .then(data => {
        console.log(data);
    })
    .catch(err => console.error(err));
}

3

Я думаю, это нормально, если ты знаешь, что делаешь. Но это может сбивать с толку, потому что async componentDidMount()может все еще работать после componentWillUnmountзапуска и размонтирования компонента.

Вы также можете запускать внутри синхронные и асинхронные задачи componentDidMount. Если бы componentDidMountбыл асинхронный, вам пришлось бы поместить весь синхронный код перед первым await. Для кого-то может быть неочевидно, что код до первого awaitвыполняется синхронно. В этом случае я бы, вероятно, сохранил componentDidMountсинхронность, но использовал бы методы sync и async.

Независимо от того, выбираете ли вы методы вызова async componentDidMount()vs sync , вы должны убедиться, что очистили все прослушиватели или асинхронные методы, которые все еще могут работать, когда компонент отключается.componentDidMount()async


2

Фактически, асинхронная загрузка в ComponentDidMount является рекомендуемым шаблоном проектирования, поскольку React переходит от устаревших методов жизненного цикла (componentWillMount, componentWillReceiveProps, componentWillUpdate) к асинхронному рендерингу.

Это сообщение в блоге очень полезно для объяснения того, почему это безопасно, и предоставления примеров асинхронной загрузки в ComponentDidMount:

https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html


3
Асинхронный рендеринг на самом деле не имеет ничего общего с явно асинхронным жизненным циклом. На самом деле это антипаттерн. Рекомендуемое решение - фактически вызвать асинхронный метод из метода жизненного цикла
Клейтон Рэй

1

Мне нравится использовать что-то вроде этого

componentDidMount(){
   const result = makeResquest()
}
async makeRequest(){
   const res = await fetch(url);
   const data = await res.json();
   return data
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.