← React/Классовые компоненты React#288 из 383← ПредыдущийСледующий →+15 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksМаршрут: старт с нуля

Классовые компоненты React

Зачем знать классовые компоненты

Классовые компоненты — устаревший синтаксис React. С появлением хуков в React 16.8 (2019) функциональные компоненты полностью заменили их для новых проектов.

Но вы обязательно встретите классовые компоненты в:

  • Унаследованных (legacy) кодовых базах
  • Старых пакетах npm без обновлений
  • Error Boundary (единственный случай, где классы всё ещё нужны)
  • Техническом собеседовании
  • Синтаксис классового компонента

    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>
        )
      }
    }

    setState: асинхронное обновление

    // Проблема: 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 и PureComponent

    // 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. С появлением хуков в React 16.8 (2019) функциональные компоненты полностью заменили их для новых проектов.

    Но вы обязательно встретите классовые компоненты в:

  • Унаследованных (legacy) кодовых базах
  • Старых пакетах npm без обновлений
  • Error Boundary (единственный случай, где классы всё ещё нужны)
  • Техническом собеседовании
  • Синтаксис классового компонента

    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>
        )
      }
    }

    setState: асинхронное обновление

    // Проблема: 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 и PureComponent

    // 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 })).

    Загружаем среду выполнения...
    Загружаем AI-помощника...