Різниця між функціональними та класовими компонентми в React
Функціональні компоненти - це JavaScript-функції, які отримують props і повертають JSX. Класові компоненти - ES6-класи, що розширюють React.Component, зберігають стан через this.state і реагують на lifecycle через спеціальні методи.
Теорія
TL;DR
- Функціональний компонент = виклик функції. Класовий = об'єкт із методами, що живе весь час монтування.
- З React 16.8 хуки (
useState,useEffect) дають функціональним компонентам все те, що мали класи. - Єдиний виняток: error boundaries. Тільки класові компоненти підтримують
componentDidCatch. - Новий код: завжди функціональний компонент. Без обговорень.
Швидкий приклад
// Функціональний: хук керує станом, мінімум коду
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() |
| Lifecycle | useEffect() | componentDidMount(), componentDidUpdate() тощо |
| Context | useContext() | this.context |
this | Не потрібен | Обов'язковий, потребує прив'язки |
| Розмір коду | На 30-50% менше | Більше шаблонного коду |
| Error boundaries | Не підтримуються | Підтримуються через componentDidCatch() |
| Коли використовувати | За замовчуванням для нового коду | Error boundaries, legacy-код |
Типові помилки
Умовний виклик хуків
// НЕПРАВИЛЬНО: порядок хуків порушується між рендерами
function Component({ showEmail }) {
if (showEmail) {
const [email, setEmail] = useState(""); // зсуває порядок викликів
}
}
// ПРАВИЛЬНО: хуки завжди викликаються на верхньому рівні
function Component({ showEmail }) {
const [email, setEmail] = useState("");
// відображаємо поле email лише якщо showEmail === true
}React відстежує хуки за порядком їх виклику. Умовний виклик зсуває цей порядок між рендерами і прив'язує стан не до того хука.
Відсутній масив залежностей у useEffect
// НЕПРАВИЛЬНО: запускається після кожного рендеру, нескінченний цикл
useEffect(() => {
fetch("/api/data").then(r => r.json()).then(setData);
});
// ПРАВИЛЬНО: порожній масив = запускається один раз при монтуванні
useEffect(() => {
fetch("/api/data").then(r => r.json()).then(setData);
}, []);Забута прив'язка методу в класовому компоненті
// НЕПРАВИЛЬНО: 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 для очищення.
Приклади
Форма входу з валідацією
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 (єдиний випадок для класового компонента)
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 пропонує для перехоплення помилок рендеру. Обгорни ним додаток один раз і забудь.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.