Можливо ви бачили нову фічу 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);
Наш компонент — функція
У коді вище ми імпортували 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®ion=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);
Тут useState
та useEffect
імпортовані, щоб встановити значення стану як результат виклику API.
Отримання даних та оновлення стану
Для «використання ефектів» нам необхідно помістити нашу дію у функцію useEffect
. Ми передаємо «дію» ефекту як анонімну функцію першим аргументом для useEffect
.
У нашому прикладі, ми здійснили виклик API для кінцевої точки. Так ми повернули список імен. Коли повертається response
, ми перетворюємо його у JSON, а потім використовуємо setNames(data)
для встановлення стану.
let [names, setNames] = useState([]);
useEffect(() => {
fetch("https://uinames.com/api/?amount=25®ion=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);
У цьому коді контекст 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);
У прикладі ми використовуємо 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);
У наведеному коді ми створюємо функцію 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");
});
У цьому фрагменті коду ми спочатку імпортуємо хелпери з 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);
});
У assert-функції тесту ми встановлюємо для полів вводу константи, тег p
, який відображає поточне значення, та кнопку submit
. До того ж ми визначаємо значення, яке буде введено у поле як константу newName
. Щоб здійснити перевірку:
fireEvent.change(inputName, { target: { value: newName } });
Метод fireEvent.change
заповнює поле вводу значенням. У нас ім'я зберігається у константі newName
, після чого здійснюється submit
.
Тест перевіряє рівність значень ref
та newName
після того як здійснено клік.
Нарешті, ви повинні побачити повідомлення у консолі: «Вітання! Немає провальних тестів!»
Реакція спільноти на Хуки
React Hooks вже встигли сколихнути спільноту з моменту свого виходу. Є безліч прикладів і випадків використання нової фічі. Деякі з основних:
- Сайт, що демонструє колекцію React Hooks.
- react-use — бібліотека с цілою купою Хуків.
-
Приклад з CodeSandbox, в якому створюються анімації з хуком
useEffect
та react-spring. -
Приклад використання хуку
useMutableReducer
, який дозволяє змінити стан та оновити його у редьюсері. - Приклад з CodeSandbox, що демонструє комплексне використання зв'язку «батьківський-дочірній» та редьюсера.
- toggle component, створений з React Hooks.
- Інша колекція React Hooks, що відрізняється хуками для input-значень, орієнтації пристроїв та видимості документів
Посилання на різні типи Хуків
Існують різні типи Хуків, які ви можете почати використовувати у вашому React-коді.
-
useState
— дозволяє писати чисті функції зі станом. -
useEffect
— дозволяє виконувати побічні ефекти: виклики API, оновлення DOM, підписку на слухачів подій. -
useContext
— дозволяє писати чисті функції з контекстом. -
useReducer
— дає посилання на Redux-подібний редьюсер. -
useRef
—useContext
дозволяє писати чисті функції, що повертають змінюванийref
-об'єкт. -
useMemo
— потрібен для повернення запам'ятованого значення. -
useCallback
хук використовується для повернення запам'ятованого колбеку. -
useImperativeMethods
— встановлює значення екземпляра, як значення, яке надається батьківським компонентам при використанніref
. - Хук
useMutationEffects
подібний до хукаuseEffect
: вони обидва дозволяють виконувати зміни DOM. - Хук
useLayoutEffect
потрібен для зчитування макету з DOM та синхронного повторного рендерингу. - Custom Hooks дозволяють додавати логіку компонентів у повторно використовувані функції.
Майбутнє хуків
Перевага Хуків у тому, що вони працюють поруч з наявним кодом, тому ви можете повільно вносити зміни, що приймають Хуки. Усе, що треба зробити — оновити React-залежності до версії, що підтримує Хуки.
Як поява хуків вплине на подальшу долю класів? За словами команди React, класи становлять велику частину кодової бази, тому вони залишаться на деякий час.
У нас немає на меті позбавлятися класів. У Facebook ми маємо десятки тисяч компонентів класу і ми, звичайно, не збираємось їх переписувати. Але якщо спільнота React підтримує хуки, немає сенсу в існуванні двох різних способів написання компонентів.
Більше ресурсів
- Команда React виконала чудову роботу з документації React Hooks. Більше за посиланням
- Огляньте Офіційне API
- Поточна RPC: переходьте за посиланням для того, щоб поставити питання чи залишити коментарі
Ще немає коментарів