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

Как реализовать жизненный цикл через хуки?

2.0 Middle🔥 261 комментариев
#React

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

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

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

Жизненный цикл React компонента через хуки

В функциональных компонентах нет методов жизненного цикла вроде componentDidMount. Вместо этого используются хуки, особенно useEffect. Рассмотрю, как реализовать весь жизненный цикл.

1. Сопоставление методов класса и хуков

// Классовый компонент (старый подход)
class Counter extends React.Component {
  componentDidMount() {
    // Выполнится один раз при монтировании
    console.log("Компонент смонтирован");
  }

  componentDidUpdate(prevProps, prevState) {
    // Выполнится после каждого обновления
    console.log("Компонент обновился");
  }

  componentWillUnmount() {
    // Выполнится при размонтировании
    console.log("Компонент размонтирован");
  }

  render() {
    return <div>{this.state.count}</div>;
  }
}

// Функциональный компонент с хуками (новый подход)
function Counter() {
  const [count, setCount] = useState(0);

  // Эквивалент componentDidMount (только монтирование)
  useEffect(() => {
    console.log("Компонент смонтирован");
  }, []); // Пустой массив зависимостей!

  // Эквивалент componentDidUpdate (после каждого обновления)
  useEffect(() => {
    console.log("Компонент обновился или count изменился");
  }); // Нет массива зависимостей или он не пуст

  // Эквивалент componentWillUnmount (перед размонтированием)
  useEffect(() => {
    return () => {
      console.log("Компонент размонтирован");
    };
  }, []);

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

2. useEffect подробно: монтирование, обновление, размонтирование

import { useEffect, useState } from "react";

function DataFetcher({ userId }: { userId: string }) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // 1. МОНТИРОВАНИЕ: выполнить один раз при загрузке компонента
  useEffect(() => {
    console.log("Компонент смонтирован, загружаем данные");
    
    setIsLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setData(data))
      .catch(err => setError(err.message))
      .finally(() => setIsLoading(false));

    // Очистка при размонтировании (return в useEffect)
    return () => {
      console.log("Компонент размонтируется, отменяем запрос");
      // Отменить fetch, очистить таймеры и т.д.
    };
  }, []); // Пустой массив = выполнить один раз при монтировании

  // 2. ОБНОВЛЕНИЕ: выполнить когда userId изменился
  useEffect(() => {
    console.log(`userId изменился на ${userId}, перезагружаем`);
    // Логика обновления при изменении userId
  }, [userId]); // userId в зависимостях = выполнить при его изменении

  // 3. КАЖДЫЙ РЕНДЕР: без зависимостей выполнится всегда
  // (Не рекомендуется! Может вызвать бесконечные обновления)
  useEffect(() => {
    console.log("Это выполнится после КАЖДОГО рендера!");
  }); // Нет массива зависимостей

  return (
    <div>
      {isLoading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      {data && <p>{JSON.stringify(data)}</p>}
    </div>
  );
}

3. Полный жизненный цикл компонента

function LifecycleExample({ isVisible }: { isVisible: boolean }) {
  const [count, setCount] = useState(0);
  const [status, setStatus] = useState("initialized");

  // ФАЗА 1: МОНТИРОВАНИЕ (один раз)
  useEffect(() => {
    console.log("1. MOUNTED - компонент появился в DOM");
    setStatus("mounted");

    // Инициализация: подписаться на события, запустить таймер и т.д.
    const timer = setInterval(() => {
      console.log("Таймер работает");
    }, 5000);

    // ФАЗА 4: РАЗМОНТИРОВАНИЕ (очистка)
    return () => {
      console.log("4. UNMOUNTED - компонент исчезает из DOM");
      clearInterval(timer); // Очистить таймер
      setStatus("unmounted");
    };
  }, []);

  // ФАЗА 2: ОБНОВЛЕНИЕ (когда isVisible изменяется)
  useEffect(() => {
    if (isVisible) {
      console.log("2a. UPDATED - компонент видимый");
      setStatus("visible");
    } else {
      console.log("2b. UPDATED - компонент скрытый");
      setStatus("hidden");
    }

    // ФАЗА 3: ОЧИСТКА перед следующим обновлением (optional)
    return () => {
      console.log("3. CLEANUP before next update - очищаем старые ресурсы");
    };
  }, [isVisible]); // Зависимость на isVisible

  // ФАЗА 2.2: ОБНОВЛЕНИЕ (когда count изменяется)
  useEffect(() => {
    console.log(`2. COUNT UPDATED - новое значение: ${count}`);

    // Отправить аналитику
    // Обновить заголовок страницы
    document.title = `Count: ${count}`;

    return () => {
      console.log("Очистка перед следующим обновлением count");
    };
  }, [count]);

  // ФАЗА 2.3: ВЫПОЛНИТЬ ПОСЛЕ КАЖДОГО РЕНДЕРА (без зависимостей)
  // Обычно избегают! Может привести к баг-борам
  useEffect(() => {
    console.log("Это выполнится после КАЖДОГО рендера");
    // Опасно! Может вызвать бесконечный цикл
  });

  return (
    <div>
      <p>Status: {status}</p>
      <p>Count: {count}</p>
      <p>isVisible: {isVisible ? "да" : "нет"}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// Использование:
// <LifecycleExample isVisible={true} />
// В консоли:
// 1. MOUNTED
// 2a. UPDATED (visible)
// 2. COUNT UPDATED (0)
// ... click button ...
// 2. COUNT UPDATED (1)
// ... hide component ...
// 3. CLEANUP
// 2b. UPDATED (hidden)
// ... unmount ...
// 4. UNMOUNTED

4. Общие проблемы и как их избежать

// ПРОБЛЕМА 1: Бесконечный цикл обновлений
function BadComponent() {
  const [count, setCount] = useState(0);

  // БЕЗ зависимостей выполняется после каждого рендера
  useEffect(() => {
    setCount(count + 1); // Вызывает новый рендер -> новый useEffect
  }); // Бесконечный цикл!

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

// РЕШЕНИЕ: добавить зависимость или пустой массив
function GoodComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(1); // Выполнится один раз при монтировании
  }, []); // Пустой массив = только при монтировании

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

// ПРОБЛЕМА 2: Утечка памяти - забыли очистить таймер
function BadTimer() {
  useEffect(() => {
    const interval = setInterval(() => {
      console.log("tick");
    }, 1000); // Таймер будет работать даже после размонтирования!
  }, []);
}

// РЕШЕНИЕ: вернуть функцию очистки
function GoodTimer() {
  useEffect(() => {
    const interval = setInterval(() => {
      console.log("tick");
    }, 1000);

    return () => {
      clearInterval(interval); // Очищаем при размонтировании
    };
  }, []);
}

// ПРОБЛЕМА 3: Зависимость не включена
function BadDependency({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
  }, []); // userId не в зависимостях!
  // Будет грузить только первого пользователя

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

// РЕШЕНИЕ: добавить зависимость
function GoodDependency({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
  }, [userId]); // Теперь перезагружает при изменении userId

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

5. Практический пример: полный цикл жизни

interface WindowSize {
  width: number;
  height: number;
}

function ResponsiveComponent() {
  const [windowSize, setWindowSize] = useState<WindowSize>({
    width: window.innerWidth,
    height: window.innerHeight
  });

  // Монтирование + Обновление + Размонтирование
  useEffect(() => {
    // МОНТИРОВАНИЕ: подписаться на событие
    const handleResize = () => {
      console.log("Окно изменило размер");
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    console.log("MOUNTED: добавляю слушатель resize");
    window.addEventListener("resize", handleResize);

    // РАЗМОНТИРОВАНИЕ: отписаться от события
    return () => {
      console.log("UNMOUNTED: удаляю слушатель resize");
      window.removeEventListener("resize", handleResize);
    };
  }, []); // Выполнить только при монтировании/размонтировании

  const isMobile = windowSize.width < 768;

  return (
    <div>
      <p>Window size: {windowSize.width}x{windowSize.height}</p>
      <p>{isMobile ? "Mobile" : "Desktop"}</p>
    </div>
  );
}

6. Кастомные хуки для повтора жизненного цикла

// Кастомный хук для выполнения кода при монтировании
function useMount(callback: () => void | (() => void)) {
  useEffect(callback, []);
}

// Кастомный хук для выполнения кода при размонтировании
function useUnmount(callback: () => void) {
  useEffect(() => callback, []);
}

// Кастомный хук для отслеживания изменений
function useDidUpdate(callback: () => void, dependencies: any[]) {
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }
    callback();
  }, dependencies);
}

// Использование:
function MyComponent() {
  useMount(() => {
    console.log("Компонент смонтирован");
    return () => console.log("Компонент размонтирован");
  });

  useDidUpdate(() => {
    console.log("Произошло обновление");
  }, [someDependency]);

  return <div>Hello</div>;
}

Рекомендация

Жизненный цикл через хуки:

  1. useEffect(fn, []) — монтирование
  2. useEffect(fn, [deps]) — обновление при изменении зависимостей
  3. useEffect(() => cleanup, []) — размонтирование
  4. return () => {} в useEffect — очистка ресурсов
  5. Всегда добавляй зависимости! — иначе будут баги и утечки памяти
Как реализовать жизненный цикл через хуки? | PrepBro