Знайомство з React Hooks

22 хв. читання
26 листопада 2018

Можливо ви бачили нову фічу React — Hooks. Але вас може цікавити як саме використовувати її. У статті ми покажемо декілька прикладів використання React Hooks.

Ключовий момент тут: хуки дозволяють використовувати стан та інші фічі React без написання класу.

Навіщо?

Хоча компонентний дизайн дозволяє повторно використовувати view у застосунку, залишається проблема повторного використання логіки стану між компонентами.

Для повторного використання логіки стану компонентів досі не було оптимального рішення. Зазвичай, усе закінчувалось дублюванням логіки у конструкторі й методах життєвого циклу.

Типовим розв'язанням проблеми було застосування:

  • компонентів вищого порядку (HOC);
  • render props.

Але обидва патерни мають свої недоліки й сприяють ускладненню кодової бази.

Хуки пропонують краще рішення: вони дозволяють створювати функціональні компоненти, які мають доступ до стану, контексту, методів життєвого циклу без створення компонентів класу.

Як хуки зіставляються з компонентами класів

Якщо ви знайомі з React, то найкращий спосіб зрозуміти хуки — відтворити з їх допомогою поведінку, до якої ми звикли у «компонентних класах».

Нагадаємо, що при створенні компонентних класів, нам часто необхідно:

  • Підтримувати state.
  • Використовувати методи життєвого циклу componentDidMount() та componentDidUpdate().
  • Отримувати доступ до контексту (встановивши contextType).

За допомогою React Hooks ми можемо відтворити аналогічну поведінку у функціональних компонентах:

  • Стан компонента використовує хук useState().
  • Методи життєвого циклу на зразок componentDidMount() та componentDidUpdate() використовують хук useEffect().
  • Статичний contextType використовує хук useContext().

Для використання хуків налаштовуємо: react "next"

Ви вже можете спробувати хуки, встановивши значення next для react та react-dom у файлі package.json.

// package.json
"react": "next",
"react-dom": "next"

Приклад хуку useState()

Стан — незамінна частина React. Ми можемо оголошувати змінні стану, що будуть зберігати дані у нашому застосунку. З компонентами класу, стан визначається так:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

До приходу хуків стан застосовувався, як правило, у компонентах класу. Тепер ми можемо додавати стан до функціонального компонента.

Розглянемо приклад нижче. За допомогою switch ми будемо змінювати колір лампочки в залежності від значення стану. Для цього застосуємо хук useState.

Пояснимо що відбувається у цьому коді:

import React, { useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function LightBulb() {
  let [light, setLight] = useState(0);

  const setOff = () => setLight(0);
  const setOn = () => setLight(1);

  let fillColor = light === 1 ? "#ffbb73" : "#000000";

  return (
    <div className="App">
      <div>
        <LightbulbSvg fillColor={fillColor} />
      </div>

      <button onClick={setOff}>Off</button>
      <button onClick={setOn}>On</button>
    </div>
  );
}

function LightbulbSvg(props) {
  return (
    /*
      Нижче розмітка для SVG у формі лампочки.
      Важлива частина — `fill`, де динамічно встановлюється колір, зважаючи на props
    */
    <svg width="56px" height="90px" viewBox="0 0 56 90" version="1.1">
      <defs />
      <g
        id="Page-1"
        stroke="none"
        stroke-width="1"
        fill="none"
        fill-rule="evenodd"
      >
        <g id="noun_bulb_1912567" fill="#000000" fill-rule="nonzero">
          <path
            d="M38.985,68.873 L17.015,68.873 C15.615,68.873 14.48,70.009 14.48,71.409 C14.48,72.809 15.615,73.944 17.015,73.944 L38.986,73.944 C40.386,73.944 41.521,72.809 41.521,71.409 C41.521,70.009 40.386,68.873 38.985,68.873 Z"
            id="Shape"
          />
          <path
            d="M41.521,78.592 C41.521,77.192 40.386,76.057 38.986,76.057 L17.015,76.057 C15.615,76.057 14.48,77.192 14.48,78.592 C14.48,79.993 15.615,81.128 17.015,81.128 L38.986,81.128 C40.386,81.127 41.521,79.993 41.521,78.592 Z"
            id="Shape"
          />
          <path
            d="M18.282,83.24 C17.114,83.24 16.793,83.952 17.559,84.83 L21.806,89.682 C21.961,89.858 22.273,90 22.508,90 L33.492,90 C33.726,90 34.039,89.858 34.193,89.682 L38.44,84.83 C39.207,83.952 38.885,83.24 37.717,83.24 L18.282,83.24 Z"
            id="Shape"
          />
          <path
            d="M16.857,66.322 L39.142,66.322 C40.541,66.322 41.784,65.19 42.04,63.814 C44.63,49.959 55.886,41.575 55.886,27.887 C55.887,12.485 43.401,0 28,0 C12.599,0 0.113,12.485 0.113,27.887 C0.113,41.575 11.369,49.958 13.959,63.814 C14.216,65.19 15.458,66.322 16.857,66.322 Z"
            id="Shape"
            fill={props.fillColor}
          />
        </g>
      </g>
    </svg>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<LightBulb />, rootElement);

Приклад коду на CodeSandbox

Наш компонент — функція

У коді вище ми імпортували useState з react. useState — новий спосіб використовувати можливості this.state.

Помітьте, що цей компонент — функція, а не клас.

Читання та запис стану

Всередині цієї функції ми викликаємо useState для створення змінної стану:

let [light, setLight] = useState(0);

Тут змінна може бути будь-якого типу, на відміну від стану в класах, який обов'язково типу Object.

Як видно вище, ми деструктуруємо значення, що повертає useState.

  • Перше значення (у нас це light) — поточний стан (щось на зразок this.state)
  • Друге значення — функція, що оновлює стан (як традиційне this.setState).

Далі ми створюємо дві функції, які встановлюють стан в 0 чи 1:

const setOff = () => setLight(0);
const setOn = () => setLight(1);

Потім ми використовуємо ці функції як обробники подій для кнопок у view:

<button onClick={setOff}>Off</button>
<button onClick={setOn}>On</button>

React відстежує стан

Коли натиснуто кнопку ON, викликається setOn, який викликає setLight(1). Останній виклик оновлює значення light при наступному відображенні. Це трохи нагадує магію, але React насправді відстежує значення цієї змінної та передає нове значення при повторному відображенні компонента.

Потім ми звертаємось до поточного стану (light), щоб визначити, чи потрібно натискати ON. Ми задаємо колір для SVG в залежності від значення змінної стану. Якщо це 0(off), то fillColor встановлено як #000000. Якщо ж це 1(on), fillColor отримає значення #ffbb73.

Декілька станів

Ви можете створити декілька станів, викликавши useState понад один раз. Наприклад:

let [light, setLight] = useState(0);
let [count, setCount] = useState(10);
let [name, setName] = useState("Yomi");

Зауважте: ви повинні знати про деякі обмеження на використання хуків. Важливо знати, що хуки викликаються лише на верхньому рівні вашої функції. Огляньте Правила Хуків для більш детальної інформації.

Приклад хука useEffect()

Хук useEffect() дозволяє виконувати побічні ефекти (side effects) у функціональних компонентах. Побічними ефектами можуть бути виклики API, оновлення DOM, підписка на слухачів подій — місця, де застосовується «імперативний» підхід.

З хуком useEffect() React знає, що ви хочете виконати певну дію після рендерингу.

Поглянемо на цей приклад. Ми застосуємо хук useEffect() для здійснення викликів API та отримання відповіді:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  let [names, setNames] = useState([]);

  useEffect(() => {
    fetch("https://uinames.com/api/?amount=25&region=nigeria")
      .then(response => response.json())
      .then(data => {
        setNames(data);
      });
  }, []);

  return (
    <div className="App">
      <div>
        {names.map((item, i) => (
          <div key={i}>
            {item.name} {item.surname}
          </div>
        ))}
      </div>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Приклад коду на CodeSandbox

Тут useState та useEffect імпортовані, щоб встановити значення стану як результат виклику API.

Отримання даних та оновлення стану

Для «використання ефектів» нам необхідно помістити нашу дію у функцію useEffect. Ми передаємо «дію» ефекту як анонімну функцію першим аргументом для useEffect.

У нашому прикладі, ми здійснили виклик API для кінцевої точки. Так ми повернули список імен. Коли повертається response, ми перетворюємо його у JSON, а потім використовуємо setNames(data) для встановлення стану.

let [names, setNames] = useState([]);

useEffect(() => {
  fetch("https://uinames.com/api/?amount=25&region=nigeria")
    .then(response => response.json())
    .then(data => {
      setNames(data);
    });
}, []);

Проблеми продуктивності при використанні ефектів

При використанні useEffect варто відзначити деякі особливості.

По-перше, за замовчуванням, useEffect викликатиметься при кожному відображенні. З одного боку, це перевага, тому що не треба турбуватися про застарілі дані, а з іншого — виникає потреба робити HTTP запити при кожному рeндерингу.

Ви можете пропустити ефекти, використовуючи другий аргумент для useEffect як у нашому прикладі. У нас цей аргумент — список змінних, які ми хочемо «переглянути». Таким чином, ефект буде перезапускатися лише якщо якесь значення змінюється.

У прикладі другий аргумент передається у вигляді порожнього масиву. Тобто ми сповіщаємо React, що хочемо викликати цей ефект, лише коли встановлюється компонент.

Дізнатися більше про продуктивність Ефектів можна за посиланням.

useEffect як і useState застосовується до декількох екземплярів, тобто у вас може бути декілька функцій useEffect.

Приклад хука useContext()

Мета Context

Context у React — можливість для дочірнього компонента отримати доступ до значення у батьківському компоненті.

Щоб зрозуміти необхідність контексту при створенні застосунку на React, отримайте значення з верхівки вашого React-дерева для нижньої частини. Без контексту усе закінчиться тим, що ви передаватимете props через компоненти, які насправді їх не потребують. При некоректній реалізації це також призводить до ненавмисної зв'язаності.

Передача вниз деревом «незв'язаних» компонентів також називається прокидуванням props (props drilling).

React Context розв'язує цю проблему. Він дозволяє обмінюватись значеннями через дерево компонентів з тими компонентами, які потребують ці значення.

З useContext() легше користуватися Context

Хук useContext() значно полегшує використання Context.

Функція useContext() приймає контекстний об'єкт, який отримуємо після виконання React.createContext(). Цей об'єкт потім повертає поточне значення контексту.

Розглянемо наступний приклад:

import React, { useContext } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const JediContext = React.createContext();

function Display() {
  const value = useContext(JediContext);
  return <div>{value}, I am your Father.</div>;
}

function App() {
  return (
    <JediContext.Provider value={"Luke"}>
      <Display />
    </JediContext.Provider>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Приклад коду на CodeSandbox

У цьому коді контекст JediContext створюється за допомогою React.createContext().

У компоненті App ми використовуємо JediContext.Provider і встановлюємо value як "Luke". Це означає, що будь-який об'єкт читання контексту у дереві тепер може прочитати це значення. Для цього у функції Display() ми викликаємо useContext і передаємо JediContext як аргумент.

Далі ми передаємо у контекст об'єкт, який ми отримали від React.createContext, і він автоматично виводить значення. Коли значення provider оновлюється, хук спричинить повторний рендeринг з останнім значенням контексту.

Посилання на Context у більших застосунках

Вище ми створили JediContext всередині області видимості обох компонентів, але у більш масштабних застосунках Display та App будуть у різних файлах. Можливо ви будете спантеличені питанням: «як отримати посилання на JediContext серед файлів?»

Рішення — створити новий файл, що експортує JediContext.

Наприклад, у вас є файл context.js:

const JediContext = React.createContext();
export { JediContext };

Тоді в App.js (та Display.js) ми здійснимо імпорт:

import { JediContext } from "./context.js";

Приклад хука useRef()

Посилання (Refs) забезпечують доступ до елементів React, створених методом render().

Функція useRef() повертає ref-об'єкт.

const refContainer = useRef(initialValue);

useRef() та форми з input

Розглянемо приклад використання хука useRef().

import React, { useState, useRef } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  let [name, setName] = useState("Nate");

  let nameRef = useRef();

  const submitButton = () => {
    setName(nameRef.current.value);
  };

  return (
    <div className="App">
      <p>{name}</p>

      <div>
        <input ref={nameRef} type="text" />
        <button type="button" onClick={submitButton}>
          Submit
        </button>
      </div>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Приклад коду на CodeSandbox

У прикладі ми використовуємо useRef() разом з useState() для відображення значення з input у тегу p.

У змінній nameRef створюється екземпляр ref. Ця змінна може бути використана у полі вводу, якщо її відмітити як ref. По суті, це означає, що вміст поля буде доступним через ref.

Кнопка надсилання у коді має обробник події onClick. Обробник submitButton викликає setName (створену через useState).

Так само, як ми робили вже з хуками useState, використаємо setName, щоб встановити стан name. Щоб витягнути ім'я з тегу input, ми читаємо значення nameRef.current.value.

Зауважте, що useRef можна використовувати не лише для атрибута ref.

Використання користувацьких хуків

Крута особливість хуків полягає у тому, що ви можете з легкістю обмінюватись логікою з декількома компонентами за допомогою користувацького хука.

У прикладі нижче ми створимо користувацький хук setCounter(), з яким можна відстежувати стан. Він надає користувацькі функції оновлення стану.

Огляньте також хук useCounter у react-use і за цим посиланням.

import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

function useCounter({ initialState }) {
  const [count, setCount] = useState(initialState);
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  return [count, { increment, decrement, setCount }];
}

function App() {
  const [myCount, { increment, decrement }] = useCounter({ initialState: 0 });
  return (
    <div>
      <p>{myCount}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Приклад коду на CodeSandbox

У наведеному коді ми створюємо функцію useCounter, яка зберігає логіку нашого користувацького хука.

Зверніть увагу, що useCounter може також використовувати інші хуки. Ми почнемо зі створення нового хуку через userState.

Далі, ми визначимо дві допоміжні функції: increment та decrement, які викликають setCount і коригують поточне значення count.

Обов'язково треба повернути посилання для того, щоб взаємодіяти з нашим хуком.


Питання: Що з return та масивом з об'єктом?

Відповідь: Стандарти API для хуків ще не допрацьовані. Все, що ми робимо тут — повертаємо масив, де:

  • Перший елемент — поточне значення хука
  • Другий елемент — це об'єкт, що містить функцію для взаємодії з хуком

Такий стандарт дозволяє з легкістю «перейменовувати» поточне значення хука — як ми робили вище з myCount.

Зверніть увагу, ви можете повернути все, що захочете у вашому користувацькому хуку.


У прикладі вище, ми використали increment та decrement як onClick — обробники у нашому view. Коли користувач натискає на кнопки, лічильник оновлюється та повторно відображається (як myCount) у view.

Написання тестів для React Hooks

Для тестування хуків ми будемо використовувати react-testing-library.

react-testing-library — легке рішення для тестування компонентів React. Бібліотека поширюється на react-dom та react-dom/test-utils для забезпечення утилітних функцій. react-testing-library гарантує, що ваші тести працюватимуть прямо на вузлах DOM.

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

Далі ми напишемо тести для наших хуків на основі взаємодії з нашими компонентами. Гарні новини: наші тести виглядатимуть як звичайні React тести.

Написання тесту для хука useState()

Поглянемо на приклад тесту для userState. Нам треба переконатися, що при натисканні на кнопку "Off", стан встановлюється як 0, а при натисканні на «On» — встановлюється як 1.

import React from "react";
import { render, fireEvent, getByTestId } from "react-testing-library";
// імпорт компонента lighbulb 
import LightBulb from "../index";

test("bulb is on", () => {
  // отримання вузла DOM з вашим відображеним елементом React
  const { container } = render(<LightBulb />);
  // тег p у компоненті LightBulb, що містить поточне значення стану
  const lightState = getByTestId(container, "lightState");
  // посилання на кнопку on 
  const onButton = getByTestId(container, "onButton");
  // посилання на кнопку off 
  const offButton = getByTestId(container, "offButton");

  // імітація кліку на кнопку on 
  fireEvent.click(onButton);
  //очікується значення стану 1
  expect(lightState.textContent).toBe("1");
  // імітація кліку на кнопку off
  fireEvent.click(offButton);
  // очікується значення стану 0
  expect(lightState.textContent).toBe("0");
});

Приклад коду на CodeSandbox

У цьому фрагменті коду ми спочатку імпортуємо хелпери з react-testing-library та компонент для тестування.

  • render допоможе відобразити наш компонент. Контейнер, в якому він відображається, доданий до document.body.
  • getByTestId отримує DOM-елемент по data-testid.
  • fireEvent потрібен для того, щоб приєднати обробник подій на document та обробляти деякі DOM-події через делегацію подій. (Наприклад, клік на кнопку.)

Далі, в assert-функції у тесті ми створюємо константи для data-testid та їх значень, які ми хочемо протестувати. З посиланнями на елементи в DOM, ми можемо використовувати метод fireEvent для імітації кліку на кнопку.

Тест перевіряє чи було змінено стан на 1 при натисканні на onButton та на 0, при натисканні на offButton.

Написання тестів для хука useEffect()

Для нашого прикладу, ми будемо тестувати додавання елементу в кошик, використовуючи хук useEffect. Кількість предметів також зберігається у localStorage.

Ми будемо писати тести, щоб переконатися, що оновлення предмету у кошику також вплине на localStorage. Навіть якщо сторінка буде перезавантажуватись, кількість предметів у кошику збережеться.

import React from "react";
import { render, fireEvent, getByTestId } from "react-testing-library";
// імпорт компоненту App 
import App from "../index";

test("cart item is updated", () => {
  // встановлення значення count як 0
  window.localStorage.setItem("cartItem", 0);
  // отримання вузла DOM з відображеним елементом React
  const { container, rerender } = render(<App />);
  // посилання на кнопку add, що збільшує кількість елементів
  const addButton = getByTestId(container, "addButton");
  // посилання на кнопку reset, яка скидає кількість елементів
  const resetButton = getByTestId(container, "resetButton");
  // посилання на тег p, що відображає кількість елементів
  const countTitle = getByTestId(container, "countTitle");

  // імітація кліку на кнопку add 
  fireEvent.click(addButton);
  // очікується, що значення count буде 1
  expect(countTitle.textContent).toBe("Cart Item - 1");
  // імітація перезавантаження сторінки
  rerender(<App />);
  // досі очікується, що значення count буде 1
  expect(window.localStorage.getItem("cartItem")).toBe("1");
  // імітація кліку на кнопку reset 
  fireEvent.click(resetButton);
  // очікується, що значення count буде 0
  expect(countTitle.textContent).toBe("Cart Item - 0");
});

В assert-функції нашого тесту ми спочатку встановлюємо значення cartItem у localStorage як 0. Тоді ми отримуємо container і rerender з компоненту App через деструктуризацію. rerender дозволяє імітувати перезавантаження сторінки.

Далі, ми отримуємо посилання на кнопки та p-тег, що відображає поточну кількість елементів у кошику. Це значення ми присвоюємо константам.

Таким чином, тест буде імітувати натискання на addButton, перевіряти чи значення count встановлено як 1 та перезавантажувати сторінку. Далі перевіряється чи значення count у localStorage також дорівнює 1. Наостанок, імітується клік на resetButton та перевіряється значення count, яке повинно дорівнювати 0.

Написання тестів для хука useRef()

Для нашого тесту будемо використовувати приклад з useRef(), який розглядали вище. Хук useRef() використовується, щоб отримати значення з поля input і встановити його як значення стану. У файлі index.js розміщена логіка введення та надсилання значення.

import React from "react";
import { render, fireEvent, getByTestId } from "react-testing-library";
// імпорт компоненту App 
import App from "../index";

test("input field is updated", () => {
  // отримання вузла DOM з відображеним елементом React
  const { container } = render(<App />);
  // посилання на поле вводу
  const inputName = getByTestId(container, "nameinput");
  //посилання на тег p, що відображає значення з ref 
  const name = getByTestId(container, "name");
  // посилання на кнопку submit що встановлює значення стану як значення ref 
  const submitButton = getByTestId(container, "submitButton");
  // значення для вводу у полі
  const newName = "Yomi";

  // імітація вводу значення 'Yomi' у поле вводу
  fireEvent.change(inputName, { target: { value: newName } });
  // імітація кліку на кнопку submit 
  fireEvent.click(submitButton);
  // у тесті очікуємо, що значення ref відповідатиме введеному значенню.
  expect(name.textContent).toEqual(newName);
});

Приклад коду на CodeSandbox

У assert-функції тесту ми встановлюємо для полів вводу константи, тег p, який відображає поточне значення, та кнопку submit. До того ж ми визначаємо значення, яке буде введено у поле як константу newName. Щоб здійснити перевірку:

fireEvent.change(inputName, { target: { value: newName } });

Метод fireEvent.change заповнює поле вводу значенням. У нас ім'я зберігається у константі newName, після чого здійснюється submit.

Тест перевіряє рівність значень ref та newName після того як здійснено клік.

Нарешті, ви повинні побачити повідомлення у консолі: «Вітання! Немає провальних тестів!»

Реакція спільноти на Хуки

React Hooks вже встигли сколихнути спільноту з моменту свого виходу. Є безліч прикладів і випадків використання нової фічі. Деякі з основних:

Посилання на різні типи Хуків

Існують різні типи Хуків, які ви можете почати використовувати у вашому React-коді.

  • useState — дозволяє писати чисті функції зі станом.
  • useEffect — дозволяє виконувати побічні ефекти: виклики API, оновлення DOM, підписку на слухачів подій.
  • useContext — дозволяє писати чисті функції з контекстом.
  • useReducer — дає посилання на Redux-подібний редьюсер.
  • useRefuseContext дозволяє писати чисті функції, що повертають змінюваний ref-об'єкт.
  • useMemo — потрібен для повернення запам'ятованого значення.
  • useCallback хук використовується для повернення запам'ятованого колбеку.
  • useImperativeMethods — встановлює значення екземпляра, як значення, яке надається батьківським компонентам при використанні ref.
  • Хук useMutationEffects подібний до хука useEffect: вони обидва дозволяють виконувати зміни DOM.
  • Хук useLayoutEffect потрібен для зчитування макету з DOM та синхронного повторного рендерингу.
  • Custom Hooks дозволяють додавати логіку компонентів у повторно використовувані функції.

Майбутнє хуків

Перевага Хуків у тому, що вони працюють поруч з наявним кодом, тому ви можете повільно вносити зміни, що приймають Хуки. Усе, що треба зробити — оновити React-залежності до версії, що підтримує Хуки.

Як поява хуків вплине на подальшу долю класів? За словами команди React, класи становлять велику частину кодової бази, тому вони залишаться на деякий час.

У нас немає на меті позбавлятися класів. У Facebook ми маємо десятки тисяч компонентів класу і ми, звичайно, не збираємось їх переписувати. Але якщо спільнота React підтримує хуки, немає сенсу в існуванні двох різних способів написання компонентів.

Більше ресурсів

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація