Front-end course

React. Hooks

React. Hooks

React Hooks - это новое дополнение в React 16.8. Они позволяют вам использовать состояние и другие функции React без написания класса.

Hooks - это функции, которые позволяют вам «зацепить» состояние React и функции жизненного цикла компонентов функций. Hooks не работают внутри классов - они позволяют использовать React без классов.

React предоставляет несколько встроенных хуков, таких как useState. Вы также можете создавать свои собственные Hooks для повторного использования поведения с состоянием между различными компонентами. Так выглядит реализация простейшего счетчика с использованием React Hooks:

import React, { useState } from 'react';

const example = () => {
  const [count, setCount] = useState(0);

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

Мотивация стоящая за Hooks

Хотя компоненто-ориентированная архитектура позволяет нам повторно использовать view в нашем приложении, одна из самых больших проблем, с которыми сталкивается разработчик, заключается в том, как повторно использовать логику, находящуюся в state, между компонентами. Когда у нас есть компоненты, которые имеют сходную логику состояния, нет хороших решений для переиспользования компонентов, и это иногда может привести к дублированию логики в конструкторе и методах жизненного цикла. Чтобы решить эту проблему, обычно используют:

  • компоненты высшего порядка (High Order Components)
  • render props

Но оба эти паттерна имеют недостатки, которые могут способствовать усложнению кодовой базы.

Hooks нацелены на решение всех этих проблем, позволяя вам писать функциональные компоненты, которые имеют доступ к state, context, методам жизненного цикла, ref и т. д., без написания классов.

Как Hooks соотносятся с классами

Если вы знакомы с React, один из лучших способов понять Hooks - это посмотреть, каким образом мы можем воспроизвести поведение, с которым мы привыкли работать при использовании классов, используя Hooks. Напомним, что при написании классов компонентов нам часто необходимо:

  • Управлять state;
  • Использовать методы жизненного цикла, такие как componentDidMount() и componentDidUpdate();
  • Доступ к context (static contextType);

С помощью React Hooks мы можем воспроизвести аналогичное поведение в функциональных компонентах:

  • Для доступа к состоянию компонента использовать useState() hook;
  • Вместо использования методов жизненного цикла таких как componentDidMount() и componentDidUpdate(), использовать useEffect() hook;
  • Вместо static свойства contextType использовать useContext() hook;

useState

State является неотъемлемой частью React. Он позволяет нам объявлять переменные, которые содержат данные. Они, в свою очередь, будут использоваться в нашем приложении. С помощью классов state обычно определяется следующим образом:

class Example extends React.Component {
    // or
    constructor(props) {
        super(props)
        this.state = {
            count: 0
        }
    }
    // or 
    state = { count: 0 }
    
    render() {
        return <div>{this.state.count}</div>  
    }
}

До Hooks state обычно использовался только в компоненте - классе, но, как упоминалось выше, Hooks позволяет нам добавлять состояние и к функциональному компоненту.

import React, { useState } from 'react';

const example = () => {
  const [count, setCount] = useState(0);

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

В вышеприведенном блоке кода, мы начинаем с импорта useState из React. UseState — это новый способ использования возможностей, которые раньше могло предложить this.state. Затем обратите внимание, что этот компонент является функцией, а не классом. Интересно!

Внутри этой функции мы вызываем useState для создания переменной в state:const [count, setCount] = useState(0); Здесь useState - это Hook! Мы вызываем его внутри компонента функции, чтобы добавить к нему локальное состояние. React сохранит это состояние между повторными визуализациями. Как видно выше, мы используем деструктуризацию по возвращаемому значению useState.

  • Первое значение, count в этом случае, является текущим state (как this.state)
  • Второе значение - это функция, используемая для обновления значения state (первого значения) (как this.setState).

Единственный аргумент для useState - это начальное состояние. В приведенном выше примере это 0, потому что наш счетчик начинается с нуля. Обратите внимание, что в отличие от this.state, состояние здесь не обязательно должно быть объектом - хотя может быть, если хотите. Аргумент начального состояния используется только во время первого рендеринга. Вы можете использовать State Hook более одного раза в одном компоненте:

const ExampleWithManyStates = () => {
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}

useEffect

UseEffect Hook позволяет выполнять side эффекты в функциональных компонентах. Побочными эффектами могут быть обращения к API, обновление DOM, подписка на обработчики событий - все, что хотите, лишь бы произошло «императивное» действие. Используя useEffect() Hook, React знает, что вы хотите выполнить определенное действие после рендеринга. Давайте посмотрим на пример ниже. Мы будем использовать useEffect() для вызова API и получения ответа.

import { useState, useEffect } from "react";

const example = () => {
	const [posts, setPosts] = useState([ ]);

	useEffect(() => {
		fetch("https://jsonplaceholder.typicode.com/posts")
			.then(response => response.json())
			.then(json => setPosts(json));
	});

	return (
		<div>
			{posts.map(el => (
				<div key={el.id}>
					<h3>{el.title}</h3>
					<p>{el.body}</p>
				</div>
			))}
		</div>
	);
};

Получение данных и обновления state

Чтобы «использовать эффект», нам нужно поместить наш action в функцию useEffect, то есть мы передаем «action» эффект как анонимную функцию, как первый аргумент useEffect. В примере выше мы обращаемся к API, которое возвращает список постов. Когда возвращается response, мы конвертируем его в JSON, а затем используем setPosts(data) для установки state.

Проблемы с производительностью при использовании Effects

Однако стоит сказать еще кое-что об использовании useEffect. Первое, о чем нужно подумать, это то, что по умолчанию наш useEffect будет вызываться на каждом рендере! Хорошей новостью является то, что нам не нужно беспокоиться об устаревших данных, но плохая новость заключается в том, что мы, вероятно, не хотим делать HTTP-запрос для каждого рендеринга (как в этом случае). Вы можете пропустить effects, используя второй аргумент useEffect. Второй аргумент useEffect - это список переменных, которые мы хотим «наблюдать», а затем мы будем повторно запускать эффект только при изменении одного из этих значений.

В приведенном выше примере кода обратите внимание, что мы ничего не передаем в качестве второго аргумента. Давайте поправим этом добавив вторым параметром пустой массив.

useEffect(() => {
		fetch("https://jsonplaceholder.typicode.com/posts")
			.then(response => response.json())
			.then(json => setPosts(json));
	}, []);

Это мы говорим React, что мы хотим только назвать этот effect при монтировании компонента.

Кроме того, как и функция useState, useEffect позволяет использовать несколько экземпляров, что означает, что вы можете иметь несколько функций useEffect.

В компонентах React есть два основных вида побочных эффектов: те, которые не требуют очистки, и те, которые требуют.

Эффекты без очистки

Иногда мы хотим запустить дополнительный код после того, как React обновит DOM. Сетевые запросы, ручные мутации DOM и ведение журнала - это типичные примеры эффектов, которые не требуют очистки. Мы говорим это потому, что можем запустить их и сразу же забыть о них. Давайте сравним, как классы и hooks позволяют нам выражать такие побочные эффекты.

Пример с использованием классов

В компонентах класса React сам метод рендеринга не должен вызывать побочных эффектов. Было бы слишком рано - мы обычно хотим выполнять наши эффекты после того, как React обновит DOM. Вот почему в классах React мы помещаем побочные эффекты в componentDidMount и componentDidUpdate.

class Example extends React.Component {
    state = { count: 0 };

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

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

Пример c использованием хуков

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

Что делает useEffect?

Используя этот хук, вы сообщаете React, что ваш компонент должен что-то делать после рендеринга. React запомнит переданную вами функцию (мы будем называть ее «нашим эффектом») и вызовет ее позже после выполнения обновлений DOM. В этом случае мы устанавливаем заголовок документа, но мы также можем выполнять выборку данных или вызывать какой-то другой императивный API. Почему useEffect вызывается внутри компонента? Размещение useEffect внутри компонента позволяет получить доступ к переменной состояния счетчика (или любым реквизитам) прямо из эффекта. Нам не нужен специальный API для его чтения - он уже находится в области действия функции. Хуки охватывают JavaScript-замыкания и избегают внедрения специфичных для React API, где JavaScript уже предоставляет решение. Запускается ли useEffect после каждого рендера? Да! По умолчанию он запускается как после первого рендера, так и после каждого обновления. Вместо того, чтобы думать в терминах «монтирования» и «обновления», вам может оказаться проще думать, что эффекты происходят «после рендеринга». React гарантирует, что DOM был обновлен к моменту запуска эффектов.

Детальное объяснение

Теперь, когда мы знаем больше об эффектах, эти строки должны иметь смысл:

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

useEffect(() => {
    document.title = `You clicked ${count} times`;
});

Мы объявляем переменную состояния count, а затем говорим React, что нам нужно использовать эффект. Мы передаем функцию в useEffect Hook. Эта функция, которую мы передаем, является нашим эффектом. Внутри нашего эффекта мы устанавливаем заголовок документа с помощью API браузера document.title. Мы можем прочитать последний счет внутри эффекта, потому что он входит в сферу нашей функции. Когда React отображает наш компонент, он запоминает эффект, который мы использовали, а затем запускает наш эффект после обновления DOM. Это происходит для каждого рендера, включая первый.Функция, переданная useEffect, будет отличаться для каждого рендера. Это намеренно.

Фактически, это то, что позволяет нам считать значение счетчика изнутри эффекта, не беспокоясь о его устаревании. Каждый раз, когда мы перерисовываем, мы планируем новый эффект, заменяя предыдущий. В некотором смысле это заставляет эффекты вести себя как часть результата рендеринга - каждый эффект «принадлежит» определенному рендеру.

В отличие от componentDidMount или componentDidUpdate, эффекты, запланированные с помощью useEffect, не блокируют браузер от обновления экрана. Это делает ваше приложение более отзывчивым. Большинство эффектов не должно происходить синхронно. В редких случаях, когда они это делают (например, измерение макета), существует отдельный хук useLayoutEffect с API, идентичным useEffect.

Эффекты с очисткой

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

Давайте сравним, как мы можем сделать это с классами и с hook.

Пример с использованием классов

В классе React вы обычно устанавливаете подписку в componentDidMount и очищаете ее в componentWillUnmount. Например, допустим, у нас есть модуль ChatAPI, который позволяет нам подписаться на онлайн-статус друга. Вот как мы можем подписаться и отобразить этот статус с помощью класса:

class FriendStatus extends React.Component {
  state = { isOnline: null };

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(this.props.friend.id,this.handleStatusChange);
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(this.props.friend.id,this.handleStatusChange);
  }

  handleStatusChange = status => {
    this.setState({ isOnline: status.isOnline });
  }

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

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

Пример c использованием хуков

Давайте посмотрим, как мы могли бы написать этот компонент с помощью Hooks. Вы можете подумать, что для очистки нам понадобится отдельный эффект. Но код для добавления и удаления подписки настолько тесно связан, что useEffect предназначен для того, чтобы держать его вместе. Если ваш эффект возвращает функцию, React запустит ее, когда придет время для очистки:

import React, { useState, useEffect } from 'react';

const FriendStatus = props => {
  const [isOnline, setIsOnline] = useState(null);

  const handleStatusChange = status => {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

Почему мы вернули функцию из нашего эффекта?

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

Когда именно React убирает эффект?

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

useContext

Контекст в React - это способ для дочернего компонента получить доступ к значению в родительском компоненте. Чтобы понять необходимость context: при создании React приложения вам часто нужно передавать значения с верха вашего дерева React вниз. Не используя context, вы передаете props через компоненты, которым не обязательно о них знать. Передача props вниз по дереву «несвязанных» компонентов ласково называется props drilling. React Context решает проблему props drilling, позволяя вам делиться значениями через дерево компонентов, с любым компонентом, который запрашивает эти значения. useContext() упрощает использование context. С useContext Hook использование context становится проще, чем когда-либо. Функция useContext() принимает объект сontext, который изначально возвращается из React.createContext(), а затем возвращает текущее значение контекста.

import React, { useContext } from 'react';

const WhoIsJedi = React.createContext();

const Display = () => {
    const value = useContext(WhoIsJedi)
    return <div>{value}</div>
}


const App = () => {
    return <WhoIsJedi.Provider value={'Luke'}>
        <Display />
    </WhoIsJedi.Provider>
}

В приведенном выше коде context WhoIsJedi создается с использованием React.createContext(). Мы используем WhoIsJedi.Provider в нашем App компоненте и устанавливаем там значение «Luke». Это означает, что любой компонент, которому нужно получить доступ к context теперь сможет считать это значение. Чтобы прочитать это значение в функции Display(), мы вызываем useContext, передавая аргумент WhoIsJedi. Затем мы передаем объект context, который мы получили из React.createContext, и он автоматически выводит значение. Когда значение провайдера будет обновляться, этот Hook автоматически сработает с последним значением context.

useRef

Refs предоставляет способ доступа к React элементам , созданным в методе render().

Функция useRef() возвращает объект ref.

const refContainer = useRef();

Давайте посмотрим пример использования useRef() hook.

import React, { useRef, useState } from 'react';

const App = () => {
    const [ name, setName ] = useState('Tony');
    
    const inputRef = useRef();
    
    const submit = () => setName(inputRef.current.value)
    
    return (
        <div>
            <p>{name}</p>
            <div>
                <input ref={inputRef} type="text" />
                <button type="button" onClick={submit}>Submit</button>
            </div>
        </div>
    )
}

В приведенном выше примере мы используем useRef() в сочетании с useState(), чтобы отрендерить значение input в тег p.Ref создается в переменной inputRef. Затем переменную inputRef можно использовать в input, задав как ref. По существу, это означает, что теперь содержимое поля ввода будет доступно через ref. Кнопка отправки в коде имеет обработчик события onClick, называемый submit. Функция submit вызывает setName (созданный через useState).

Пользовательские Hooks

Одной из самых крутых особенностей Hooks является то, что вы можете легко делиться логикой между несколькими компонентами, создавая собственный Hook.

Рассмотрим один из примеров пользовательского hook, наш компонент:

const App = () => {
    return <div>
        <h1>{myCount}</h1>
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
    </div>
}

И наша задача в том чтобы реализовать вывод счетчика и работу кнопок через пользовательский hook:

import React, { useState } from 'react';

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

export default useCounter

В приведенном выше блоке кода мы создаем функцию useCounter, которая хранит логику нашего hook. Затем мы определяем две вспомогательные функции: increment и decrement, которые вызывают setCount и соответственно корректируют текущий count. Наконец, мы возвращаем ссылки, необходимые для взаимодействия с нашим Hook. Добавим наш hook в компонент

import useCounter from '../../somePlaсe';

const App = () => {

    const [myCount, { increment, decrement }] = useCounter({ initialState: 0 })

    return <div>
        <h1>{myCount}</h1>
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
    </div>
}

Правила Hooks

Хуки - это функции JavaScript, но при их использовании необходимо следовать двум правилам.

Существует плагин для линтера для автоматического применения этих правил:

  • Вызывайте Hooks только на верхнем уровне

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

    • Вызовите хуки из компонентов функции React.
    • Вызовите хуки из пользовательских хуков

Следуя этим правилам, вы гарантируете, что вся логика состояния в компоненте четко видна из его исходного кода.


Written by Vadim Goloviychuk