Логотип компании Goodbit

React Hooks Простыми Словами

Евгений

Евгений

Руководитель DevOps Отдела

Мужчина стоит возле большого смартфона и что-то смотрит на телефоне. Рядом сидит девушка на кубе и пишет что-то на ноутбуке

React
Hooks

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

На заре React-человечества

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


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

Хук useState – простой хук для разработчика, но важный для всего приложения

Начнём с самого простого и важного хука – useState. Из самого названия становится понятно, что он связан с состоянием компонента. Именно благодаря ему у функциональных компонентов появилось состояние.


Приведу простой пример использования данного хука, а потом разберем его подробнее.


const App = () => {const [value, valueChange] = useState(0);
return(<div>{value}<button onClick={() => valueChange(value + 1)Увеличить значение на 1</button></div>);};

По сути, этот пример состоит из состояния value, которое содержит в себе целочисленное значение, и кнопки “Увеличить значение на 1”. При нажатии на нее состояние value увеличивается на 1.

Посмотрите на строчку:


const [value, valueChange] = useState(0);

В ней создается состояние и метод, который будет менять это значение. Хук useState по сути принимает в качестве параметра начальное значение, то есть на начальном этапе наш value будет иметь значение 1. И возвращает useState массив из двух элементов: первый – состояние, второй – метод, который будет его изменять. Разработчики хуков использовали довольно изящный подход. При использовании деструктуризации он позволяет задать любое значение состояния и метода минимальным количеством кода.


<button onClick={() => valueChange(value + 1)Увеличить значение на 1</button>

Тут добавлен обработчик события нажатия на кнопку. Поясню: при нажатии на кнопку мы вызываем метод valueChange и отправляем туда новое значение – в нашем случае увеличенное на один.


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

Хук useContext – сквозь пространство

Чтобы передать какие-то данные в компонент, мы можем использовать props. Но есть и альтернативный способ – context.


Если вы ранее его не использовали, то контекст позволяет передавать данные от родительского компонента к дочернему, минуя промежуточные.


Чтобы было понятнее, создали небольшой пример, который позволит понять его работу. У нас есть три компонента. Первый из них External – внешний, второй – Intermediate, то есть промежуточный, а третий назовем Internal, и он будет внутренним. По сути, все они будут вложены друг в друга. Наша задача – передача данных из компонента External в компонент Internal, минуя Intermediate, так как к нему эти данные отношения не имеют.


import {createContext, useContext} from "react"
const MyContext = createContext("without provider");
const External = () => {return (<MyContext.Provider value="Hello, I am External"><Intermediate /></MyContext.Provider>);};
const Intermediate = () => {return <Internal />};
const Internal = () => {const context = useContext(MyContext);
return `I am Internal component. I have got the message from External:"${context}`};

Чтобы использовать контекст, мы создаём объект MyContext, вызывая метод createContext. В компоненте External оборачиваем компонент Intermediate в компонент MyContext.Provider. Тем самым говорим, что все вложенные в него компоненты смогут получить доступ к данным, которые мы передаем, помещая их в параметр value.


Причём они будут доступны только в тех компонентах, в которых нам это нужно. Для этого мы должны использовать хук useContext, а в качестве аргумента у него будет объект MyContext. Хук useContext вернёт нам данные, переданные в параметр value у MyContext.Provider, которые мы поместим в переменную context. Обратите внимание, что в качестве аргумента в createContext мы передали строку (“without context”). Его значение попадет в переменную context в том случае, если вы вдруг забудете создать обертку MyContext.Provider, то есть он поможет не допустить ошибку из-за невнимательности.


Благодаря хуку useContext можно использовать context в функциональных компонентах, и данные будут попадать только в те компоненты, в которых они нужны. Также он избавит от проблемы с drops drilling.

Хуки useEffect и useLayoutEffect – придание жизни компонентам, а точнее – придание методов жизненного цикла

Если вы работали с классовыми компонентами, то знакомы с методами жизненного цикла. Они служат для того, чтобы совершать какие-то операции на разных стадиях жизни компонента. Для этого у нас есть два хука – useEffect и useLayoutEffect. Они похожи между собой, за исключением небольшой разницы в рендеринге. В случае с useLayoutEffect React не запускает рендеринг построенного DOM дерева до тех пор, пока не отработает useLayoutEffect. Если же мы берём useEffect, то React сразу запускает рендеринг построенного DOM, не дожидаясь запуска useEffect.


С помощью этих двух хуков в функциональных компонентах можно смоделировать работу трех методов жизненного цикла – componentDidMount, componentDidUpdate, componentWillUnmount. Более точно их работу имитирует useLayoutEffect, так как в классовых компонентах отрисовка DOM-дерева не запускается до тех пор, пока не отработает метод componentDidMount.


Мужчина смотрит на монитор с кодом
Два монитора на котором написан код

Поскольку эти два хука имеют один и тот же интерфейс, продемонстрируем его на более популярном хуке – useEffect, а для другого всё будет аналогично.


useEffect принимает в себя два аргумента:


  • callback. Внутри него вся полезная нагрузка, которую мы хотим описать. Например, можно делать запросы на сервер, задание обработчиков событий на документ или что-то ещё;
  • массив, состоящий из аргументов. При изменении значения внутри любого из них будет запускаться наш callback. Именно благодаря этому аргументу мы можем имитировать методы жизненного цикла.

Рассмотрим имитацию componentDidMount. Хук useEffect запускается не только при изменении элементов массива из второго аргумента, но также и после монтирования компонента. Фактически componentDidMount запускается на той же стадии. Если мы укажем в качестве второго аргумента пустой массив, callback запустится на стадии монтирования компонента. А поскольку никаких зависимостей для хука внутри массива мы не задали, то аргумент callback не будет больше запускаться.


const App = ({data}) => {useEffect(() => {console.log("componentDidMount");}, []);
return null;};

Имитация componentDidUpdate также возможна. Но в случае с хуками есть лучший способ. Поскольку мы можем указать в массиве только те зависимости, которые нам нужны, у нас есть более гибкий аналог метода componentDidUpdate, который запускается при изменении необходимых параметров. Если нужно сделать аналогию componentDidUpdate, в качестве зависимостей можно указать все параметры и состояния. При этом важно учитывать, что useEffect запускается и на стадии монтирования.


const App = ({data}) => {useEffect(() => {console.log("componentDidUpdate");}, [data]);
return null;};

Хук useRef – прямая связь с узлами и не только

Бывают ситуации, когда необходимо обратиться к какому-то DOM-объекту напрямую. Для этого существует хук useRef.


Рассмотрим пример:


const App = () => {const ref = useRef();
useEffect(() => {console.log(ref.current);}, []);
return <div ref={ref} />};

Мы создаём объект ref и указываем его в качестве элемента, обозначающего DOM-объект, к которому мы хотим обратиться, а также прописываем этот объект в качестве параметра. Далее мы можем взаимодействовать с Dom-объектом напрямую, как если бы мы нашли его с помощью селектора. Для этого используем свойство current у объекта ref.


Помимо этого можно применить useRef, если нам нужно будет запомнить данные в компоненте. Но не рекомендуем вызывать рендеринг в случае изменения состояния компонента. В классовых компонентах для этой цели обычно используют this.

Хук useReducer – снова идём сквозь пространство

Разработчикам React так понравился Redux, что они решили добавить его аналог в состав React. Этот хук позволяет вынести данные из компонентов.


Монитор с кодом

import {useReducer} from "react"
const initialState = {count: 0};
function reducer(state, action) {switch (action.type) {case "increment":return {...state,count: state.count + 1,};case "decrement":return {...state,count: state.count - 1,};default:throw new Error();}}
const App = () => {const [state, dispatch] = useReducer(reducer, initialState);
return (<>{state.count}<button onClick={() => dispatch({type: "decrement"})}>-</button><button onClick={() => dispatch({type: "increment"})}>+</button></>);};

Монитор с кодом
Мужчина в наушниках сидит за столом и пишет код
Мужчина в наушниках сидит за столом и пишет код

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

Хук useMemo – оптимизируй вычисления

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


const MyComponent = {a, b} => {const sqrt = a * a;
return (<div><div>A в квадрате: {sqrt}</div><div>B: {b}</div></div>);};

В этой ситуации компонент перерендеривается в том случае, если изменяется один из параметров – a или b. Представим, что у нас много раз изменяется параметр b, при этом параметр a остаётся прежним. В таком случае мы много раз вычисляем одно и то же произведение, которое помещаем в переменную sqrt. Но зачем нам это, если параметр a в этом случае остаётся прежним? Получается, мы лишний раз нагружаем наш ПК вычислениями одного и того же. И хотя в данном случае операция произведения не самая “энергозатратная”, в других ситуациях возможна лишняя нагрузка. Избежать избыточных вычислений нам помогает хук useMemo.

Давайте немного преобразуем наш пример.


const MyComponent = {a, b} => {const sqrt = useMemo(() => a * a);
return (<div><div>A в квадрате: {sqrt}</div><div>B: {b}</div></div>);};

Тут всё осталось по-прежнему, за исключением ситуации, когда мы обернули наше произведение в хук useMemo, в который передали callback и массив зависимостей. По сути они работают также, как и в useEffect: как только меняется какая-то зависимость из массива, запускается callback, который рассчитывает другое значение. Если ни одна зависимость не поменялась, то при рендеринге в переменную будет подставлено предыдущее вычисленное значение.

Хук useCallback – ещё больше оптимизации

В силу того, что функциональный компонент – это функция, при каждом рендеринге запускается всё, что объявлено в ней. Предположим, что мы создаем внутри компонента функцию и передаем ее в дочерний компонент. Это самая обыкновенная практика. Она часто встречается, когда нам нужно из дочернего компонента изменить что-то в родительском. Создадим небольшой пример, чтобы это продемонстрировать.


const MyComponent = memo(({changer}) => {return (<div><button onClick={changer}>+</button></div>);};
const App = () => {const [value, valueChange] = useState(Math.random());
const changer = () => valueChange(Math.random());
return (<div>{value}<ControlPannel changer={changer} /></div>);};

В данном примере представлены два компонента, один из них – ControlPanel, который отвечает за стилизацию контрольной панели. В ней всего одна кнопка, которая меняет состояние родительского компонента. В качестве параметра в него передан метод changer, который внутри себя содержит вызов метода valueChange, он-то и обновляет состояние. Для простоты изменим значение состояние, просто поместив туда случайное число. Мы специально обернули ControlPanel в memo, чтобы этот компонент перерисовывался только в том случае, если изменились его параметры. Однако в данном случае у нас возникает проблема: при каждой отрисовке компонента App мы будем заново создавать метод changer. Хотя сигнатура у метода будет одинаковой, каждый раз будет создан новый метод, следовательно, у ControlPanel будут происходить повторные рендеринги, но по сути ничего не меняется. В этом случае в качестве параметра будут передаваться разные реализации одной и той же функции.


Избежать этого поможет useCallback.


const MyComponent = memo(({increment}) => {return (<div><button onClick={increment}>+</button></div>);};
const App = () => {const [value, valueChange] = useState(Math.random());
const increment = useCallback(() =>valueChange(Math.random()), []);
return (<div>{value}<ControlPannel increment={increment} /></div>);};

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

Пользовательский хук – создай мир своими руками

Пользовательские хуки – это те же самые функции, которые внутри себя используют какие-либо из стандартных хуков. Единственное требование, которое здесь необходимо соблюдать – относиться к ним, как к хукам. То есть, соблюдать правила, что мы используем при работе с хуками: не вызывать их внутри условных конструкций (таких, как if или switch) и внутри циклов (например for), а также не использовать хуки внутри колбэков других хуков.


Для того чтобы все в команде соблюдали указанные правила и понимали, что это хуки, а не просто методы, называть их лучше в формате useИмяХука.


Рассмотрим пример пользовательского хука:


const useSingleLog = () => {useEffect(() => {console.log("I am single log");}, []);};

Как вы видите, мы создали хук, который позволяет нам единожды вывести строку в консоль. Он содержит в себе хук useEffect. По сути, мы можем использовать его в любых компонентах.