Как правильно отменить все задачи async / await в хуке useEffect для предотвращения утечек памяти при реагировании? - PullRequest
3 голосов
/ 25 октября 2019

Я работаю над приложением Reaction Chap, которое извлекает данные из базы данных Firebase. В моем компоненте «Dashboard» у меня есть проверка ловушки useEffect для аутентифицированного пользователя, и, если это так, извлекает данные из firebase и устанавливает состояние переменной электронной почты и переменной чатов. Я использую abortController для очистки useEffect, однако всякий раз, когда я впервые выхожу из системы и снова вхожу в систему, я получаю предупреждение об утечке памяти.

index.js: 1375 Предупреждение. Невозможно выполнить обновление состояния React для отключенного компонента. Это не работает, но это указывает на утечку памяти в вашем приложении. Чтобы исправить, отмените все подписки и асинхронные задачи в функции очистки useEffect.

в Dashboard (создан Context.Consumer)

Изначально у меня не было abortController, я простовернул консольный лог на clean up. Провел дополнительные исследования и обнаружил abortController, однако в примерах используются fetch и signal, и я не смог найти никаких ресурсов по использованию с async / await. Я открыт для изменения способа извлечения данных (будь то с помощью fetch, async / await или любого другого решения) Я просто не смог заставить его работать с другими методами.

const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);

const signOut = () => {
    firebase.auth().signOut();
  };

useEffect(() => {
    const abortController = new AbortController();
    firebase.auth().onAuthStateChanged(async _user => {
      if (!_user) {
        history.push('/login');
      } else {
        await firebase
          .firestore()
          .collection('chats')
          .where('users', 'array-contains', _user.email)
          .onSnapshot(async res => {
            const chatsMap = res.docs.map(_doc => _doc.data());
            console.log('res:', res.docs);
            await setEmail(_user.email);
            await setChats(chatsMap);
          });
      }
    });

    return () => {
      abortController.abort();
      console.log('aborting...');
    };
  }, [history, setEmail, setChats]);

Ожидаемый результат - правильно очистить / отменить все асинхронные задачи в функции очистки useEffect. После того, как один из пользователей выходит из системы, либо тот же или другой пользователь снова входит в систему, я получаю следующее предупреждение в консоли

index.js: 1375 Предупреждение: не удается выполнить обновление состояния React на отключенном компьютере. составная часть. Это не работает, но это указывает на утечку памяти в вашем приложении. Чтобы исправить, отмените все подписки и асинхронные задачи в функции очистки useEffect.

в Dashboard (создан Context.Consumer)

Ответы [ 3 ]

3 голосов
/ 26 октября 2019

В случае firebase вы имеете дело не с async/await, а с потоками. Вы должны просто отписаться от потоков Firebase в функции очистки:

const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);

const signOut = () => {
    firebase.auth().signOut();
  };

useEffect(() => {
    let unsubscribeSnapshot;
    const unsubscribeAuth = firebase.auth().onAuthStateChanged(_user => {
        // you're not dealing with promises but streams so async/await is not needed here
      if (!_user) {
        history.push('/login');
      } else {
        unsubscribeSnapshot = firebase
          .firestore()
          .collection('chats')
          .where('users', 'array-contains', _user.email)
          .onSnapshot(res => {
            const chatsMap = res.docs.map(_doc => _doc.data());
            console.log('res:', res.docs);
            setEmail(_user.email);
            setChats(chatsMap);
          });
      }
    });

    return () => {
      unsubscribeAuth();
      unsubscribeSnapshot && unsubscribeSnapshot();
    };
  }, [history]); // setters are stable between renders so you don't have to put them here
0 голосов
/ 25 октября 2019

Один из способов отменить async/await - создать нечто вроде встроенного AbortController, который будет возвращать две функции: одну для отмены и одну для проверки отмены, а затем перед каждым шагом в async/await проверку на отменунеобходимо запустить:

function $AbortController() {
  let res, rej;
  const p = new Promise((resolve, reject) => {
    res = resolve;
    rej = () => reject($AbortController.cSymbol);
  })
  
  function isCanceled() {
    return Promise.race([p, Promise.resolve()]);
  }
  
  return [
    rej,
    isCanceled
  ];
}

$AbortController.cSymbol = Symbol("cancel");

function delay(t) {
  return new Promise((res) => {
    setTimeout(res, t);
  })
}

let cancel, isCanceled;

document.getElementById("start-logging").addEventListener("click", async (e) => {
  try {
    cancel && cancel();
    [cancel, isCanceled] = $AbortController();
    const lisCanceled = isCanceled;
    while(true) {
      await lisCanceled(); // check for cancellation
      document.getElementById("container").insertAdjacentHTML("beforeend", `<p>${Date.now()}</p>`);
       await delay(2000);
    }
  } catch (e) {
    if(e === $AbortController.cSymbol) {
      console.log("cancelled");
    }
  }
})

document.getElementById("cancel-logging").addEventListener("click", () => cancel())
<button id="start-logging">start logging</button>
<button id="cancel-logging">cancel logging</button>
<div id="container"></div>
0 голосов
/ 25 октября 2019

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

Если вы хотите дождаться результата из Firestore, вы, вероятно, ищете метод get() . Он получает данные один раз, а затем разрешает обещание.

await firebase
      .firestore()
      .collection('chats')
      .where('users', 'array-contains', _user.email)
      .get(async res => {
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...