Классовые компоненты — устаревший синтаксис React. С появлением хуков в React 16.8 (2019) функциональные компоненты полностью заменили их для новых проектов.
Но вы обязательно встретите классовые компоненты в:
import React from 'react'
class Counter extends React.Component {
// 1. Конструктор: инициализация state
constructor(props) {
super(props) // обязательно! вызов конструктора React.Component
this.state = {
count: 0,
name: props.initialName || 'Гость'
}
// Привязка методов (или используйте стрелочные функции)
this.handleClick = this.handleClick.bind(this)
}
// 2. Методы компонента
handleClick() {
this.setState({ count: this.state.count + 1 })
}
// Стрелочная функция не требует bind:
handleReset = () => {
this.setState({ count: 0 })
}
// 3. Lifecycle методы
componentDidMount() {
// = useEffect(() => {...}, [])
console.log('Компонент смонтирован')
document.title = 'Счётчик: ' + this.state.count
}
componentDidUpdate(prevProps, prevState) {
// = useEffect(() => {...}, [зависимости])
if (prevState.count !== this.state.count) {
document.title = 'Счётчик: ' + this.state.count
}
}
componentWillUnmount() {
// = return () => {...} из useEffect
console.log('Компонент размонтирован')
document.title = 'React App' // очистка
}
// 4. render: обязательный метод
render() {
return (
<div>
<p>Привет, {this.props.name}! Счёт: {this.state.count}</p>
<button onClick={this.handleClick}>+1</button>
<button onClick={this.handleReset}>Сбросить</button>
</div>
)
}
}// Проблема: this.state сразу после setState может быть устаревшим
handleBroken() {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 }) // БАГ: оба читают старый count
// Итог: count увеличится на 1, а не на 2
}
// Решение: функциональная форма setState
handleCorrect() {
this.setState(prev => ({ count: prev.count + 1 }))
this.setState(prev => ({ count: prev.count + 1 })) // корректно
// Итог: count увеличится на 2
}
// setState с колбэком после обновления:
this.setState({ count: newCount }, () => {
console.log('Состояние обновлено:', this.state.count)
})// shouldComponentUpdate: аналог React.memo
class OptimizedList extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// Рендерим только если items изменились
return nextProps.items !== this.props.items
}
render() {
return <ul>{this.props.items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
}
}
// PureComponent: автоматически делает shallow comparison
// = React.memo для классовых компонентов
class PureCounter extends React.PureComponent {
render() {
return <div>{this.props.count}</div>
}
}// БЫЛО: классовый компонент
class UserProfile extends React.Component {
state = { user: null, isLoading: true }
async componentDidMount() {
const user = await fetchUser(this.props.userId)
this.setState({ user, isLoading: false })
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.setState({ isLoading: true })
fetchUser(this.props.userId).then(user =>
this.setState({ user, isLoading: false })
)
}
}
render() {
if (this.state.isLoading) return <Spinner />
return <div>{this.state.user.name}</div>
}
}
// СТАЛО: функциональный компонент
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
setIsLoading(true)
fetchUser(userId).then(user => {
setUser(user)
setIsLoading(false)
})
}, [userId]) // зависимость [userId] = componentDidUpdate
if (isLoading) return <Spinner />
return <div>{user.name}</div>
}| Класс | Хук |
|---|---|
| constructor / state = {...} | useState |
| componentDidMount | useEffect(() => {}, []) |
| componentDidUpdate | useEffect(() => {}, [deps]) |
| componentWillUnmount | return () => {} в useEffect |
| shouldComponentUpdate | React.memo |
| PureComponent | React.memo |
| getDerivedStateFromError | Только классы (Error Boundary) |
| this.forceUpdate() | Нет прямого аналога |
Сравнение классового и функционального подхода через JavaScript-объекты: жизненный цикл, setState, shouldUpdate и конвертация
// Демонстрируем паттерн классового компонента через JavaScript-объект.
// Это показывает как React внутренне управляет жизненным циклом.
// --- "Класс" компонента (аналог React.Component) ---
function createClassComponent(config) {
const { initialState = {}, props = {} } = config
let state = { ...initialState }
let isMounted = false
const renders = []
const component = {
props: { ...props },
state: { ...state },
setState(updater, callback) {
const prevState = { ...component.state }
const update = typeof updater === 'function' ? updater(prevState) : updater
component.state = { ...component.state, ...update }
console.log('setState: обновление', JSON.stringify(update))
// Проверяем shouldComponentUpdate
if (component.shouldComponentUpdate) {
const shouldUpdate = component.shouldComponentUpdate(component.props, component.state)
if (!shouldUpdate) {
console.log('shouldComponentUpdate вернул false — пропускаем рендер')
return
}
}
// Рендер
const output = component.render()
renders.push(output)
console.log('render() → ' + output)
// componentDidUpdate
if (isMounted && component.componentDidUpdate) {
component.componentDidUpdate(component.props, prevState)
}
callback?.()
},
mount() {
console.log('Монтирование компонента...')
// Начальный рендер
const output = component.render()
renders.push(output)
console.log('render() → ' + output)
isMounted = true
if (component.componentDidMount) {
component.componentDidMount()
}
},
unmount() {
console.log('Размонтирование компонента...')
if (component.componentWillUnmount) {
component.componentWillUnmount()
}
isMounted = false
},
getRenderCount: () => renders.length,
getLastRender: () => renders[renders.length - 1],
}
// Применяем config
Object.assign(component, config.methods || {})
if (config.lifecycle) Object.assign(component, config.lifecycle)
return component
}
// --- Компонент 1: Counter с lifecycle ---
console.log('=== Классовый компонент: Counter ===')
const Counter = createClassComponent({
initialState: { count: 0, step: 1 },
props: { title: 'Счётчик' },
methods: {
render() {
return this.props.title + ': ' + this.state.count + ' (шаг: ' + this.state.step + ')'
},
increment() {
this.setState(prev => ({ count: prev.count + prev.step }))
},
reset() {
this.setState({ count: 0 })
},
},
lifecycle: {
componentDidMount() {
console.log('[componentDidMount] Компонент готов. Начальный счёт:', this.state.count)
},
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log('[componentDidUpdate] Счёт изменился:', prevState.count, '→', this.state.count)
}
},
componentWillUnmount() {
console.log('[componentWillUnmount] Очистка ресурсов')
}
}
})
Counter.mount()
Counter.increment()
Counter.increment()
Counter.setState({ step: 5 })
Counter.increment()
Counter.unmount()
console.log('
Всего рендеров:', Counter.getRenderCount())
// --- shouldComponentUpdate: оптимизация ---
console.log('
=== shouldComponentUpdate ===')
const OptimizedList = createClassComponent({
initialState: { items: ['React', 'Vue', 'Angular'], filter: '' },
props: { title: 'Список' },
methods: {
render() {
const filtered = this.state.items.filter(i =>
i.toLowerCase().includes(this.state.filter.toLowerCase())
)
return this.props.title + ': [' + filtered.join(', ') + ']'
},
},
lifecycle: {
shouldComponentUpdate(nextProps, nextState) {
// Рендерим только если items или filter изменились
const shouldUpdate =
nextState.items !== this.state.items ||
nextState.filter !== this.state.filter
if (!shouldUpdate) console.log('[shouldComponentUpdate] Пропускаем')
return shouldUpdate
}
}
})
OptimizedList.mount()
// Обновление не влияет на items/filter — нет рендера
OptimizedList.setState({ unrelatedProp: 'value' })
console.log('Рендеров после бессмысленного setState:', OptimizedList.getRenderCount()) // 1
OptimizedList.setState({ filter: 'r' }) // теперь рендер
console.log('Рендеров после filter:', OptimizedList.getRenderCount()) // 2
console.log('Последний рендер:', OptimizedList.getLastRender())
// --- Конвертация: класс → функция ---
console.log('
=== Конвертация: сравнение подходов ===')
const conversions = [
['this.state = { count: 0 }', 'const [count, setCount] = useState(0)'],
['this.setState({ count: n })', 'setCount(n)'],
['componentDidMount()', 'useEffect(() => {}, [])'],
['componentDidUpdate(prev, prevState)', 'useEffect(() => {}, [deps])'],
['componentWillUnmount()', 'useEffect(() => { return () => cleanup() }, [])'],
['shouldComponentUpdate / PureComponent', 'React.memo(Component)'],
]
console.log('Таблица конвертации:')
conversions.forEach(([classApi, hooksApi]) => {
console.log(' ' + classApi.padEnd(38) + '→ ' + hooksApi)
})Классовые компоненты — устаревший синтаксис React. С появлением хуков в React 16.8 (2019) функциональные компоненты полностью заменили их для новых проектов.
Но вы обязательно встретите классовые компоненты в:
import React from 'react'
class Counter extends React.Component {
// 1. Конструктор: инициализация state
constructor(props) {
super(props) // обязательно! вызов конструктора React.Component
this.state = {
count: 0,
name: props.initialName || 'Гость'
}
// Привязка методов (или используйте стрелочные функции)
this.handleClick = this.handleClick.bind(this)
}
// 2. Методы компонента
handleClick() {
this.setState({ count: this.state.count + 1 })
}
// Стрелочная функция не требует bind:
handleReset = () => {
this.setState({ count: 0 })
}
// 3. Lifecycle методы
componentDidMount() {
// = useEffect(() => {...}, [])
console.log('Компонент смонтирован')
document.title = 'Счётчик: ' + this.state.count
}
componentDidUpdate(prevProps, prevState) {
// = useEffect(() => {...}, [зависимости])
if (prevState.count !== this.state.count) {
document.title = 'Счётчик: ' + this.state.count
}
}
componentWillUnmount() {
// = return () => {...} из useEffect
console.log('Компонент размонтирован')
document.title = 'React App' // очистка
}
// 4. render: обязательный метод
render() {
return (
<div>
<p>Привет, {this.props.name}! Счёт: {this.state.count}</p>
<button onClick={this.handleClick}>+1</button>
<button onClick={this.handleReset}>Сбросить</button>
</div>
)
}
}// Проблема: this.state сразу после setState может быть устаревшим
handleBroken() {
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 }) // БАГ: оба читают старый count
// Итог: count увеличится на 1, а не на 2
}
// Решение: функциональная форма setState
handleCorrect() {
this.setState(prev => ({ count: prev.count + 1 }))
this.setState(prev => ({ count: prev.count + 1 })) // корректно
// Итог: count увеличится на 2
}
// setState с колбэком после обновления:
this.setState({ count: newCount }, () => {
console.log('Состояние обновлено:', this.state.count)
})// shouldComponentUpdate: аналог React.memo
class OptimizedList extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
// Рендерим только если items изменились
return nextProps.items !== this.props.items
}
render() {
return <ul>{this.props.items.map(i => <li key={i.id}>{i.name}</li>)}</ul>
}
}
// PureComponent: автоматически делает shallow comparison
// = React.memo для классовых компонентов
class PureCounter extends React.PureComponent {
render() {
return <div>{this.props.count}</div>
}
}// БЫЛО: классовый компонент
class UserProfile extends React.Component {
state = { user: null, isLoading: true }
async componentDidMount() {
const user = await fetchUser(this.props.userId)
this.setState({ user, isLoading: false })
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.setState({ isLoading: true })
fetchUser(this.props.userId).then(user =>
this.setState({ user, isLoading: false })
)
}
}
render() {
if (this.state.isLoading) return <Spinner />
return <div>{this.state.user.name}</div>
}
}
// СТАЛО: функциональный компонент
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
setIsLoading(true)
fetchUser(userId).then(user => {
setUser(user)
setIsLoading(false)
})
}, [userId]) // зависимость [userId] = componentDidUpdate
if (isLoading) return <Spinner />
return <div>{user.name}</div>
}| Класс | Хук |
|---|---|
| constructor / state = {...} | useState |
| componentDidMount | useEffect(() => {}, []) |
| componentDidUpdate | useEffect(() => {}, [deps]) |
| componentWillUnmount | return () => {} в useEffect |
| shouldComponentUpdate | React.memo |
| PureComponent | React.memo |
| getDerivedStateFromError | Только классы (Error Boundary) |
| this.forceUpdate() | Нет прямого аналога |
Сравнение классового и функционального подхода через JavaScript-объекты: жизненный цикл, setState, shouldUpdate и конвертация
// Демонстрируем паттерн классового компонента через JavaScript-объект.
// Это показывает как React внутренне управляет жизненным циклом.
// --- "Класс" компонента (аналог React.Component) ---
function createClassComponent(config) {
const { initialState = {}, props = {} } = config
let state = { ...initialState }
let isMounted = false
const renders = []
const component = {
props: { ...props },
state: { ...state },
setState(updater, callback) {
const prevState = { ...component.state }
const update = typeof updater === 'function' ? updater(prevState) : updater
component.state = { ...component.state, ...update }
console.log('setState: обновление', JSON.stringify(update))
// Проверяем shouldComponentUpdate
if (component.shouldComponentUpdate) {
const shouldUpdate = component.shouldComponentUpdate(component.props, component.state)
if (!shouldUpdate) {
console.log('shouldComponentUpdate вернул false — пропускаем рендер')
return
}
}
// Рендер
const output = component.render()
renders.push(output)
console.log('render() → ' + output)
// componentDidUpdate
if (isMounted && component.componentDidUpdate) {
component.componentDidUpdate(component.props, prevState)
}
callback?.()
},
mount() {
console.log('Монтирование компонента...')
// Начальный рендер
const output = component.render()
renders.push(output)
console.log('render() → ' + output)
isMounted = true
if (component.componentDidMount) {
component.componentDidMount()
}
},
unmount() {
console.log('Размонтирование компонента...')
if (component.componentWillUnmount) {
component.componentWillUnmount()
}
isMounted = false
},
getRenderCount: () => renders.length,
getLastRender: () => renders[renders.length - 1],
}
// Применяем config
Object.assign(component, config.methods || {})
if (config.lifecycle) Object.assign(component, config.lifecycle)
return component
}
// --- Компонент 1: Counter с lifecycle ---
console.log('=== Классовый компонент: Counter ===')
const Counter = createClassComponent({
initialState: { count: 0, step: 1 },
props: { title: 'Счётчик' },
methods: {
render() {
return this.props.title + ': ' + this.state.count + ' (шаг: ' + this.state.step + ')'
},
increment() {
this.setState(prev => ({ count: prev.count + prev.step }))
},
reset() {
this.setState({ count: 0 })
},
},
lifecycle: {
componentDidMount() {
console.log('[componentDidMount] Компонент готов. Начальный счёт:', this.state.count)
},
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
console.log('[componentDidUpdate] Счёт изменился:', prevState.count, '→', this.state.count)
}
},
componentWillUnmount() {
console.log('[componentWillUnmount] Очистка ресурсов')
}
}
})
Counter.mount()
Counter.increment()
Counter.increment()
Counter.setState({ step: 5 })
Counter.increment()
Counter.unmount()
console.log('
Всего рендеров:', Counter.getRenderCount())
// --- shouldComponentUpdate: оптимизация ---
console.log('
=== shouldComponentUpdate ===')
const OptimizedList = createClassComponent({
initialState: { items: ['React', 'Vue', 'Angular'], filter: '' },
props: { title: 'Список' },
methods: {
render() {
const filtered = this.state.items.filter(i =>
i.toLowerCase().includes(this.state.filter.toLowerCase())
)
return this.props.title + ': [' + filtered.join(', ') + ']'
},
},
lifecycle: {
shouldComponentUpdate(nextProps, nextState) {
// Рендерим только если items или filter изменились
const shouldUpdate =
nextState.items !== this.state.items ||
nextState.filter !== this.state.filter
if (!shouldUpdate) console.log('[shouldComponentUpdate] Пропускаем')
return shouldUpdate
}
}
})
OptimizedList.mount()
// Обновление не влияет на items/filter — нет рендера
OptimizedList.setState({ unrelatedProp: 'value' })
console.log('Рендеров после бессмысленного setState:', OptimizedList.getRenderCount()) // 1
OptimizedList.setState({ filter: 'r' }) // теперь рендер
console.log('Рендеров после filter:', OptimizedList.getRenderCount()) // 2
console.log('Последний рендер:', OptimizedList.getLastRender())
// --- Конвертация: класс → функция ---
console.log('
=== Конвертация: сравнение подходов ===')
const conversions = [
['this.state = { count: 0 }', 'const [count, setCount] = useState(0)'],
['this.setState({ count: n })', 'setCount(n)'],
['componentDidMount()', 'useEffect(() => {}, [])'],
['componentDidUpdate(prev, prevState)', 'useEffect(() => {}, [deps])'],
['componentWillUnmount()', 'useEffect(() => { return () => cleanup() }, [])'],
['shouldComponentUpdate / PureComponent', 'React.memo(Component)'],
]
console.log('Таблица конвертации:')
conversions.forEach(([classApi, hooksApi]) => {
console.log(' ' + classApi.padEnd(38) + '→ ' + hooksApi)
})Создай классовый компонент Timer с полным жизненным циклом. Таймер должен: отображать секунды; иметь кнопки Start/Stop/Reset; обновлять document.title при изменении счётчика; очищать интервал при размонтировании. Заполни пропуски (???) для: обновления state через setState, сравнения prevState в componentDidUpdate, очистки в componentWillUnmount.
В componentDidUpdate: prevState.seconds !== this.state.seconds. В componentWillUnmount: if (this.intervalId) clearInterval(this.intervalId). В handleStart для setState: this.setState(prev => ({ seconds: prev.seconds + 1 })).