По мере того, как React рос и развивался на протяжении многих лет, мы наблюдали изменения в известной среде Javascript/Typescript с точки зрения лучших практик создания и реализации компонентов. Как человек, который учился с компонентами на основе классов (и рассказывал о них другим), переписывание компонентов с использованием синтаксиса функциональных компонентов имело свои особенности. В этом посте будет описано и продемонстрировано, как вы можете взять ваши текущие компоненты класса и преобразовать их в функциональные компоненты с помощью нескольких быстрых переписываний.

Базовый состав компонентов с использованием методов жизненного цикла

В следующих примерах у нас есть оболочка макета компонента на основе классов с компонентами на основе функций.

Если вы не знакомы с компонентом на основе классов, вот основы:

  • Прежде чем компонент будет смонтирован в DOM, запускается метод constructor(). Затем компонент выполняет начальный проход метода render().
  • После первого запуска render() в жизненном цикле вызывается componentDidMount(), который является необязательным методом, и запускает там любой код. После завершения componentDidMount() снова запускается render(), а componentDidMount() больше никогда не вызывается.
  • Если происходит обновление состояния и оно определено, каждый раз запускается componentDidUpdate(), за которым следует еще один запуск метода render().
  • Наконец, когда компонент собирается быть размонтированным из дерева DOM, вызывается componentWillUnmount() для выполнения любой работы по очистке, если вы ее реализовали.

По сути, порядок операций компонента на основе классов — constructor() ---> render() ---> componentDidMount() ---> render() ---> componentDidUpdate <---> render() ---> componentWillUnmount(). Функциональные компоненты могут быть написаны с похожим жизненным циклом, но с большим отличием; каждый из этих методов жизненного цикла может быть обработан хуком useEffect()! Если мы возьмем наш пример на основе классов, вот как он потенциально может выглядеть как функциональный компонент:

Для функциональных компонентов useEffect() является универсальным крючком. В приведенном выше примере мы видим различные способы написания useEffect() для достижения паритета с методами жизненного цикла компонентов на основе классов. useEffect() — это хук, который, вообще говоря, принимает в качестве параметра анонимную функцию. Когда конкретный useEffect() вызывается в жизненном цикле, данный код анонимной функции запускается соответствующим образом. По своей сути, без каких-либо дополнений или модификаций, useEffect(() => { ... }) работает как componentDidUpdate().

componentDidMount() Написано как функция Hook

Чтобы преобразовать его в поведение componentDidMount(), нам нужно добавить пустые скобки [] в качестве аргумента к крючку, выделенного жирным шрифтом, как показано: useEffect(() => { ... }, []). Вы спросите, а зачем добавлять []? Причина в том, что передача пустого списка в useEffect() говорит джинну обработчика жизненного цикла запускать этот хук до тех пор, пока заданные значения состояния в [] не меняются. Поскольку мы не добавляем никаких значений состояния в список в [], таких как [someFunState], useEffect(() => { ... }, []) запустится только один раз, потому что нет значений состояния, за которыми следует эта реализация, которые могли бы повторно запустить ее.

componentDidUpdate()с хуком условной записи как функции

Если вы когда-либо писали componentDidUpdate(), скорее всего, вы написали по крайней мере один с каким-то условием состояния, чтобы вы случайно не запускали части кода в своей реализации. useEffect() устраняет эту проблему, потому что вы можете отделить свой условный код, когда запускается обновление, просто используя несколько перехватчиков useEffect()! Предположим, у нас есть два значения состояния: stateA и stateB. С компонентом на основе классов, чтобы гарантировать выполнение кода при обновлении только одного из состояний, мы должны написать что-то вроде:

componentDidUpdate(previousState) {
  if (previousState.stateA !== this.state.stateA) {
    // ... conditional code when stateA updates
  }
  if (previousState.stateB !== this.state.stateB) {
    // ... conditional code when stateB updates
  }
  // ... other code that runs when either state updates
}

Вместо этого мы могли бы сделать следующее:

// code that runs whenever there is any state update
useEffect(() => { ... })
useEffect(() => { // ... code run when stateA updates }, [stateA])
useEffect(() => { // ... code run when stateB updates }, [stateB])

Здесь у нас есть универсальный useEffect(), который будет запускать некоторый код, очень похожий на componentDidUpdate(), без каких-либо условий. Затем во втором и третьем хуках useEffect() каждый из них запускается тогда и только тогда, когда изменяется соответствующее значение состояния, переданное в параметре списка (например, [stateA]). Это означает, что вместо того, чтобы componentDidUpdate() перебирать все возможности, мы можем иметь хук useEffect(), посвященный определенной переменной состояния, когда она была обновлена. Это не только делает наш компонент более модульным с внутренней работой обновлений изменения состояния, но и предлагает оптимизацию производительности, потому что вы можете пропустить useEffect(), когда данная переменная состояния не изменяется.

componentWillUnmount() Написано как функциональный хук

В отличие от других методов жизненного цикла в компоненте на основе классов, настройка componentWillUnmount() в случае функционального компонента не так проста. Вместо этого вы должны вернуть какую-то функцию очистки как часть соответствующего метода useEffect(). Например, если ваш код очистки должен быть запущен после того, как ваш componentDidMount() сделал определенные вызовы службы или подписки на API, тогда ваш хук useEffect() будет иметь двойную цель: выполнить реализацию componentDidMount() и реализацию componentWillUnmount().

useEffect(() => {
  // ... code you want ran during a componentDidMount()
  
  return () => {
    // ... code that will be run once the component unmounts
    //     from the DOM tree 
  }
}, [])

Выше мы пишем код, который мы обычно делаем, и добавляем [], чтобы показать, что этот хук useEffect() будет запущен только один раз. Кроме того, этот хук возвращает функцию. Возвращенная функция действует как наша componentWillUnmount() и будет выполняться, как только компонент будет размонтирован из дерева DOM.

Использование компонента

Одна из прелестей использования функциональных компонентов вместо компонентов на основе классов заключается в том, что то, как вы объявляете их в других компонентах, абсолютно не меняется! В приведенном ниже примере демонстрируется взаимодействие основанных на классах и функциональных компонентов:

import { ClassComponentExample } from './ClassComponentExample'
import { FunctionComponentExample } from './FunctionComponentExample'
import { Component } from 'react'
function App() {
  const onClick = () => {}
  const componentProps = {
    id: "componentId",
    onClick: onClick,
    text: "This is a component, but what kind?"
  }
  return(
    <ClassComponentExample {...componentProps}/>
    <FunctionComponentExample {...componentProps}/>
  )
} 

Использование реквизита

В отличие от методов жизненного цикла, доступ к свойствам, которые передаются в ваших компонентах, имеет только одно ключевое отличие. Для компонентов на основе классов вам может быть знакомо ключевое слово this, прежде чем вы получите доступ ко многим переменным и функциям. С функциональными компонентами мы опускаем this (каламбур)! В следующем примере мы показываем наш «Пример использования компонента» и то, как каждый из этих примеров реализован и использует props:

Чтобы использовать props в ClassComponentExample, мы набираем this.props.propName. В FunctionComponentExample мы просто вводим props.propName без ключевого слова this. Например:

// class component
// ... Notice bold text for this keyword
return(
  <div>
    <div> {this.props.text} Well, it has {this.props.id}</div>
    <button onClick={this.props.onClick}>Click me!</button>
  </div>
)
//function component
// ... Notice how this is not used at all
return(
  <div>
    <div> {props.text} Well, it has {props.id}</div>
    <button onClick={props.onClick}>Click me!</button>
  </div>
)

Объявление и использование состояний

Как и в случае с нашим товарищем props, мы следуем аналогичному соглашению о доступе. В компонентах на основе классов мы вводим this.state.stateVariableName для доступа к нему. Вместо этого с функциональными компонентами мы напрямую обращаемся к переменной состояния, такой как stateVariableName. Но как? Это связано с тем, что декларация штатов существенно отличается. В следующем примере показано, как реализовать одинаковые состояния между классовым и функциональным компонентом.

В ClassComponentExample мы устанавливаем состояние в конструкторе с некоторыми начальными значениями, используя:

this.state = {                                    
  id: "componentId",                                   
  text: "This is a component, but what kind?"                                 }

Вместо этого в FunctionComponentExample мы создаем наши состояния, используя кортеж в формате const [<stateVariableName>, <setStateVariableName>] = useState(<initial value>) . Переменная, объявленная в первом месте, является переменной состояния. Это переменная только для чтения, которую мы не можем изменить. Во втором месте мы объявляем имя дескриптора для изменения значения состояния. В довершение всего, мы сообщаем React, что const [<stateVariableName>, <setStateVariableName>] — это кортеж состояния, вызывая хук функции useState(), которому затем присваивается начальное значение переменной состояния. Таким образом, чтобы преобразовать пример на основе классов в функциональный компонент, мы должны переписать наше объявление состояния следующим образом:

const [text, setText] = useState("This is a component, but what kind?")                               
const [id, setId] = useState("componentId")

Наконец, чтобы изменить и обновить значения состояния, в следующих примерах показано, как это делается:

// class-based component
this.setState({
  text: "new text value"
})
//functional component
setText("new text value")

Каждый пример работает точно так же в соответствующем контексте. В компоненте на основе классов мы обычно вызываем this.setState({}) и сообщаем React, какие состояния мы хотим обновить. Вместо этого в наших функциональных компонентах мы вызываем каждый отдельный дескриптор состояния, который хотим обновить, в данном случае это setText().

Краткое содержание

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