React hooks — зрада чи перемога?


З виходом нового React 16.6.0 в документації з’явився HOOKS (PROPOSAL). Вони зараз доступні в react 17.0.0-alpha і обговорюються у відкритому RFC: React Hooks. Давайте розберемося, що це таке і навіщо це потрібно під катом.

Та це RFC і ви можете вплинути на кінцеву реалізацію обговорюючи з творцями react чому вони вибрали той чи інший підхід.

Давайте поглянемо на те, як виглядає стандартний хук:

import { useState } from 'react';

function Example() {
 // Declare a new state variable, which we'll call "count"
 const [count, setCount] = useState(0);

 return (
<div>
 <p>You clicked {count} times</p>
 <button onClick={() => setCount(count + 1)}>
 Click me
</button>
</div>
);
}

 

Спробуйте обміркувати цей код, це тизер і до кінця статті ви вже будете розуміти, що він означає. Перше, що варто знати, що це не ламає зворотну сумісність і можливо їх додадуть в 16.7 після збору зворотного зв’язку і побажань в RFC.
Як запевняють хлопці, це не план по випилювання класів з реакта.
Так само хуки не замінюють поточні концепції реакта, все на місці props/state/context/refs. Це всього лише ще один спосіб використовувати їх силу.

 

Мотивація

Хуки вирішують на перший погляд не зв’язкові проблеми, які з’явилися при підтримці десятків тисяч компонентів протягом 5 років у facebook.
Найскладніше це переиспользовать логіку в stateful компонентах, у реакта немає способу прикріпити багаторазове поведінку до компоненту(наприклад підключити його до сховища). Якщо ви працювали з React вам відоме поняття HOC(high-order-component) або render props. Це досить хороші патерни, але іноді вони використовуються надмірно, вони вимагають реструктуризації компонентів, для того, щоб їх можна було використовувати, що зазвичай робить код більш громіздким. Варто подивитися на типове реактив додаток і стане зрозуміло про що йде мова.

Це називається wrapped-hell — пекло обгорток.
Додаток з одних HOC це нормально в поточних реаліях, підключили компонент до стору/темі/локалізації/кастомным хокам, я думаю це всім знайома.
Стає зрозуміло, що реакту необхідний інший примітивний механізм для розділення логіки.
З допомогою хуків ми можемо витягувати стан компонента, так щоб його можна було тестувати і переиспользовать. Хуки дозволяють повторно використовувати логіку стану без зміни ієрархії компонентів. Це полегшує обмін посиланнями між багатьма компонентами або всієї системи в цілому. Так само класові компоненти виглядають досить страшно, ми описуємо методи життєвого циклу componentDidMount/shouldComponentUpdate/componentDidUpdate, стан компонента, створюємо методи для роботи з станом/стором, биндим методи для екземпляра компоненту і так можна продовжувати до нескінченності. Зазвичай такі компоненти виходять за рамки x рядків, де x досить складно для розуміння.
Хуки дозволяють робити теж саме розбиваючи логіку між компонентами на маленькі функції і використовувати їх усередині компонентів.

 

Класи складні для людей і для машин

У спостереженні facebook класи є великою перешкодою при вивченні React. Вам необхідно зрозуміти як працює this, а він не працює так як в інших мовах програмування, так само слід пам’ятати про прив’язку обробників подій. Без стабільних пропозицій синтаксису код виглядає дуже багатослівно. Люди прекрасно розуміють патерни props/state і так званий top-down data flow, але досить складно розуміють класи. Особливо якщо не обмежуватися шаблонами, не так давно хлопці з реакта эксперементировали з компонуванням компонентів c Prepack і побачили багатообіцяючі результати, але тим не менш компоненти класу дозволяють створювати ненавмисні погані патерни, які змушують ці оптимізації зникати, так само класи не дуже добре мігрують і при гарячої перезавантаження класи роблять її ненадійною. В першу чергу хлопцям хотілося надати API яке підтримує всі оптимізації і відмінно працює з гарячою перезавантаженням.

Читайте також  Використання налагоджувача Android Studio по максимуму

 

Глянемо на хуки

State hook

Код нижче рендерить параграф і кнопку і якщо ми натиснемо на кнопку значення в параграфі буде инкрементировано.

 

import { useState } from 'react';

function Example() {
 // Declare a new state variable, which we'll call "count"
 const [count, setCount] = useState(0);

 return (
<div>
 <p>You clicked {count} times</p>
 <button onClick={() => setCount(count + 1)}>
 Click me
</button>
</div>
);
}

 

Звідси можна зробити висновок що даний хук працює схоже з таким поняттям як state.
Трохи детальніше метод useState приймає один аргумент, це значення за замовчуванням і повертає кортеж(tuple) в якому є саме значення і метод для його зміни, на відміну від setState, setCount не буде робити merge значень, а просто відновить його. Так само ми можемо використовувати множинне оголошення станів, наприклад:

 

function ExampleWithManyStates() {
 // Declare multiple state variables!
 const [age, setAge] = useState(42);
 const [fruit, setFruit] = useState('banana');
 const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
 // ...
}

 

таким чином ми створюємо відразу кілька станів і нам не потрібно думати про те, щоб як то декомпозировать. Таким чином можна виділити, що хуки це функції, які дозволяють “підключатися” до фішках класових компонентів, так само хуки не працюють усередині класів, це важливо запам’ятати.

 

Effect hook

 

Часто в класових компонентах, ми робимо side effect функції, наприклад підписуємося на події або робимо запити за даними, зазвичай для цього ми використовуємо методи componentDidMount/componentDidUpdate

 

import { useState, useEffect } from 'react';

function Example() {
 const [count, setCount] = useState(0);

 // Similar to componentDidMount and componentDidUpdate:
 useEffect(() => {
 // Update the document title using the browser API
 document.title = `You clicked ${count} times`;
});

 return (
<div>
 <p>You clicked {count} times</p>
 <button onClick={() => setCount(count + 1)}>
 Click me
</button>
</div>
);
}

 

Коли ми викликаєте useEffect ми говоримо реакту зробити ‘side effect’ після оновлення змін у DOM дереві. Ефекти оголошуються всередині компонента, тому мають доступ до props/state. Причому їх ми можемо точно так само створювати скільки завгодно.

 

function FriendStatusWithCounter(props) {
 const [count, setCount] = useState(0);
 useEffect(() => {
 document.title = `You clicked ${count} times`;
});

 const [isOnline, setIsOnline] = useState(null);
 useEffect(() => {
 ChatAPI.subscribeToFriendStatus(props.friend.id handleStatusChange);
 return () => {
 ChatAPI.unsubscribeFromFriendStatus(props.friend.id handleStatusChange);
};
});

 function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
 // ...

 

Відразу ж варто звернути увагу на другий side effect в ньому ми повертаємо функцію, робимо ми це для того, щоб виконати якісь дії після того як компонент виконує unmount, в новому api це називають ефекти з очищенням. Інші ефекти можуть повертати, що завгодно.

Читайте також  Пишемо з IoC Starter. Базовий маппінг запитів, використовуючи context, web і orm

 

Правила хуків

 

Хуки це просто javascript функції, але вони вимагають всього двох правил:

 

  • Виконувати хуки слід в самому верху ієрархії функції(це означає, що не слід викликати хуки в умовах і циклах, інакше реактив не може гарантувати порядок виконання хуків)
  • Викликати хуки тільки в React функціях або функціональних компонентах або викликати хуки з кастомних хуків(це нижче).
    Для проходження цих правил хлопці з команди реакта створили лінтер плагін який видасть помилку якщо ви викликаєте хуки в компонентах класу або в циклах і умовах.

 

Кастомні хуки

 

У теж час нам хочеться переиспользовать логіку stateful компонентів, зазвичай для цього використовують або HOC або render props патерни, але вони створюють додатковий обсяг нашої програми.
Наприклад опишемо наступну функцію:

 

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
 const [isOnline, setIsOnline] = useState(null);

 function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

 useEffect(() => {
 ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
 return () => {
 ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});

 return isOnline;
}

 

Усвідомте цей код, це буде кастомный хук, який ми можемо викликати в різних компонентах. Наприклад так:

 

function FriendStatus(props) {
 const isOnline = useFriendStatus(props.friend.id);

 if (isOnline === null) {
 return 'Loading...';
}
 return isOnline ? 'Online' : 'Offline';
}

 

або так

 

function FriendListItem(props) {
 const isOnline = useFriendStatus(props.friend.id);

 return (
 <li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}

 

У будь-якому випадку, ми переиспользуем стан компонента, кожен виклик функції useFriendStatus створює ізольований стан. Так само варто відзначити, що початок цієї функції починається зі слова use це говорить про те, що це хук. Радимо дотримуватися цей формат. Ви можете писати кастомні хуки на що завгодно, анімації/підписки/таймери і багато багато іншого.

 

Є ще пара хуків.

 

useContext

 

useContext дозволяє використовувати замість renderProps звичайне значення, що повертається, в нього слід передати контекст який ми хочемо отримати і він його поверне, таким чином ми можемо звільнитися від усіх HOC, які передавали context в props.

 

function Example() {
 const locale = useContext(LocaleContext);
 const theme = useContext(ThemeContext);
 // ...
}

 

І тепер об’єкт контексту ми можемо просто використовувати у повернутому значенні.

 

useCallback

 

const memoizedCallback = useCallback(
 () => {
 doSomething(a, b);
},
 [a, b],
);

 

Як часто вам доводилося створювати компонент класу, тільки для того, щоб зберегти на метод? Цього не потрібно робити, ми можемо використовувати useCallback і наші компоненти не будуть перерисовываться тому що прийшла нова посилання на onClick.

 

useMemo

 

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

 

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

 

Та тут доводиться дублювати значення в масиві, щоб хук зрозумів, що вони не змінилися.

 

useRef

 

Читайте також  Написання шейдерів у Unity. GrabPass, PerRendererData

useRef повертає мутируемое значення, де поле .current буде ініціалізований першим аргументом, об’єкт буде існувати, поки існує компонент.
Самий звичайний приклад при фокусі на input

 

function TextInputWithFocusButton() {
 const inputEl = useRef(null);
 const onButtonClick = () => {
 // `current` points to the mounted text input element
inputEl.current.focus();
};
 return (
<>
 <input ref={inputEl} type="text" />
 <button onClick={onButtonClick}>Focus the input</button>
</>
);
}

 

useImperativeMethods

 

useImperativeMethods кастомизирует значення примірника який передається з батьків і використовує ref безпосередньо. Як завжди слід уникати передачу посилань на пряму і слід використовувати forwardRef

 

function FancyInput(props, ref) {
 const inputRef = useRef();
 useImperativeMethods(ref () => ({
 focus: () => {
inputRef.current.focus();
}
}));
 return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

 

У цьому прикладі компонент який рендерить FancyInput може викликати fancyInputRef.current.focus().

 

useMutationEffect

 

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

 

useLayoutEffect

 

useLayoutEffect так само схожий на useEffect за винятком того, що запускається синхронно після всіх оновлень DOM і синхронного ре-рендера. Оновлення заплановані в useLayoutEffect застосовуються синхронно, до того, як браузер отримає можливість промалювати елементи. Так само слід намагатися використовувати стандартний useEffect щоб не блокувати візуальні зміни.

 

useReducer

 

useReducer — це хук для створення редюсера який повертає стан і можливість диспатчить зміни:

 

const [state, dispatch] = useReducer(reducer, initialState);

 

Якщо ви розумієте як працює Redux, то ви розумієте, як працює useReducer. Той же приклад, що був з лічильником зверху тільки через useReducer:

 

const initialState = {count: 0};

function reducer(state, action) {
 switch (action.type) {
 case 'reset':
 return initialState;
 case 'increment':
 return {count: state.count + 1};
 case 'decrement':
 return {count: state.count - 1};
}
}

function Counter({initialCount}) {
 const [state, dispatch] = useReducer(reducer, initialState);
 return (
<>
 Count: {state.count}
 <button onClick={() => dispatch({type: 'reset'})}>
Reset
</button>
 <button onClick={() => dispatch({type: 'increment'})}>+</button>
 <button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}

 

Так само useReducer приймає 3 аргумент, це action який повинен виконуватися при ініціалізації редюсера:

 

const initialState = {count: 0};

function reducer(state, action) {
 switch (action.type) {
 case 'reset':
 return {count: action.payload};
 case 'increment':
 return {count: state.count + 1};
 case 'decrement':
 return {count: state.count - 1};
}
}

function Counter({initialCount}) {
 const [state, dispatch] = useReducer(
reducer,
initialState,
 {type: 'reset', payload: initialCount},
);

 return (
<>
 Count: {state.count}
<button
 onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
 <button onClick={() => dispatch({type: 'increment'})}>+</button>
 <button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}

 

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

 

Підбиваючи підсумок

Хуки досить потужний підхід за рішенням wrapper-hell і вирішують кілька проблем, але всі їх можна звести одному з визначенням передача посилань. Вже зараз починають з’являтися збірники хуків по використанню або цей збірник. Більш детальніше з хуками можна познайомитися в документації

Степан Лютий

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

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *