Попытка использовать функцию очистки в обработчике useEffect для очистки img.onload - PullRequest
4 голосов
/ 08 ноября 2019

Я недавно создал компонент React (называемый ItemIndexItem), который отображает изображения в моем приложении. Например, у меня есть компонент поиска, который будет показывать индекс отфильтрованных элементов. Каждый отображаемый элемент является компонентом ItemIndexItem. Нажатие ItemIndexItem отправляет вас на страницу ItemShow, где используется тот же ItemIndexItem.

Search.jsx

render() {
  return (
    <ul>
      <li key={item.id}>
        <div>
          <Link to={`/items/${item.id}`}>
            <ItemIndexItem src={item.photos[0].photoUrl} />
            <p>${item.price}</p>
          </Link>
        </div>
      </li>
      ...more li's
    </ul>
  )
}

ItemIndexItem.jsx

import React, { useState, useEffect } from "react";

export default function ItemIndexItem(props) {
  const [imageIsReady, setImageIsReady] = useState(false);

  useEffect(() => {
    let img = new Image();
    img.src = props.src;

    img.onload = () => {
      setImageIsReady(true);
    };
  });

  if (!imageIsReady) return null;

  return (
    <div>
      <img src={props.src} />
    </div>
  );
}

Компонент работает точно так, как требуется, за исключением ошибки утечки памяти, выдаваемой в консоли:

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

в ItemIndexItem (создан ItemShow)

в div (создан ItemShow)

Для справки, это код внутри ItemShow, где я отображаю ItemIndexItem:

ItemShow.jsx

return (
 ...
   <div>
     <ul>
       {this.props.item.photos.map(photo => {
         return (
           <li key={photo.photoUrl}>
             <div>
               <ItemIndexItem type='show' src={photo.photoUrl} />
             </div>
           </li>
         );
       })}
     </ul>
   </div>
 ...

Я пытался использовать функцию возврата useEffect для установкиimg до нуля:

return () => img = null;

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

Ответы [ 3 ]

2 голосов
/ 08 ноября 2019

Вы устанавливаете состояние компонента, который больше не монтируется. Вы можете использовать крючок useRef, чтобы определить, установлен ли ваш компонент или нет, например:

function useIsMounted() {
  const isMounted = React.useRef(true);

  React.useEffect(() => {
    return () => {
      isMounted.current = false;
    };
  }, []);

  return isMounted;
}

и в вашем ItemIndexItem ...

export default function ItemIndexItem(props) {
  const isMounted = useIsMounted();
  const [imageIsReady, setImageIsReady] = useState(false);

  ...
  img.onload = () => {
    if (isMounted.current) {
      setImageIsReady(true);
    }
  ...
}

Как описано в документации React useRef .

useRef возвращает изменяемый объект ref, свойство .current которого инициализируется переданным аргументом (initialValue). Возвращенный объект будет сохраняться в течение всего времени жизни компонента.

Это означает, что вы можете использовать его для создания ссылок на элементы HTML, но вы можете также поместить другие переменные внутри этой ссылки, такие каклогическое. В случае моего хука 'useIsMounting' он устанавливает его как смонтированный при инициализации, и устанавливает его как несмонтированный при размонтировании.

0 голосов
/ 15 ноября 2019

Хотя на этот вопрос уже есть два рабочих ответа, я хотел бы дать третий (надеюсь, более простой):

Вам не нужен еще один useRef или useIfMounted крючок - всевам нужна локальная переменная для отслеживания, если эффект все еще активен, и ваш эффект должен возвращать функцию очистки, которая устанавливает эту переменную на false.

Кроме того, ваш эффект должен зависеть от [props.src] ине на [], потому что если props.src изменится, вы, вероятно, захотите дождаться нового образа:

import React, { useState, useEffect } from "react";

export default function ItemIndexItem(props) {
  const [imageIsReady, setImageIsReady] = useState(false);

  useEffect(() => {
    if (imageIsReady) {
      // Oh, oh, props.src changed ...
      setImageIsReady(false);
    }
    let effectActive = true;
    let img = new Image();
    img.src = props.src;
    img.onload = () => {
      // Only call setImageIsReady if the effect is still active!
      if (effectActive) {
        setImageIsReady(true);
      }
    };
    // The cleanup function below will be called,
    // when either the ItemIndexItem component is
    // unmounted or when props.src changes ...
    return () => { effectActive = false; }
  });

  if (!imageIsReady) return null;

  return (
    <div>
      <img src={props.src} />
    </div>
  );
}
0 голосов
/ 08 ноября 2019

Вы устанавливаете состояние компонента, которого больше нет в дереве. У меня есть небольшая утилита ловушки, чтобы помочь с этим сценарием:

import { useCallback, useEffect, useRef } from 'react'

export const useIfMounted = () => {
  const isMounted = useRef(true)
  useEffect(
    () => () => {
      isMounted.current = false
    }, [])

  const ifMounted = useCallback(
    func => {
      if (isMounted.current && func) {
        func()
      } else {
        console.log('not mounted, not doing anything')
      }
    },[])

  return ifMounted
}

export default useIfMounted

Который вы можете затем использовать так:

    const ifMounted = useIfMounted()

    //other code

    img.onload = () => {
      ifMounted(() => setImageIsReady(true))
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...