React Hooks — огляд можливостей нового API

Мене звати Володимир Симоненко. Я фронтенд-розробник у компанії PyTeam. Займаюся веб-розробкою 3 роки. Загальний досвід у розробці програмного забезпечення на різних позиціях — 8 років. Нещодавно я робив доповідь на тему React Hooks і вирішив поділитися інформацією на DOU. Це стаття-огляд нових можливостей відомої та популярної бібліотеки для веб-інтерфейсів React.js і буде більш цікава тим, хто вже знайомий с реактом.

Hooks — нове API, що дозволяє писати функціональні компоненти зі станом та використовувати інші можливості реакту без написання класів. Доступно в прев’ю версії (поточна версія 16.8.0-alpha.1). Встановлюємо: npm i –S react@next.

Навіщо це потрібно

По-перше, це потрібно, щоб повторно використовувати логіку, що знаходиться в state, між компонентами. Для вирішення цієї задачі зазвичай використовують такі підходи: Higher-Order Components (HOC) і Render Props.

По-друге, хуки дозволяють розділити один компонент на більш дрібні функції в залежності від того, які частини пов’язані (наприклад, налаштування підписки або вибірка даних), замість примусового поділу на основі методів життєвого циклу.

По-третє, хуки дозволяють використовувати більше можливостей React без класів. Класи складні для людей і для машин. У спостереженні facebook класи є великою перешкодою при вивченні React. Необхідно зрозуміти, як працює this, а він не працює так, як в інших мовах програмування. Так само слід пам’ятати про прив’язку обробників подій. Без стабільних пропозицій синтаксису код виглядає дуже багатослівно. Також класи не дуже добре мінімізуються, і вони роблять гаряче перезавантаження (hot reload) ненадійним.

Hooks API

Базові:

Додаткові:

Напишемо кілька рядків

На основі хуків реакту напишемо власний (custom hook), який буде реагувати на зміну ширини області перегляду вікна браузера і буде повертати значення екрану. Такий хук буде корисним для адаптивного дизайну (responsive design).

Важливо! Угода про іменування призначених для користувача хуків: Сustom Hook — це функція JavaScript, ім’я якої починається з «use» і яка може викликати інші хуки.

Отже, створюємо функціональний компонент з локальним станом:

import { useState } from 'react'
 
export default function useBreakpoints() {
  const [points, setPoints] = useState(window.innerWidth)
 
  return points
}

Функція useState повертає масив, у якому під індексом 0 знаходиться зміна, що буде зберігати state та під індексом 1 повертає функцію, що буде змінювати state. Зверніть увагу, що points буде мати ініційоване значення, яке ми передали в хук useState, а саме window.innerWidth. Якщо порівняти ці рядки з компонентом на основі класу, то ми маємо аналог ініціації змінної points в методі constructor, setPoints — це аналог setState.

Створюємо обробник, який буде записувати актуальну ширину екрану:

…
const handleResize = () => setPoints(window.innerWidth)
…

Підписуємо обробник handleResize на подію зміни документу:

import { useState, useEffect } from 'react'
 
export default function useBreakpoints() {
…
  useEffect(() => {
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  })
…
}

Хук useEffect буде викликатись декілька разів протягом життєвого циклу компонента — один раз після монтування (одразу як компонент буде додано в дерево) та кожного разу, коли є необхідність оновлення. Якщо знову провести аналогію з компонентом на основі класу, то ми маємо componentDidMount, componentDidUpdate, componentWillUnmount в одному місці. Зверніть увагу: щоб уникнути memory leak, необхідно відписатись, коли компонент буде відмонтовано і знищено. Для цього необхідно повернути функцію «відписки» в контексті анонімної функції, що була передана в хук useEffect, як це наведено в прикладі. Готово!

Порада

Щоб викликати функцію лише один раз після монтування, скористуйтеся таким трюком. Передайте другим параметром пустий масив:

…
useEffect(() => handleResize(), [])
…

Другим аргументом хук приймає масив об’єктів, що необхідно відстежувани. Такий механізм потрібен для контрольованих викликів. Тобто побічні ефекти в контексті хука будуть виконанні тільки при зміні значень, переданих в масив об’єктів.

Важливі правила

  1. Викликайте хуки тільки в середині функціональних компонентів.
  2. Викликайте хуки тільки на верхньому рівні функції, тобто на початку блоку. Не викликайте хуки в межах loops, conditions чи nested functions.

Для розробки є плагін eslint-plugin-react-hooks для лінтера ESLint, що буде відстежувати виконання цих правил.

Як щодо тестування

З точки зору React, компонент, який використовує Hooks, — це звичайний компонент. Для тестування кастомних хуків можна використати утиліти react-testing-library. Необхідно лише створити компонент і викликати в ньому хук. Далі можна симулювати подію зміни document view та перевірити «вихлоп» функції (даруйте за жаргон, не стримався ;) ).

import React from 'react'
import { render } from 'react-testing-library'
import useBreakpoints from './useBreakpoints'
 
const sizeSmall = 320
 
void function fireResize(width = sizeSmall) {
    window.innerWidth = width
    window.dispatchEvent(new Event('resize'))
}()
 
function EffecfulComponent() {
    const breakpoint = useBreakpoints()
    return <span>{breakpoint}</span>
}
 
test('useBreakpoints listen to window resize', () => {
    const { container } = render(<EffecfulComponent />)
    const span = container.firstChild
    expect(span.textContent).toBe(sizeSmall.toString())
})

Переваги

  1. Функціональні компоненти на основі хуків більш наочні за класи та мають майже всі можливості реакт компоненту.
  2. Хуки дозволяють розділити один компонент на більш дрібні функції в залежності від того, які частини пов’язані.
  3. Можна повторно використовувати логіку в компонентах та ділитись цілими колекціями хуків, наприклад через npm.js

Недоліки

  1. Немає можливості викликати хуки в компонентах на основі класу (див. правила). Тому неможливо без додаткового пласту додати хуки в поточний проект, що робить нераціональним (дорожчим) їх використання. Тому вважаю, що доцільно використовувати хуки тільки в нових проектах.

Це переваги та недоліки, виявлені на перший погляд. Вважаю, що цей список дещо ширший, але в цілому переваг, як мені вбачається, більше:)

P. S. Хоч я є «адептом» Vue.js, я дуже привітно ставлюся до такого кроку зі сторони реакт-спільноти — не просто тримати планку найпопулярнішої js-бібліотеки, а й робити такі зміни та ще й в кращий бік. Вважаю, що React Hooks API — це майбутнє реакту, хоча підтримка класів залишається.

В репозиторії можете подивитись код більш складного прикладу реалізації цього хуку, разом з демо та прикладом юніт-тесту.

Все про українське ІТ в телеграмі — підписуйтеся на канал DOU

👍ПодобаєтьсяСподобалось3
До обраногоВ обраному0
LinkedIn



39 коментарів

Підписатись на коментаріВідписатись від коментарів Коментарі можуть залишати тільки користувачі з підтвердженими акаунтами.

«По-перше, це потрібно, щоб повторно використовувати логіку, що знаходиться в state, між компонентами.» — Каким образом?

Навіть не знаю як вам відповісти, ви код дивились?

Да, я просто не понял что такое «логіка, що знаходиться в state»

Ок, это та логика которая мутирует состояние или возвращает его значение

А, теперь понятно. Спасибо. Просто State это не абстрактное понятие в React, а вполне конкретное и в таком свете фраза «логіка, що знаходиться в state» сбивает с толку.

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

При єтом очень просто допустить утечку, потому что нет больше методов жизненного цикла.

если вам надо методы жизненного цикла — используйте классовые компоненты.
руки никто не связывал, к батарее не приковывал.
про какую утечку речь?

на мой опыт отвечания на SO самые распространненые проблемы с хуками: бесконечный рендер(например, в useEffect происходит вызов сеттера или ж в депенедсях динамически конструируемый объект) и когда коллбек не пересоздается вовремя и указывает на неактуальный state. утечку из-за именно хуков не встретил даже гипотетически.

кстати, по ситуации на сейчас, только componentDidCatch на хуках не сделать.

Третий пункт мотивации притянут за уши, ерунда какая-то написана.
«У спостереженні facebook класи є великою перешкодою при вивченні React.» Редукс — вот самая большая сложность, а с классами всё отлично.

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

реакт можно использовать и без редакса,

да, но почему-то в подавляющем большинстве используют редукс ;)

вы можете посмотреть на скомпилиный код компонента

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

близок к классу.

тоесть будет проще чем компонент на основе класса?

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

скомпилированный код ковырять очень редко приходится

значение имеет

Насчет сложности для машин

а вот

новое АПИ придется осваивать

любишь кататься, люби и саночки возить)

а с классами всё отлично.

С классами нечего отличного, почитайте блог реакта, посмотрите доклады. Собственно благодаря хукам все проблемы с наследованием и расширением компонетов решаются, а причем тут редакс — хз.
Да он боль, но хуки и этот топик не про это :)

почитайте блог реакта, посмотрите доклады

Читал, но паническое отвращение Элиота и компании к «классам» это преувеличение

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

пытались упростить код за счет миграции на функциональные компоненты, но пока что оно запутаннее, чем lifecycle методы классов

fireResize

нигде не вызывается. Я что-то упустил из виду или этот метод не нужен ?

Согласен, немножко завуалировано выглядит)

Прийшов такий на інтерв’ю, а тебе російською запитують, что такое состояние, хук и монтирования, Орейро. Лолірую.

React, react, та в продакшин...

Вы пускали проект на хуках в продакшн?

воу-воу, да, вы, сударь, бунтарь.
еще ж не было релиза фичи.
«на свой страх и риск»

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

Это не правда. Вам нечего не мешает использовать хуки в новых компонентах, переписать старые на хуки или же например интегрировать существующий стейт в хуки, аля const { items } = useGlobalState(), дорогого в этом нечего нет :)

Вы хорошо подметили без плясок никак:) Невозможно в компоненте на основе класса вызвать хук.

Невозможно в компоненте на основе класса вызвать хук.

В этом попросту нет смысла :)

Невозможно в компоненте на основе класса вызвать хук.

зачем такая надобность? чем не подходят lifecycle methods?
даже если бы можно было бы, то хуки в render() и что-угодно-еще в lifecycle methods — та еще неконтролируемая каша была б.

Думаю что в том числе именно из-за сложности контроля и есть данное ограничение. А надобность вызова простая — повторно использовать логику.

но ведь для этого надо реюзать не сами хуки, а то, что внутри(к примеру, редьюсер, который передается внутрь useReducer), разве нет? это как раз запросто, если изначально предусмотреть такой вариант вызова.

я вас не понял или вы меня не поняли) В глобальном плане к примеру у нас есть реакт проект с набором компонентов на основе классов и есть логика которую вам нужно реализовать.Как бы хотелось просто импортировать либу (собстввенно делать то что мы обычно делаем;), котороя имеет уже реализованую логику, и даже больше, она стабильна и прошла тестирование временем. Но эта либа реализована на хуках. Дальше либо пляски либо собственная имплементация. Скорее собственная имплементация.

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

воу-воу, палехчи.

т.е. реально набор функций, которые вызывают `useEffect`, `useState` etc? и этот набор «прошел тестирование временем»? не конструктор, который можно хоть в componentDidMount, хоть в useEffect положить?

Не воспринимаете так буквально: под «прошел тестирование временем» имел ввиду работа комьюнити над либой. Вы же импортирует надежные зависимости, правильно?! ( под надежными имею ввиду те которые вызвали у вас доверие)
И да

`useEffect`, `useState` etc

реализация проверена временем по сравнению с вашей которой нет) И если перед вами завтра будет стоять задача реализовать такие же фичи, что вы выбирите: импортировать от реакта или написать свои, а что выберет бизнес?!

это гипотетическое обсуждение или подобная либа реально существует?

и да, и да)
Есть уже колеции: тыц и тыц
из них вполне достойно выглядят: тыц, тыц, тыц, тыц

это за 2 минуты гугления

Підписатись на коментарі