Как скоро хуки вытеснят классы React?

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

“Мы стремимся к тому, чтобы хуки смогли охватить все существующие случаи применения классов, но при этом продолжим поддерживать компоненты классов на протяжении обозримого будущего. В Facebook у нас есть десятки тысяч компонентов, написанных в классах, и мы не планируем их переписывать. Наоборот, мы начинаем использовать хуки в новом коде наравне с классами”,— документация React.

Вместе с тем отмечу, что React существенно развился с момента выхода его первой публичной версии, равно как и язык, на котором он был написан. Поэтому сам факт имеет место быть, и они однозначно будут адаптироваться к последним трендам, о чём я и собираюсь рассказать в этой статье. На вопрос же будет ли моё итоговое заключение верным, уверенно сможет ответить лишь время.

Краткий обзор эволюции React

Как вам, возможно, известно, хуки были добавлены в React только недавно (с версии 16.8), но совпало ли это с добавлением поддержки функциональных компонентов? Не совсем. 

Поскольку React был разработан до выпуска ES6, для определения компонентов, хотя они уже рассматривались как классы, нужно было использовать вспомогательный метод React.createClass.

Этот метод получал литеральный объект со свойствами для методов и возвращал классовый компонент. Выглядело это так:

var CommentBox = React.createClass({
  getInitialState: function() {
    $.ajax({
      url: 'comments.json',
      success: function(data) {
        this.setState({data: data});
      }.bind(this)
    });
    return {data: []};
  },
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList data={this.state.data} />
        <CommentForm />
      </div>
    );
  }
});

Этот пример взят из старой документации React, поэтому в нём вы можете видеть такие элементы, как jQuery и старое доброе ключевое слово var. В то время таким способом создавались компоненты, и при необходимости можно было использовать свойство state для обработки компонентов с состояниями так же, как сейчас это делается с более современными классовыми компонентами.

Дальнейшая эволюция привнесла функциональные компоненты. И хоть состояний они не имели, но отлично подходили для более простых компонентов. Поскольку предназначались они для использования в качестве функций, а не странной смеси из объектов и функций (возможно, благодаря модели данных JavaScript), конструкция this была выведена из игры и, как следствие, состояние также было исключено. Компоненты классов, нуждавшиеся только в методе render, могли быть с лёгкостью переведены в функцию, возвращающую код этого метода. 

Ниже приведён пример функционального компонента:

function Square(props) {
  return (
    <button className="square" onClick={() => props.onClick()}>
      {props.value}
    </button>
  );
}

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

Как раз в это время ES6 уже набирала обороты, и метод React.createClass был заменён теперь уже стандартным синтаксисом Class.

В итоге в версии 16.8 команда React ввела хуки, не только ставя функциональные компоненты вровень с классовыми, но также делая их более лёгкими в написании и даже потенциально превосходящими своих старших собратьев.

На данный момент, будучи React-разработчиком, вы можете выбирать между классовыми и функциональными компонентами (с хуками, конечно же). Но какие из них лучше? И стоит ли делать ставку на классовые, учитывая количество вложенных командой React усилий в проектирование хуков для функциональных?

Плюсы и минусы 

Несмотря на то, что добавление хуков сделало функциональные компоненты не менее способными, чем классовые, между ними всё-таки остаются некоторые различия.

Например, с точки зрения многословности, классы требуют писать больше, просто из-за необходимого в их синтаксисе рутинного кода. 

И эта проблема касается не только крупных баз кода. Пишут и поддерживают их люди, а многословный код гораздо сложнее прослеживать и понимать, нежели краткий (это не всегда именно так, но всё же остаётся одним из главных правил).

Ниже приведён очень простой пример, где вы можете видеть примитивный компонент, определённый при помощи синтаксиса классов:

class Follower extends React.Component {
  constructor(props) {
    super(props);
    this.showMessage = this.showMessage.bind(this);    
    this.handleClick = this.handleClick.bind(this);  
  }

  showMessage() {
    alert('Followed ' + this.props.user);
  }

  handleClick() {
    setTimeout(this.showMessage, 3000);
  }

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

Теперь давайте взглянем на тот же пример, но использующий функциональный подход:

function Follower(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

Здесь нам удалось удалить всего 6 строк, но это уже почти 50% кода первого компонента. Представьте, сколько вы можете сэкономить в более крупных и сложных компонентах.

Публикация и повторное использование компонентов

Поскольку различные библиотеки и фреймворки делают возможным применение React практически для всех целей (iOS, Android, десктопные приложения, статические страницы, SSR приложения и т.д.), мы переиспользуем компоненты React гораздо чаще, чем делали это ранее. Этот подход решает две задачи: ускоряет разработку (посредством повышения переиспользования кода) и позволяет сохранять согласованный UI между разными проектами.

Написание ясных и легковесных компонентов становится ещё более важным. Вам нужно, чтобы ваша команда или даже вы сам в будущем могли понимать код настолько быстро, насколько это возможно, что упростит его дальнейшее переиспользование и поддержку, в том числе другими людьми.

Если вам доводилось использовать концентратор облачных компонентов, вроде Bit.dev, то вы знаете, что хорошо структурированные компоненты имеют решающее значение в мире.

Когда вы используете Bit.dev для публикации переиспользуемых компонентов, вы добавляете их из базы кода вашего приложения параллельно с его созданием. Вы не пишете весь “проект библиотеки компонентов” с нуля. Вам нужно, чтобы база кода была максимально ясна и поделена на модули, чтобы компоненты можно было легко опубликовать для использования и обслуживания другими. 

Пример: поиск компонентов React на Bit.dev.

Разница в наследовании

Между функциональными и классовыми компонентами есть ещё одно, хоть и менее заметное, но более существенное отличие. И вместо того, чтобы рассказывать о нём, я продемонстрирую пример:

В случае, если качество GIF не позволяет разобрать детали, поясняю, что именно происходит с каждым компонентом:

  1. Я кликаю кнопку “Alert”, которая через 3 секунды должна вызвать окно оповещения. Текст оповещения будет зависеть от значения, выбранного в выпадающем меню.
  2. До истечения 3-х секунд я меняю выбранное значение в выпадающем меню. 
  3. Конечный результат в окне оповещения совпадает с ожидаемым от функционального компонента, а классовый компонент переписывает значение.

Давайте взглянем на код, чтобы лучше понять происходящее. Вот функциональный компонент:

import React from "react";
import { useState } from "react";

export default function App() {
  const [message, setMessage] = useState("Default message");

  const alertMessage = () => {
    setTimeout(_ => {
      alert(message);
    }, 3000);
  };

  const handleSwitch = e => {
    setMessage(e.target.value);
  };

  return (
    <div className="App">
      <h1>Sample functional component</h1>
      <select onChange={handleSwitch}>
        <option value="hello world!">Hello</option>
        <option value="good bye">Bye</option>
      </select>
      <br />
      <input type="button" value="Alert" onClick={alertMessage} />
    </div>
  );
}

А вот классовый:

import React from "react";

export default class ClassApp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      message: "Default message"
    };
    this.handleSwitch = this.handleSwitch.bind(this);
    this.alertMessage = this.alertMessage.bind(this);
  }

  alertMessage() {
    setTimeout(_ => {
      alert(this.state.message);
    }, 3000);
  }

  handleSwitch(e) {
    this.setState({
      message: e.target.value
    });
  }

  render() {
    return (
      <div className="App">
        <h1>Sample Class component</h1>
        <select onChange={this.handleSwitch}>
          <option value="hello world!">Hello</option>
          <option value="good bye">Bye</option>
        </select>
        <br />
        <input type="button" value="Alert" onClick={this.alertMessage} />
      </div>
    );
  }
}

Они отображают один и тот же HTML и делают одно и то же — при изменении события для выпадающего меню они устанавливают сообщение в состоянии компонента, а при нажатии кнопки “Alert” включается отсчёт таймаута. Тогда почему же их поведение отличается?

Отгадка кроется в функциональном компоненте. Сможете догадаться в чём она заключается?

Осторожно, спойлер!

Это замыкания! Всё верно, из-за природы замыканий при каждом повторном отображении компонента (когда мы обновляем состояние) создаётся новая функция, но, когда срабатывает alert, предыдущие замыкания не уничтожаются, поэтому компонент выводит правильное сообщение. Но поскольку у нас нет такого же замыкания для классового компонента, то, когда мы меняем состояние и происходит повторное отображение, мы вынужденно имеем дело с последним состоянием. Вы можете самостоятельно поиграться с этим примером в песочнице.

Иначе говоря, функциональные компоненты захватывают отображаемое значение.

С хуками обмен логикой проще

Вы когда-нибудь пробовали извлечь общее поведение из разных компонентов, чтобы инкапсулировать его и передать другим компонентам? В случае с классовыми компонентами есть способы этого добиться. Но эти способы не что иное, как обходные пути ограничений, присутствующих в подходе, использующем классы.

Слышали ли вы о компонентах высшего порядка (HOC)? Это компоненты, создаваемые только для обёртывания других компонентов и расширения их логики дополнительным кодом. Другими словами, вам нужно инкапсулировать обобщённую логику в компонент, просто чтобы гарантировать, что, когда вы обернёте в неё другой компонент, эта логика будет добавлена целевому компоненту. Как вам такая тирада? Представьте необходимость писать для этого код или читать базу кода, где такой шаблон применён. 

А как насчёт render props? Это уже другой шаблон, который состоит из передачи инкапсулированной логики в компоненты совместно с их свойствами (props). Т.е. для добавления дополнительного поведения вы используете свойства. В итоге получается подобный код:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

Лично я думаю, что это лучше, чем использовать компоненты для обёртывания компонентов, хотя здесь всё же присутствует нечёткость, и для передачи логики используются свойства, что может быть несколько опасно, если вы не контролируете код полностью.

Но теперь вы можете инкапсулировать совместно используемую логику с помощью хуков, например так:

//Ваш пользовательский хук, инкапсулирующий логику, необходимую для отслеживания движений мыши
function useMyCustomLogic() {
  const [mousePosition, setMousePosition] = useState({x: 0, y:0})
  
  function handleMouseMove(event){
    setMousePosition( {
      x: event.clientX,
      y: event.clientY
    })
  }
  
  useEffect( () => {
    window.addEventListener("mousemove", handleMouseMove)
    
    return _ => {
      window.removeEventListener("mousemove", handleMouseMove)
    }  
  }, [])
  return mousePosition
}

//Главный компонент App 
function App() {
  const {x,y} = useMyCustomLogic()
  
  return (<p>Mouse coordinates: x = {x}, y = {y}</p> )
}

Итак, что мы здесь имеем? При помощи простого пользовательского хука мы правильно инкапсулировали логику и можем чётко использовать её внутри любых наших компонентов. Так выглядит чистый подход к совместному использованию логики.

С помощью хуков проще добавлять логику эффектов

При работе с классами во многих случаях нам приходится распространять логику эффекта по множеству методов жизненного цикла. Это даёт нужный результат, но вот выглядит не очень элегантно. 

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

Чтобы посмотреть пример, просто вернитесь к коду выше, установите в начале слушателя событий для события mousemove и удалите его, когда он станет не нужен.

Безопасно ли использовать классовые компоненты в 2020?

Я бы сказал, что на время нашей жизни вполне ещё безопасно, т.к. React нацелен на предоставление своим пользователям наилучшей практики разработки. Это значит, что классы являются достаточно новыми для экосистемы JS и в ближайшем будущем никуда не денутся, равно как и их поддержка командой React.

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

В моём представлении более новые версии React могут начать добавлять в функциональные компоненты возможности несовместимые с классовыми, оставляя последние в невыгодном положении, как это было с первыми до версии 16.8. 

Читайте также:


Перевод статьи Fernando Doglio: Will React Classes Get Deprecated Because of Hooks?