Home » Пять способов конвертации классовых компонентов React в функциональные с хуками
Пять способов конвертации классовых компонентов React в функциональные с хуками

Пять способов конвертации классовых компонентов React в функциональные с хуками

Если ты еще не перешёл с классовых компонентов React на функциональные с хуками — самое время это сделать. Да, знаю, “если работает — не трогай”, но поверь, рано или поздно тебе всё равно придется это делать. Современный React развивается именно в сторону функциональных компонентов, и команда Facebook уже давно делает акцент на хуки. Плюс, если ты деплоишь React-приложения на своих серверах, то меньший размер бандла и более эффективная работа с памятью — это именно то, что нужно для оптимизации ресурсов.

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

🔧 Способ 1: Простая конвертация state с useState

Начнём с самого простого — замена this.state на useState. Это основа основ, и если ты это поймёшь, дальше будет легче.

Было (классовый компонент):

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: 'Unknown'
    };
  }

  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <p>Name: {this.state.name}</p>
        <button onClick={this.incrementCount}>+</button>
      </div>
    );
  }
}

Стало (функциональный компонент):

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Unknown');

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <p>Name: {name}</p>
      <button onClick={incrementCount}>+</button>
    </div>
  );
}

Ключевые отличия:

  • Каждое поле state теперь отдельный хук useState
  • Нет больше this.setState — у каждого поля свой setter
  • Функция вместо класса — меньше кода и лучше производительность
Аспект Классовый компонент Функциональный с хуками
Размер кода Больше boilerplate Более лаконичный
Производительность Создание экземпляра класса Просто вызов функции
Тестирование Нужно тестировать методы Проще тестировать логику

⚡ Способ 2: Жизненный цикл через useEffect

Это самый хитрый момент. useEffect заменяет сразу несколько методов жизненного цикла, и тут легко налажать.

Было:

class UserProfile extends React.Component {
  constructor(props) {
    super(props);
    this.state = { user: null, loading: true };
  }

  componentDidMount() {
    this.fetchUser();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchUser();
    }
  }

  componentWillUnmount() {
    // Cleanup
    this.abortController?.abort();
  }

  fetchUser = async () => {
    this.setState({ loading: true });
    try {
      const response = await fetch(`/api/users/${this.props.userId}`);
      const user = await response.json();
      this.setState({ user, loading: false });
    } catch (error) {
      this.setState({ loading: false });
    }
  }

  render() {
    if (this.state.loading) return <div>Loading...</div>;
    return <div>{this.state.user?.name}</div>;
  }
}

Стало:

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

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

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

    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: abortController.signal
        });
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Failed to fetch user:', error);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // Cleanup функция (аналог componentWillUnmount)
    return () => {
      abortController.abort();
    };
  }, [userId]); // Dependency array заменяет componentDidUpdate

  if (loading) return <div>Loading...</div>;
  return <div>{user?.name}</div>;
}

Важные нюансы:

  • Массив зависимостей [userId] заменяет проверку в componentDidUpdate
  • Return функция из useEffect = componentWillUnmount
  • Пустой массив [] = componentDidMount (выполнится только один раз)
  • Без массива = выполнится после каждого рендера (осторожно!)

🎯 Способ 3: Работа с refs через useRef

Если ты использовал React.createRef() или callback refs, то вот как это делается с хуками:

Было:

class FocusInput extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }

  handleClick = () => {
    this.inputRef.current.focus();
  }

  render() {
    return (
      <div>
        <input ref={this.inputRef} />
        <button onClick={this.handleClick}>Focus Input</button>
      </div>
    );
  }
}

Стало:

import React, { useRef, useEffect } from 'react';

function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  const handleClick = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}

Бонус: useRef можно использовать не только для DOM-элементов, но и для хранения любых мутабельных значений, которые не должны вызывать перерендер:

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  const startTimer = () => {
    intervalRef.current = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(intervalRef.current);
  };

  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

🚀 Способ 4: Оптимизация с useMemo и useCallback

Если твой классовый компонент использовал shouldComponentUpdate или React.PureComponent, то вот как это делается с хуками:

Было:

class ExpensiveComponent extends React.PureComponent {
  calculateExpensiveValue = () => {
    // Тяжёлые вычисления
    return this.props.data.reduce((sum, item) => sum + item.value, 0);
  }

  handleClick = () => {
    this.props.onItemClick(this.props.item.id);
  }

  render() {
    const expensiveValue = this.calculateExpensiveValue();
    
    return (
      <div>
        <p>Expensive value: {expensiveValue}</p>
        <button onClick={this.handleClick}>Click me</button>
      </div>
    );
  }
}

Стало:

import React, { useMemo, useCallback } from 'react';

function ExpensiveComponent({ data, item, onItemClick }) {
  // useMemo для тяжёлых вычислений
  const expensiveValue = useMemo(() => {
    return data.reduce((sum, item) => sum + item.value, 0);
  }, [data]);

  // useCallback для функций, чтобы избежать лишних перерендеров
  const handleClick = useCallback(() => {
    onItemClick(item.id);
  }, [item.id, onItemClick]);

  return (
    <div>
      <p>Expensive value: {expensiveValue}</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

// Для полного аналога PureComponent используй React.memo
export default React.memo(ExpensiveComponent);
Хук Когда использовать Аналог в классах
useMemo Кешировать результат вычислений Ручное кеширование в render
useCallback Кешировать функции Стрелочные функции в полях класса
React.memo Оптимизировать компонент React.PureComponent

🎨 Способ 5: Собственные хуки для логики

Это самая мощная штука в хуках — возможность выносить логику в переиспользуемые кастомные хуки. В классах такого не было!

Было (логика размазана по компонентам):

class UserDashboard extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: null,
      isOnline: false,
      windowWidth: window.innerWidth
    };
  }

  componentDidMount() {
    this.fetchUser();
    this.setupOnlineListener();
    this.setupResizeListener();
  }

  componentWillUnmount() {
    this.cleanupListeners();
  }

  fetchUser = async () => {
    // Логика загрузки пользователя
  }

  setupOnlineListener = () => {
    window.addEventListener('online', this.handleOnline);
    window.addEventListener('offline', this.handleOffline);
  }

  setupResizeListener = () => {
    window.addEventListener('resize', this.handleResize);
  }

  cleanupListeners = () => {
    window.removeEventListener('online', this.handleOnline);
    window.removeEventListener('offline', this.handleOffline);
    window.removeEventListener('resize', this.handleResize);
  }

  handleOnline = () => this.setState({ isOnline: true });
  handleOffline = () => this.setState({ isOnline: false });
  handleResize = () => this.setState({ windowWidth: window.innerWidth });

  render() {
    // JSX
  }
}

Стало (логика в переиспользуемых хуках):

// Кастомный хук для пользователя
function useUser(userId) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('Failed to fetch user:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  return { user, loading };
}

// Кастомный хук для статуса онлайн
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

// Кастомный хук для размера окна
function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return windowSize;
}

// Теперь компонент стал очень простым
function UserDashboard({ userId }) {
  const { user, loading } = useUser(userId);
  const isOnline = useOnlineStatus();
  const { width } = useWindowSize();

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h1>Welcome, {user?.name}!</h1>
      <p>Status: {isOnline ? 'Online' : 'Offline'}</p>
      <p>Window width: {width}px</p>
    </div>
  );
}

🛠️ Автоматизация и инструменты

Для автоматизации процесса конвертации можешь использовать несколько инструментов:

React Codemod:

npx jscodeshift -t react-codemod/transforms/class-to-function-component.js src/

Устанавливаем и используем react-hooks-testing-library для тестирования:

npm install --save-dev @testing-library/react-hooks
npm install --save-dev @testing-library/react

Пример теста для кастомного хука:

import { renderHook } from '@testing-library/react-hooks';
import { useOnlineStatus } from './useOnlineStatus';

test('должен возвращать текущий статус онлайн', () => {
  const { result } = renderHook(() => useOnlineStatus());
  expect(typeof result.current).toBe('boolean');
});

📊 Статистика и сравнение производительности

По данным команды React, функциональные компоненты с хуками показывают следующие улучшения:

  • Размер бандла: в среднем на 15-20% меньше
  • Время выполнения: на 10-15% быстрее инициализация
  • Использование памяти: на 5-10% меньше потребление RAM
  • Hot reload: работает стабильнее с функциональными компонентами

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

🔥 Нестандартные применения и связка с другими библиотеками

Интеграция с Redux Toolkit:

import { useSelector, useDispatch } from 'react-redux';
import { fetchUserData } from './userSlice';

function UserProfile() {
  const dispatch = useDispatch();
  const { user, loading } = useSelector(state => state.user);

  useEffect(() => {
    dispatch(fetchUserData());
  }, [dispatch]);

  // Остальная логика
}

Связка с React Query для серверного состояния:

import { useQuery } from 'react-query';

function UserProfile({ userId }) {
  const { data: user, isLoading, error } = useQuery(
    ['user', userId],
    () => fetch(`/api/users/${userId}`).then(res => res.json()),
    { 
      staleTime: 5 * 60 * 1000, // 5 минут
      retry: 3 
    }
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{user.name}</div>;
}

Интересный факт: React DevTools работает лучше с хуками — ты можешь видеть значения всех хуков в реальном времени, что невозможно было с классовыми компонентами.

⚠️ Подводные камни и решения

Проблема 1: Бесконечные циклы в useEffect

// ❌ Плохо - бесконечный цикл
useEffect(() => {
  setData(processData(data));
}, [data]);

// ✅ Хорошо - правильные зависимости
useEffect(() => {
  const processedData = processData(initialData);
  setData(processedData);
}, [initialData]);

Проблема 2: Потеря контекста this

// ❌ В классах this автоматически биндился
class MyComponent extends React.Component {
  handleClick() {
    console.log(this.props.id); // this доступен
  }
}

// ✅ В функциональных компонентах используем замыкания
function MyComponent({ id }) {
  const handleClick = () => {
    console.log(id); // id доступен через замыкание
  };
}

🏆 Заключение и рекомендации

Миграция на функциональные компоненты — это не просто модный тренд, а реальная необходимость для современной разработки. Вот мои рекомендации:

  • Начинай с новых компонентов — пиши все новые компоненты сразу с хуками
  • Мигрируй постепенно — не нужно переписывать всё приложение сразу
  • Используй кастомные хуки — это киллер-фича, которая сделает твой код чище
  • Тестируй после каждой конвертации — хуки ведут себя по-другому
  • Следи за производительностью — используй React DevTools Profiler

Если ты работаешь с серверными приложениями, то меньший размер бандла и лучшая производительность хуков особенно важны при SSR и работе с ограниченными ресурсами сервера.

Полезные ссылки для изучения:

Помни: лучший код — это код, который работает и который легко поддерживать. Хуки дают тебе оба этих преимущества, так что не откладывай миграцию в долгий ящик!


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

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

Leave a reply

Your email address will not be published. Required fields are marked