← Назад к вопросам

Что будет, если в UseEffect вернуть другую Callback функцию?

1.2 Junior🔥 151 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что будет, если в useEffect вернуть другую Callback функцию?

В React useEffect может возвращать функцию очистки (cleanup function), которая выполняется перед следующим эффектом или при размонтировании компонента. Если вернуть "другую" callback функцию, это может привести к неожиданному поведению.

Основы useEffect cleanup

useEffect(() => {
  // Это побочный эффект
  const subscription = eventBus.subscribe('event', handler);
  console.log('Эффект выполнен');

  // Это функция очистки
  return () => {
    subscription.unsubscribe();
    console.log('Очистка выполнена');
  };
}, []);

Порядок выполнения:

  1. Компонент монтируется
  2. Выполняется основное тело useEffect
  3. Компонент размонтируется или зависимости меняются
  4. Выполняется функция очистки
  5. Если есть новые зависимости, выполняется новый эффект

Что произойдет при возврате другой функции?

Вариант 1: Разные функции на каждый рендер

function MyComponent() {
  useEffect(() => {
    console.log('Эффект');

    // Возвращаем новую функцию КАЖДЫЙ раз
    return () => {
      console.log('Очистка 1');
    };
  }); // Нет зависимостей!

  return <div>Компонент</div>;
}

// Что произойдет:
// 1. Рендер 1 -> Эффект -> (переменные меняются, компонент перерисовывается)
// 2. Рендер 2 -> Очистка 1 (старая функция) -> Эффект -> (переменные меняются)
// 3. Рендер 3 -> Очистка 2 (другая функция) -> Эффект
// ...
// Это запускается КАЖДЫЙ рендер!

Вариант 2: Условный возврат

function ConditionalCleanup({ condition }) {
  useEffect(() => {
    console.log('Эффект');

    if (condition) {
      return () => {
        console.log('Очистка А');
      };
    } else {
      return () => {
        console.log('Очистка Б');
      };
    }
  }, [condition]);
}

// Это работает, но может быть запутанным
// Если condition меняется:
// 1. Выполняется очистка старого условия
// 2. Выполняется новый эффект с новой функцией очистки

Вариант 3: Возврат не функции

function BadCleanup() {
  useEffect(() => {
    console.log('Эффект');

    // ОШИБКА: возвращаем не функцию
    return "это не функция"; // TypeError!
  }, []);

  return <div>Компонент</div>;
}

// React ожидает функцию или undefined
// Получит TypeError: cleanup is not a function

Правильное использование cleanup

function GoodExample({ id }) {
  useEffect(() => {
    let mounted = true;

    const fetchData = async () => {
      const data = await fetch(`/api/data/${id}`);
      if (mounted) {
        // Обновляем state только если компонент все еще на экране
        setData(data);
      }
    };

    fetchData();

    // Возвращаем ОДНУ функцию очистки
    return () => {
      mounted = false;
    };
  }, [id]);

  return <div>{data}</div>;
}

Практические примеры проблем

Проблема 1: Утечка событий

function WindowResizeListener() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      console.log('Resize detected');
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // НЕПРАВИЛЬНО - разные функции очистки
    return () => {
      // Это другая функция! removeEventListener не сработает!
      window.removeEventListener('resize', () => {
        console.log('Resize detected');
        setWidth(window.innerWidth);
      });
    };
  }, []);

  return <div>Width: {width}</div>;
}

// Проблема: обработчик события остается в памяти!
// Каждый раз при resize добавляется новый обработчик

Решение:

function WindowResizeListener() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // ПРАВИЛЬНО - та же функция
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return <div>Width: {width}</div>;
}

Проблема 2: Утечка памяти в интервалах

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

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    // НЕПРАВИЛЬНО
    return () => {
      clearInterval(() => { // Передаем функцию вместо ID!
        setCount(c => c + 1);
      });
    };
  }, []);

  return <div>{count}</div>;
}

// Интервал никогда не очищается!
// Утечка памяти при размонтировании

Решение:

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

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    // ПРАВИЛЬНО
    return () => {
      clearInterval(interval);
    };
  }, []);

  return <div>{count}</div>;
}

Проблема 3: Асинхронные запросы

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      setUser(data); // Race condition!
    };

    fetchUser();

    // НЕПРАВИЛЬНО - разные функции очистки
    return () => {
      // Эта функция не может отменить fetch!
    };
  }, [userId]);

  return <div>{user?.name}</div>;
}

// Если userId меняется быстро:
// 1. Fetch для userId=1 началась
// 2. userId меняется на 2
// 3. Fetch для userId=2 началась
// 4. Fetch 2 завершилась первой -> setUser(user2)
// 5. Fetch 1 завершилась -> setUser(user1) ПЕРЕЗАПИСЫВАЕТ!

Решение с AbortController:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const fetchUser = async () => {
      try {
        const response = await fetch(
          `/api/users/${userId}`,
          { signal: controller.signal }
        );
        const data = await response.json();
        setUser(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error(error);
        }
      }
    };

    fetchUser();

    // ПРАВИЛЬНО - отменяем запрос
    return () => {
      controller.abort();
    };
  }, [userId]);

  return <div>{user?.name}</div>;
}

Типичные ошибки

// ОШИБКА 1: Возврат Promise
useEffect(() => {
  return async () => {
    // Это async функция, не подойдет
    await cleanup();
  };
}, []);

// ОШИБКА 2: Возврат значения
useEffect(() => {
  return 123; // TypeError
}, []);

// ОШИБКА 3: Разные функции
useEffect(() => {
  const listener = () => console.log('event');
  document.addEventListener('click', listener);

  // ОШИБКА: разная функция!
  return () => {
    document.removeEventListener('click', () => {
      console.log('event');
    });
  };
}, []);

Правильные паттерны

Паттерн 1: Простая очистка

useEffect(() => {
  const handler = () => { /* ... */ };
  element.addEventListener('event', handler);

  return () => {
    element.removeEventListener('event', handler);
  };
}, []);

Паттерн 2: Множественная очистка

useEffect(() => {
  const handler1 = () => { /* ... */ };
  const handler2 = () => { /* ... */ };

  element.addEventListener('click', handler1);
  window.addEventListener('resize', handler2);

  return () => {
    element.removeEventListener('click', handler1);
    window.removeEventListener('resize', handler2);
  };
}, []);

Паттерн 3: Условная очистка

useEffect(() => {
  let subscription = null;

  if (isActive) {
    subscription = eventBus.subscribe('event', handler);
  }

  return () => {
    if (subscription) {
      subscription.unsubscribe();
    }
  };
}, [isActive]);

Чеклист правильности

Функция очистки должна:
[] Быть функцией (не Promise, не число)
[] Быть ОДНОЙ и ТОЙ ЖЕ функцией для одних зависимостей
[] Очищать ровно то, что создавал эффект
[] Быть заполнена до размонтирования или нового эффекта
[] Не выбрасывать ошибки

Выводы

  1. useEffect должен возвращать функцию или undefined
  2. Если возвращаешь функцию очистки, она должна быть синхронной
  3. Функция очистки должна удалять именно то, что создал эффект
  4. Используй AbortController для отмены асинхронных операций
  5. Избегай создания новых функций в return - используй переменные из области видимости
  6. Не забывай про утечки памяти - обработчики, интервалы, подписки

Правильное использование cleanup функций — это критично для предотвращения утечек памяти и race condition в React приложениях.

Что будет, если в UseEffect вернуть другую Callback функцию? | PrepBro