Очистка утечек памяти на неустановленном компоненте в React Hooks

19

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

контекст

Я использую Inertia.js с адаптерами Laravel (backend) и React (front-end). Если вы не знаете инерцию, это в основном:

Inertia.js позволяет быстро создавать современные одностраничные приложения React, Vue и Svelte с использованием классической серверной маршрутизации и контроллеров.

вопрос

Я делаю простую страницу входа в систему, которая имеет форму, которая при отправке будет выполнять запрос POST для загрузки следующей страницы. Вроде нормально работает, но на других страницах консоль выдает следующее предупреждение:

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

в логине (создан Inertia)

Связанный код (я упростил его, чтобы избежать ненужных строк):

import React, { useEffect, useState } from 'react'
import Layout from "../../Layouts/Auth";

{/** other imports */}

    const login = (props) => {
      const { errors } = usePage();

      const [values, setValues] = useState({email: '', password: '',});
      const [loading, setLoading] = useState(false);

      function handleSubmit(e) {
        e.preventDefault();
        setLoading(true);
        Inertia.post(window.route('login.attempt'), values)
          .then(() => {
              setLoading(false); // Warning : memory leaks during the state update on the unmounted component <--------
           })                                   
      }

      return (
        <Layout title="Access to the system">
          <div>
            <form action={handleSubmit}>
              {/*the login form*/}

              <button type="submit">Access</button>
            </form>
          </div>
        </Layout>
      );
    };

    export default login;

Теперь я знаю, что должен выполнить функцию очистки, потому что обещание запроса - это то, что генерирует это предупреждение. Я знаю, что я должен использовать, useEffectно я не знаю, как применить это в этом случае. Я видел пример, когда значение меняется, но как это сделать при вызове такого рода?

Заранее спасибо.


Обновить

По запросу полный код этого компонента:

import React, { useState } from 'react'
import Layout from "../../Layouts/Auth";
import { usePage } from '@inertiajs/inertia-react'
import { Inertia } from "@inertiajs/inertia";
import LoadingButton from "../../Shared/LoadingButton";

const login = (props) => {
  const { errors } = usePage();

  const [values, setValues] = useState({email: '', password: '',});

  const [loading, setLoading] = useState(false);

  function handleChange(e) {
    const key = e.target.id;
    const value = e.target.value;

    setValues(values => ({
      ...values,
      [key]: value,
    }))
  }

  function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    Inertia.post(window.route('login.attempt'), values)
      .then(() => {
        setLoading(false);
      })
  }

  return (
    <Layout title="Inicia sesión">
      <div className="w-full flex items-center justify-center">
        <div className="w-full max-w-5xl flex justify-center items-start z-10 font-sans text-sm">
          <div className="w-2/3 text-white mt-6 mr-16">
            <div className="h-16 mb-2 flex items-center">                  
              <span className="uppercase font-bold ml-3 text-lg hidden xl:block">
                Optima spark
              </span>
            </div>
            <h1 className="text-5xl leading-tight pb-4">
              Vuelve inteligente tus operaciones
            </h1>
            <p className="text-lg">
              Recoge data de tus instalaciones de forma automatizada; accede a información histórica y en tiempo real
              para que puedas analizar y tomar mejores decisiones para tu negocio.
            </p>

            <button type="submit" className="bg-yellow-600 w-40 hover:bg-blue-dark text-white font-semibold py-2 px-4 rounded mt-8 shadow-md">
              Más información
            </button>
          </div>

        <div className="w-1/3 flex flex-col">
          <div className="bg-white text-gray-700 shadow-md rounded rounded-lg px-8 pt-6 pb-8 mb-4 flex flex-col">
            <div className="w-full rounded-lg h-16 flex items-center justify-center">
              <span className="uppercase font-bold text-lg">Acceder</span>
            </div>

            <form onSubmit={handleSubmit} className={`relative ${loading ? 'invisible' : 'visible'}`}>

              <div className="mb-4">
                <label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="email">
                  Email
                </label>
                <input
                  id="email"
                  type="text"
                  className=" appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 outline-none focus:border-1 focus:border-yellow-500"
                  placeholder="Introduce tu e-mail.."
                  name="email"
                  value={values.email}
                  onChange={handleChange}
                />
                {errors.email && <p className="text-red-500 text-xs italic">{ errors.email[0] }</p>}
              </div>
              <div className="mb-6">
                <label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="password">
                  Contraseña
                </label>
                <input
                  className=" appearance-none border border-red rounded w-full py-2 px-3 text-gray-700 mb-3 outline-none focus:border-1 focus:border-yellow-500"
                  id="password"
                  name="password"
                  type="password"
                  placeholder="*********"
                  value={values.password}
                  onChange={handleChange}
                />
                {errors.password && <p className="text-red-500 text-xs italic">{ errors.password[0] }</p>}
              </div>
              <div className="flex flex-col items-start justify-between">
                <LoadingButton loading={loading} label='Iniciar sesión' />

                <a className="font-semibold text-sm text-blue hover:text-blue-700 mt-4"
                   href="#">
                  <u>Olvidé mi contraseña</u>
                </a>
              </div>
              <div
                className={`absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center ${!loading ? 'invisible' : 'visible'}`}
              >
                <div className="lds-ellipsis">
                  <div></div>
                  <div></div>
                  <div></div>
                  <div></div>
                </div>
              </div>
            </form>
          </div>
          <div className="w-full flex justify-center">
            <a href="https://optimaee.com">
            </a>
          </div>
        </div>
        </div>
      </div>
    </Layout>
  );
};

export default login;
Кенни Хорна
источник
@Sohail Я добавил полный код компонента
Кенни Хорна
Вы пытались просто удалить .then(() => {})?
Геррик П

Ответы:

22

Поскольку это асинхронный вызов обещания, вы должны использовать изменяемую переменную ref (с useRef), чтобы проверить уже размонтированный компонент для следующей обработки асинхронного ответа (избегая утечек памяти):

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

Два React Hooks, которые вы должны использовать в этом случае: useRefи useEffect.

С useRef, например, изменяемые переменный _isMountedвсегда направлен на одной и те же ссылки в памяти (не локальный переменной)

useRef - это хук, если требуется переменная переменная. В отличие от локальных переменных, React гарантирует, что одна и та же ссылка возвращается во время каждого рендеринга. Если вы хотите, то же самое с this.myVar в компоненте класса

Пример :

const login = (props) => {
  const _isMounted = useRef(true); // Initial value _isMounted = true

  useEffect(() => {
    return () => { // ComponentWillUnmount in Class Component
        _isMounted.current = false;
    }
  }, []);

  function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    ajaxCall = Inertia.post(window.route('login.attempt'), values)
        .then(() => {
            if (_isMounted.current) { // Check always mounted component
               // continue treatment of AJAX response... ;
            }
         )
  }
}

В этом же случае позвольте мне объяснить вам больше информации о React Hooks, используемых здесь. Также я буду сравнивать React Hooks в Функциональном Компоненте (версия React> 16.8) с LifeCycle в Компоненте Класса.

useEffect : большинство побочных эффектов происходит внутри хука. Примерами побочных эффектов являются: выборка данных, настройка подписки и изменение DOM вручную в компонентах React. UseEffect заменяет множество жизненных циклов в компоненте класса (componentDidMount, componentDidUpate, componentWillUnmount)

 useEffect(fnc, [dependency1, dependency2, ...]); // dependencies array argument is optional

1) Поведение useEffect по умолчанию запускается как после первого рендеринга (например, ComponentDidMount), так и после каждого рендеринга обновления (например, ComponentDidUpdate), если у вас нет зависимостей. Это все равно, что :useEffect(fnc);

2) Предоставление массива зависимостей для useEffect изменит его жизненный цикл. В этом примере: useEffect будет вызываться один раз после первого рендеринга, и каждый раз, когда изменяется счетчик

export default function () {
   const [count, setCount] = useState(0);

   useEffect(fnc, [count]);
}

3) useEffect будет запускаться только один раз после первого рендеринга (например, ComponentDidMount), если вы поместите пустой массив для зависимости. Это все равно, что :useEffect(fnc, []);

4) Чтобы предотвратить утечку ресурсов, все должно быть удалено, когда жизненный цикл ловушки заканчивается (например, ComponentWillUnmount) . Например, с пустым массивом зависимостей, возвращаемая функция будет вызываться после размонтирования компонента. Это все равно, что :

useEffect(() => {
   return fnc_cleanUp; // fnc_cleanUp will cancel all subscriptions and asynchronous tasks (ex. : clearInterval) 
}, []);

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

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

const login = (props) => {
  let _isMounted= true; // it isn't good because of a local variable, so the variable will be lost and re-initiated on every update render

  useEffect(() => {
    return () => {
        _isMounted = false;  // not good
    }
  }, []);

  // ...
}

Таким образом, с помощью комбинации useRef и useEffect мы можем полностью устранить утечки памяти.


Хорошие ссылки, которые вы можете прочитать больше о React Hooks:

[EN] https://medium.com/@sdolidze/the-iceberg-of-react-hooks-af0b588f43fb

[FR] https://blog.soat.fr/2019/11/react-hooks-par-lexemple/

SanjiMika
источник
1
Это сработало. Позже сегодня я прочитаю предоставленную ссылку, чтобы понять, как это решает проблему. Если бы вы могли уточнить ответ, включив детали, было бы здорово, так что это будет полезно для других, а также присуждать вам награду после льготного периода. Спасибо.
Кенни Хорна
Спасибо за ваше согласие с моим ответом. Я подумаю о вашей просьбе и сделаю это завтра.
СанджиМика
0

Вы можете использовать метод cancelActiveVisits, Inertiaчтобы отменить активную visitв useEffectочистке ловушку.

Таким образом, с этим вызовом активный visitбудет отменен, и состояние не будет обновлено.

useEffect(() => {
    return () => {
        Inertia.cancelActiveVisits(); //To cancel the active visit.
    }
}, []);

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

 function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    Inertia.post(window.route('login.attempt'), values)
      .then(data => {
         if(data) {
            setLoading(false);
         }
      })
      .catch( error => {
         console.log(error);
      });
  }

Альтернативный способ (обходной путь)

Вы можете использовать, useRefчтобы сохранить статус компонента и на основании этого вы можете обновить state.

Проблема:

Сражение показывается, потому что handleSubmitон пытается обновить состояние компонента, даже если компонент отключен от dom.

Решение:

Установите флажок , чтобы держать статус из component, если componentесть , mountedто flagзначение будет trueи если componentэто unmountedзначение флага будет ложным. Поэтому на основании этого мы можем обновить state. Для статуса флага мы можем использовать useRefссылку.

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

И затем useEffectв функции очистки мы можем установить флагfalse.

функция очистки useEffecr

useEffectКрюк позволяет использовать функцию очистки. Каждый раз, когда эффект больше не действует, например, когда компонент, использующий этот эффект, отключается, эта функция вызывается для очистки всего. В нашем случае мы можем установить флаг в false.

Пример:

let _componentStatus.current =  useRef(true);
useEffect(() => {
    return () => {
        _componentStatus.current = false;
    }
}, []);

И в handleSubmit мы можем проверить, смонтирован ли компонент или нет, и обновить состояние, основываясь на этом.

function handleSubmit(e) {
    e.preventDefault();
    setLoading(true);
    Inertia.post(window.route('login.attempt'), values)
        .then(() => {
            if (_componentStatus.current) {
                setLoading(false);
            } else {
                _componentStatus = null;
            }
        })
}

В противном случае установите _componentStatusзначение null, чтобы избежать утечек памяти.

Sohail
источник
Это не сработало: /
Кенни Хорна
Не могли бы вы утешить значение ajaxCallвнутри useEffect. и посмотрим, какова стоимость
Сохаил
Извините за задержку. Это возвращается undefined. Я добавил это сразу послеreturn () => {
Кенни Хорна
Я изменил код, пожалуйста, попробуйте новый код.
Сохаил
Я не буду говорить, что это исправление или правильный способ решения этой проблемы, но это уберет предупреждение.
Сохаил