Skip to main content

Різниця між функціональними та класовими компонентми в React

Функціональні компоненти - це JavaScript-функції, які отримують props і повертають JSX. Класові компоненти - ES6-класи, що розширюють React.Component, зберігають стан через this.state і реагують на lifecycle через спеціальні методи.

Теорія

TL;DR

  • Функціональний компонент = виклик функції. Класовий = об'єкт із методами, що живе весь час монтування.
  • З React 16.8 хуки (useState, useEffect) дають функціональним компонентам все те, що мали класи.
  • Єдиний виняток: error boundaries. Тільки класові компоненти підтримують componentDidCatch.
  • Новий код: завжди функціональний компонент. Без обговорень.

Швидкий приклад

jsx
// Функціональний: хук керує станом, мінімум коду function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } // Класовий: той самий результат, але більше шаблонного коду class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return ( <button onClick={() => this.setState({ count: this.state.count + 1 })}> {this.state.count} </button> ); } }

Функціональна версія зазвичай на 30-50% коротша. Вивід однаковий.

Головна різниця

Коли React рендерить функціональний компонент, він викликає функцію з props і отримує JSX. Кожен рендер - окремий виклик зі своїм scope. Класовий компонент працює інакше: React створює один екземпляр і зберігає його. При кожному оновленні викликається render(), але сам об'єкт живе постійно. Саме тому this.state зберігається між рендерами без хуків, але водночас ти постійно думаєш про прив'язку методів через this.

Коли що використовувати

  • Функціональний: будь-яка нова фіча, новий проект, будь-який компонент зі станом або side effects
  • Класовий: error boundaries (єдиний спосіб зловити помилки рендеру через componentDidCatch), підтримка наявного класового коду

Таблиця порівняння

АспектФункціональнийКласовий
Синтаксисfunction MyComponent(props) {}class MyComponent extends React.Component {}
Станхук useState()this.state + setState()
LifecycleuseEffect()componentDidMount(), componentDidUpdate() тощо
ContextuseContext()this.context
thisНе потрібенОбов'язковий, потребує прив'язки
Розмір кодуНа 30-50% меншеБільше шаблонного коду
Error boundariesНе підтримуютьсяПідтримуються через componentDidCatch()
Коли використовуватиЗа замовчуванням для нового кодуError boundaries, legacy-код

Типові помилки

Умовний виклик хуків

jsx
// НЕПРАВИЛЬНО: порядок хуків порушується між рендерами function Component({ showEmail }) { if (showEmail) { const [email, setEmail] = useState(""); // зсуває порядок викликів } } // ПРАВИЛЬНО: хуки завжди викликаються на верхньому рівні function Component({ showEmail }) { const [email, setEmail] = useState(""); // відображаємо поле email лише якщо showEmail === true }

React відстежує хуки за порядком їх виклику. Умовний виклик зсуває цей порядок між рендерами і прив'язує стан не до того хука.

Відсутній масив залежностей у useEffect

jsx
// НЕПРАВИЛЬНО: запускається після кожного рендеру, нескінченний цикл useEffect(() => { fetch("/api/data").then(r => r.json()).then(setData); }); // ПРАВИЛЬНО: порожній масив = запускається один раз при монтуванні useEffect(() => { fetch("/api/data").then(r => r.json()).then(setData); }, []);

Забута прив'язка методу в класовому компоненті

jsx
// НЕПРАВИЛЬНО: this = undefined всередині колбека class Button extends React.Component { handleClick() { console.log(this.state); // TypeError } render() { return <button onClick={this.handleClick}>Click</button>; } } // ПРАВИЛЬНО: стрілкова функція в полі класу зберігає this class Button extends React.Component { handleClick = () => { console.log(this.state); // працює }; render() { return <button onClick={this.handleClick}>Click</button>; } }

Де зустрічається в реальних проектах

  • React-документація і офіційні приклади: виключно функціональні компоненти з React 16.8
  • Next.js: функціональний за замовчуванням; класовий з'являється тільки як error boundary
  • Redux: хуки useSelector і useDispatch замінили патерн з connect() HOC
  • React Query: useQuery і інші хуки потребують функціональних компонентів
  • Error boundaries: тільки класові. Функціонального аналога в поточних версіях React немає.

У більшості проектів є один ErrorBoundary-клас на рівні кореня, а решта компонентів - функціональні.

Питання на співбесіді

Q: Чому error boundaries не можуть бути функціональними компонентами?
A: Error boundaries потребують componentDidCatch і getDerivedStateFromError, які є методами lifecycle тільки для класів. React поки не додав хуків для цього. Стандартне рішення - один класовий компонент, що обгортає дерево компонентів.

Q: Чи дійсно функціональні компоненти з хуками працюють швидше за класові?
A: Не в базі. Обидва дають подібний скомпільований код. Функціональні компоненти простіше оптимізувати: логіку ділять на окремі хуки і мемоізують кожен шматок через useMemo і useCallback. У класах для цього потрібен shouldComponentUpdate або PureComponent.

Q: Що таке stale closure (застаріле замикання) і чому це торкається тільки функціональних компонентів?
A: Stale closure виникає, коли колбек захоплює стару версію змінної стану. Класові компоненти обходять це, бо this.state завжди вказує на поточний стан екземпляра. У функціональних компонентах вирішується через форму з оновлювачем: setCount(prev => prev + 1) замість setCount(count + 1).

Q: Як замінити методи lifecycle класу на хуки?
A: Окремий useEffect для кожної задачі. useEffect(() => {...}, []) замінює componentDidMount. useEffect(() => {...}, [dep]) замінює componentDidUpdate для конкретного значення. Функція, повернута з ефекту, замінює componentWillUnmount для очищення.

Приклади

Форма входу з валідацією

jsx
function LoginForm() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [errors, setErrors] = useState({}); const handleSubmit = (e) => { e.preventDefault(); const newErrors = {}; if (!email.includes("@")) newErrors.email = "Невалідний email"; if (password.length < 8) newErrors.password = "Мінімум 8 символів"; if (Object.keys(newErrors).length > 0) { setErrors(newErrors); return; } console.log("Відправляємо форму..."); }; return ( <form onSubmit={handleSubmit}> <input value={email} onChange={(e) => setEmail(e.target.value)} /> {errors.email && <span>{errors.email}</span>} <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /> {errors.password && <span>{errors.password}</span>} <button type="submit">Увійти</button> </form> ); }

Три змінні стану, логіка валідації, жодного this, жодної прив'язки. Кожен стан живе поруч із логікою, яка його використовує.

Error boundary (єдиний випадок для класового компонента)

jsx
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError() { return { hasError: true }; } componentDidCatch(error, info) { console.error("Перехоплено помилку рендеру:", error, info); } render() { if (this.state.hasError) { return <h1>Щось пішло не так.</h1>; } return this.props.children; } }

Це не застарілий патерн. Це єдиний варіант, який React пропонує для перехоплення помилок рендеру. Обгорни ним додаток один раз і забудь.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?