Статьи

React Hooks: как начать и построить свой собственный

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

Это означает, что вам больше не нужно определять класс при создании компонента React. Оказывается, что классовая архитектура, используемая в React, является причиной многих проблем, с которыми ежедневно сталкиваются разработчики React. Мы часто пишем большие сложные компоненты, которые трудно разбить. Связанный код распределен по нескольким методам жизненного цикла, что затрудняет чтение, обслуживание и тестирование. Кроме того, мы имеем дело с ключевым словом this при доступе к состоянию, реквизиту и функциям. Мы также должны привязать функции к this чтобы гарантировать, что они доступны внутри компонента. Тогда мы имеем дело с проблемой чрезмерного бурения, также известной как адский обертка, когда имеем дело с компонентами более высокого порядка.

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

React Hooks был впервые анонсирован на конференции React, которая состоялась в октябре 2018 года. Он был официально представлен в React 16.8 в прошлом месяце. Эта функция все еще находится в стадии разработки — все еще есть ряд функций класса React, перенесенных в ловушки. Хорошей новостью является то, что вы можете начать использовать их сейчас. Вы все еще можете использовать компоненты класса React, если хотите, — но я сомневаюсь, что вы будете использовать после того, как закончите это вводное руководство.

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

Предпосылки

Эта статья для опытных разработчиков React. Если вы новичок, сначала пройдите следующие уроки:

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

useState Hook

Рассмотрим следующий компонент класса React:

 import React from "react"; export default class ClassDemo extends React.Component { constructor(props) { super(props); this.state = { name: "Agata" }; this.handleNameChange = this.handleNameChange.bind(this); } handleNameChange(e) { this.setState({ name: e.target.value }); } render() { return ( <section> <form autocomplete="off"> <section> <label htmlFor="name">Name</label> <input type="text" name="name" id="name" value={this.state.name} onChange={this.handleNameChange} /> </section> </form> <p>Hello {this.state.name}</p> </section> ); } } 

Вот как это выглядит:

Название класса React Hooks

При обновлении поля имени сообщение «Hello *» также должно обновиться. Дайте себе минуту, чтобы понять код. Далее мы собираемся написать новую версию этого кода, используя React Hook, известный как useState .

Его синтаксис выглядит так:

 const [state, setState] = useState(initialState); 

Когда вы вызываете функцию useState , она возвращает два элемента:

  • state — название вашего штата, например this.state.name или this.state.location
  • setState — функция для установки нового значения для вашего состояния. Похоже на this.setState({name:newValue})

initialState — это значение по умолчанию, которое вы присваиваете недавно объявленному состоянию на этапе объявления состояния. Теперь, когда у вас есть представление о том, что такое useState , давайте приведем его в действие.

 import React, { useState } from "react"; export default function HookDemo(props) { const [name, setName] = useState("Agata"); function handleNameChange(e) { setName(e.target.value); } return ( <section> <form autocomplete="off"> <section> <label htmlFor="name">Name</label> <input type="text" name="name" id="name" value={name} onChange={handleNameChange} /> </section> </form> <p>Hello {name}</p> </section> ); } 

Обратите внимание на различия между этой версией и версией класса. Это уже намного компактнее и проще для понимания, чем классная версия, но они оба делают одно и то же. Давайте рассмотрим различия:

  • Весь конструктор класса был заменен useState , который состоит только из одной строки.
  • Поскольку useState Hook выводит локальные переменные, вам больше не нужно использовать ключевое слово this для ссылки на вашу функцию или переменные состояния. Честно говоря, это большая боль для большинства разработчиков JavaScript, так как не всегда понятно, когда вы должны this использовать.
  • Код JSX теперь более чистый, так как вы можете ссылаться на значения локального состояния без использования this.state .

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

Удостоверьтесь, чтобы сделать это наверху, и никогда внутри условия Вот пример компонента с несколькими useState :

 import React, { useState } from "react"; export default function HookDemo(props) { const [name, setName] = useState("Agata"); const [location, setLocation] = useState("Nairobi"); function handleNameChange(e) { setName(e.target.value); } function handleLocationChange(e) { setLocation(e.target.value); } return ( <section> <form autocomplete="off"> <section> <label htmlFor="name">Name</label> <input type="text" name="name" id="name" value={name} onChange={handleNameChange} /> </section> <section> <label htmlFor="location">Location</label> <input type="text" name="location" id="location" value={location} onChange={handleLocationChange} /> </section> </form> <p> Hello {name} from {location} </p> </section> ); } 

Довольно просто, не правда ли? Чтобы сделать то же самое в версии Class, вам потребуется использовать еще больше this ключевых слов. Давайте перейдем к следующему основному React Hook.

useEffect Hook

Большинство компонентов React требуется для выполнения определенной операции, такой как выборка данных, подписка или изменение DOM вручную. Такие операции известны как побочные эффекты.

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

Вот простой пример:

 componentDidMount() { document.title = this.state.name + " from " + this.state.location; } 

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

 componentDidUpdate() { document.title = this.state.name + " from " + this.state.location; } 

Обновление формы теперь также должно обновить заголовок документа.

Название класса React Hooks

Давайте посмотрим, как мы можем реализовать ту же логику, используя useEffect Hook:

 import React, { useState, useEffect } from "react"; //... useEffect(() => { document.title = name + " from " + location; }); 

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

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

 //... constructor(props) { super(props); this.state = { name: "Agata", location: "Nairobi", resolution: { width: window.innerWidth, height: window.innerHeight } }; //... this.handleResize = this.handleResize.bind(this); } componentDidMount() { document.title = this.state.name + " from " + this.state.location; window.addEventListener("resize", this.handleResize); } componentDidUpdate() { document.title = this.state.name + " from " + this.state.location; window.addEventListener("resize", this.handleResize); } //... handleResize() { this.setState({ resolution: { width: window.innerWidth, height: window.innerHeight } }); } render() { return ( <section> ... <h3> {this.state.resolution.width} x {this.state.resolution.height} </h3> </section> ) } 

Добавление вышеуказанного кода отобразит текущее разрешение окна вашего браузера. Измените размер окна, и вы увидите, что номера обновляются автоматически. Если вы нажмете F11 в Chrome, он должен отобразить полное разрешение вашего монитора.

React Hooks Class Resolution

Давайте повторим приведенный выше код «класса» в нашу версию «крючка». Нам нужно определить третью useState и вторую функцию useEffect для обработки этой новой функции:

 //... state declaration const [resolution, setResolution] = useState({ width: window.innerWidth, height: window.innerHeight }); //... useEffect(() => { const handleResize = () => { setResolution({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize ", handleResize); }; }); //... jsx render <h3> {resolution.width} x {resolution.height} </h3>; 

Удивительно, но эта версия кода ловушки делает то же самое. Это чище и компактнее. Преимущество помещения кода в собственное объявление useEffect состоит в том, что мы можем легко его протестировать, поскольку код изолирован.

Вы заметили, что мы возвращаем функцию в этом useEffect ? Это потому, что любая функция, которую вы возвращаете внутри функции useEffect будет считаться кодом для очистки. Если вы не вернете функцию, очистка не будет выполнена. В этом случае потребуется очистка, в противном случае вы столкнетесь с ошибкой при запуске кода.

Таможенные Реактивные Крючки

Теперь, когда вы узнали о useState и useEffect , позвольте мне показать вам действительно крутой способ сделать ваш код еще более компактным, useState и useEffect повторного использования, чем те, которые мы достигли до сих пор. Мы собираемся создать собственный хук, чтобы еще больше упростить наш код.

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

 function useWindowResolution() { const [resolution, setResolution] = useState({ width: window.innerWidth, height: window.innerHeight }); useEffect(() => { const handleResize = () => { setResolution({ width: window.innerWidth, height: window.innerHeight }); }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize ", handleResize); console.log("cleanup"); }; }); return resolution; } 

Далее вам нужно заменить этот код:

 const [resolution, setResolution] = useState({ width: window.innerWidth, height: window.innerHeight }); 

… с этим:

 const resolution = useWindowResolution(); 

Удалите второй код useEffect . Сохраните ваш файл и протестируйте его. Изменение размера окна браузера должно эффективно обновить цифры разрешения. Если вы испытываете некоторую задержку, замените функцию useWindowResolution на useWindowResolution :

 function useWindowResolution() { const [width, setWidth] = useState(window.innerWidth); const [height, setHeight] = useState(window.innerHeight); useEffect(() => { const handleResize = () => { setWidth(window.innerWidth); setHeight(window.innerHeight); }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize ", handleResize); }; }, [width, height]); return { width, height }; } 

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

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

 //... const resolution = useWindowResolution(); useDocumentTitle(name + " from " + location); //... function useDocumentTitle(title) { useEffect(() => { document.title = title; }); } 

Название документа должно измениться, как и раньше. Теперь давайте проведем рефакторинг полей формы. Реорганизованный компонент будет выглядеть так:

 export default function HookDemo(props) { const name = useFormInput("Agata"); const location = useFormInput("Nairobi"); const resolution = useWindowResolution(); useDocumentTitle(name.value + " from " + location.value); return ( <section> <form autoComplete="off"> <section> <label htmlFor="name">Name</label> <input {...name} /> </section> <section> <label htmlFor="location">Location</label> <input {...location} /> </section> </form> <p> Hello {name.value} from {location.value} </p> <h3> {resolution.width} x {resolution.height} </h3> </section> ); } function useFormInput(initialValue) { const [value, setValue] = useState(initialValue); function handleChange(e) { setValue(e.target.value); } return { value, onChange: handleChange }; } 

Пройдите код медленно и определите все изменения, которые мы сделали. Довольно аккуратно, правда? Наш компонент намного компактнее. Мы можем упаковать useFormInput , useDocumentTitle и useWindowResolution во внешний модуль npm, поскольку они полностью независимы от основной логики. Мы можем легко использовать эти пользовательские хуки в других частях проекта или даже в других проектах в будущем.

Для справки, вот полная версия компонента ловушек:

 import React, { useState, useEffect } from "react"; export default function HookDemo(props) { const name = useFormInput("Agata"); const location = useFormInput("Nairobi"); const resolution = useWindowResolution(); useDocumentTitle(name.value + " from " + location.value); return ( <section> <form autoComplete="off"> <section> <label htmlFor="name">Name</label> <input {...name} /> </section> <section> <label htmlFor="location">Location</label> <input {...location} /> </section> </form> <p> Hello {name.value} from {location.value} </p> <h3> {resolution.width} x {resolution.height} </h3> </section> ); } function useDocumentTitle(title) { useEffect(() => { document.title = title; }); } function useFormInput(initialValue) { const [value, setValue] = useState(initialValue); function handleChange(e) { setValue(e.target.value); } return { value, onChange: handleChange }; } function useWindowResolution() { const [width, setWidth] = useState(window.innerWidth); const [height, setHeight] = useState(window.innerHeight); useEffect(() => { const handleResize = () => { setWidth(window.innerWidth); setHeight(window.innerHeight); }; window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize ", handleResize, true); }; }, [width, height]); return { width, height }; } 

Компонент хука должен отображаться и вести себя точно так же, как версия компонента класса:

React Hooks Final

Если вы сравните его с версией компонента класса, вы поймете, что функция ловушки уменьшает код вашего компонента как минимум на 30%. Вы даже можете уменьшить свой код, экспортируя повторно используемые функции в библиотеку npm.

Это конец нашего вводного руководства, но есть много о крючках, которые я не рассмотрел. Я дам вам несколько советов для вашего собственного исследования в следующем разделе.

Официальные Реактивные Крючки

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

  • useState — для управления локальным состоянием
  • useEffects — заменяет функции жизненного цикла
  • useContext — позволяет вам легко работать с React Context API (решает проблему бурения реквизита)

У нас также есть дополнительные официальные React Hooks, которые вы можете использовать в зависимости от требований вашего проекта:

  • useReducer — расширенная версия useState для управления сложной логикой состояния. Это очень похоже на Redux
  • useCallback — возвращает функцию, которая возвращает кэшируемое значение. Полезно для оптимизации производительности, если вы хотите предотвратить ненужные повторные рендеры, когда входные данные не изменились.
  • useMemo — возвращает значение из memoized функции. Аналогичен computed если вы знакомы с Vue.
  • useRef — возвращает изменяемый объект ref, который сохраняется в течение всего времени жизни компонента.
  • useImperativeHandle — настраивает значение экземпляра, которое предоставляется родительским компонентам при использовании ref .
  • useLayoutEffect — аналогичен useEffect , но срабатывает синхронно после всех мутаций DOM.
  • useDebugValue — отображает метку для пользовательских хуков в React DevTools.

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

Резюме

Сообщество React позитивно отреагировало на новую функцию React Hooks. Уже есть репозиторий с открытым исходным кодом, называемый awesome-реагировать-крючки . Сотни пользовательских React Hooks были отправлены в этот репозиторий. Вот быстрый пример одного из этих хуков для хранения значений в локальном хранилище:

 import useLocalStorage from "@rehooks/local-storage"; function MyComponent() { let name = useLocalStorage("name"); // send the key to be tracked. return ( <div> <h1>{name}</h1> </div> ); } 

Вам нужно будет установить ловушку local-storage с помощью npm или пряжи, как показано ниже:

 yarn add @rehooks/local-storage 

Довольно аккуратно, правда? Введение React Hooks произвело большой резонанс. Его волны вышли за пределы сообщества React в мир JavaScript. Это потому, что хуки — это новая концепция, которая может принести пользу всей экосистеме JavaScript. Фактически, команда Vue.js уже начала внедрять свою собственную версию.

Также говорится о React Hooks и Context API, свергнувших Redux с трона управления состоянием. Ясно, что перехватчики сделали кодирование намного проще и изменило способ написания нового кода. Если вы похожи на меня, вам, вероятно, настоятельно рекомендуется переписать все классы компонентов React и заменить их функциональными хуками компонентов.

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

Если вы достаточно уверены в своих новых знаниях основ React Hooks, я бы хотел поставить перед вами задачу. Рефакторинг этого класса таймера обратного отсчета с использованием перехватчиков React, чтобы сделать его максимально чистым и компактным. Удачного кодирования!