Главная Категории Контакты Поиск

React Hooks. Введение

Узнай, как Hooks может помочь тебе создать React приложение.

React ·28.02.2019·читать 14 мин 🤓·Автор: Alex Myzgin

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

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

Hooks - это новые дополнения, которые доступны в React 16.8.

Как использовать useState Hook

Прежде, мы использовали state для хранения данных, которые потом использовались в нашем приложении. С помощью классов, state определяется подобным образом:

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

Прежде всего, для того что бы использовать state в функциональном компоненте, нужно импортировать useState из React. Затем, для создания компонента, мы используем функцию, а не класс.

useState() API возвращает массив, первое значение содержит state (мы назвали его count, но его можно назвать как угодно) и второе это функция, которую мы вызываем для изменения состояния.

useState() принимает в качестве аргумента начальное значение state. В примере это 0 .

Так как useState() возвращает массив, мы используем деструктуризацию массива для доступа к каждому отдельному элементу, например: const [count, setCount] = useState(0), где:

count - это текущий state, а setCount это функция которая меняет state.

Пример:

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

const Counter = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
};

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

Ты можешь добавить сколько угодно вызовов useState(). Только убедитесь, что вызываешь его с верхнего уровня компонента (не в if или в любом другом блоке).

Пример на codesandbox:

Как использовать useEffect Hook

Hooks имеет еще одну очень важную особенность, которая дает функциональным компонентам доступ к жизненным циклам.

В классовых компонентах, мы часто делаем side effect функции. Например, подписываемся на события или делаем запросы на получение данных, используя методы componentDidMount, componentWillUnmount и componentDidUpdate.

Hooks предоставляет useEffect() API, который принимает функцию в качестве аргумента.

Функция запускается при первом рендеринге компонента и при каждом последующем повторном рендеринге / обновлении. Сначала React обновляет DOM, а затем вызывает любую функцию, переданную в useEffect(). И всё это без блокировки рендеринга пользовательского интерфейса, даже при блокировке кода, в отличии от старых componentDidMount и componentDidUpdate. Что позволяет приложениям работать быстрее.

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

const Counter = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("Alex");

  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);

  useEffect(() => {
    console.log(`Hi ${name} you clicked ${count} times`);
  });

  return (
    <div>
      <p>
        Hi {name} you clicked {count} times
      </p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={() => setName(name === "Alex" ? "Alexey" : "Alex")}>
        Change name
      </button>
    </div>
  );
};

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

componentWillUnmount может быть достигнута путем возвращения функции из useEffect():

useEffect(() => {
  console.log(`Hi ${name} you clicked ${count} times`);
  return () => {
    console.log(`Unmounted`);
  };
});

Функция useEffect() может вызываться много раз, что удобно для разделения несвязанной логики.

Поскольку функция useEffect() запускается при каждом последующем рендеринге / обновлении, мы можем сказать React пропустить вызов для повышения производительности, добавив второй параметр, который является массивом, содержащим список переменных state, которые нужно отслеживать. React будет повторно запускать side effect только в том случае, если один из элементов в этом массиве меняется.

useEffect(() => {
  console.log(`Hi ${name} you clicked ${count} times`);
}, [name, count]);

Точно так же можно сказать React выполнить side effect только один раз (во время сборки и разборки), передав пустой массив:

useEffect(() => {
  console.log(`Hi ${name} you clicked ${count} times`);
}, []);

useEffect() отлично подходит для доступа к сторонним API и многого другого.

Пример на codesandbox:

Правила Hooks

Hooks - это функции JavaScript, но при их использовании необходимо следовать правилам. Также есть плагин линтера для автоматического применения этих правил:

  • Используй Hooks только на верхнем уровне. Не вызывай Hooks внутри циклов, условий или вложенных функций.
  • Используй Hooks только из функций React. Не вызывай Hooks из обычных функций JavaScript.

ESLint Плагин

npm install eslint-plugin-react-hooks@next
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

или в package.json файле добавь:

{
  "eslintConfig": {
    "plugins": ["react-hooks"],
    "rules": {
      "react-hooks/rules-of-hooks": "error"
    }
  }
}

Подробнее об ESLint

Дополнительные Hooks

Следующие hooks необходимы только для конкретных крайних случаев.

  • useReducer
  • custom hook
  • useMemo
  • useCallback

useReducer

В качестве альтернативы useState ты можешь использовать хук useReducer.

useReducer лучше использовать когда логика управления состояниями сложная. Он позволяет оптимизировать производительность компонентов, которые запускают глубокие обновления, поскольку ты можешь передавать dispatch вместо обратных вызовов.

В качестве аргументов он принимает reducer типа (state, action) => newState, начальное состояние или initialState и возвращает текущий state в паре с методом dispatch. (Если ты знаком с Redux, тогда ты уже знаешь, как это работает.)

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

Вот, пример как можно заменить setCount и setName, на useReducer:

const initialState = { count: 0, name: "Alex" };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, ...{ count: state.count + 1 } };
    case "DECREMENT":
      return { ...state, ...{ count: state.count - 1 } };
    case "CHANGE_NAME":
      const name = state.name === "Alex" ? "Alexey" : "Alex";
      return { ...state, ...{ name } };
    default:
      return state;
  }
}

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

  const increment = () => dispatch({ type: "INCREMENT" });
  const decrement = () => dispatch({ type: "DECREMENT" });
  const changeName = () => dispatch({ type: "CHANGE_NAME" });

  return (
    <div>
      <p>
        Hi {state.name} you clicked {state.count} times
      </p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={changeName}>Change name</button>
    </div>
  );
};

При нажатии на кнопку Increment мы делаем dispatch действия { type: "INCREMENT" }, а внутри reducer, в зависимости от action.type, возвращает разный state.

Пользовательский хук или Custom hook

Распространенным способом совместного использования кода между компонентами является Render Prop Components или Higher Order Components (HOC), но с помощью Hooks всё гораздо проще. Ты можешь взять код из существующего компонента и извлечь логику хука для повторного использования.

Для того что бы создать пользовательский хук, нужно просто создать функцию. Однако, здесь есть одна загвоздка. Пользовательский хук должен начинаться со слова use. Мы назвали его useState и перенесли в него useReducer, а в качестве аргумента передали defaultValue. В самом компоненте поменяли useReducer на useState и передали начальное состояние или defaultValue.

const useState = (defaultValue) => {
  const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case "INCREMENT":
        return { ...state, ...{ count: state.count + 1 } };
      case "DECREMENT":
        return { ...state, ...{ count: state.count - 1 } };
      case "CHANGE_NAME":
        const name = state.name === "Alex" ? "Roger" : "Alex";
        return { ...state, ...{ name } };
      default:
        return state;
    }
  }, defaultValue);

  return [state, dispatch];
};

const Counter = () => {
  const [state, dispatch] = useState({
    count: 0,
    name: "Alex",
  });

  const increment = () => dispatch({ type: "INCREMENT" });
  const decrement = () => dispatch({ type: "DECREMENT" });
  const changeName = () => dispatch({ type: "CHANGE_NAME" });

  return (
    <div>
      <p>
        Hi {state.name} you clicked {state.count} times
      </p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={changeName}>Change name</button>
    </div>
  );
};

Существующие хуки

Со времени анонса React Hooks уже создано множество хуков. На этом сайте ты можешь найти множество готовых хуков, которые можно использовать сегодня.

useMemo

useMemo возвращает запомненное значение.

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

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

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

Если массив не указан, то всякий раз новое значение будет вычислено, когда в качестве первого аргумента передается новый экземпляр функции. А если массив пустой, то это значит что функция всегда будет одинаковой и ее не нужно перечитывать; она будет запускаться только при начальной сборке компонента.

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

Мы можем немного модифицировать наш пользовательский хук useState и добавить localStorage чтобы после перезагрузки страницы значения не обнулялиcь.

Для этого в useState, для начала, мы создадим функцию initialValue в которой читаем значения из localStorage по ключу “state” и возвращаем его. Если значения по ключу нет, мы берем значение из defaultValue. :

const initialValue = () => {
  const valueFromStorage = JSON.parse(
    window.localStorage.getItem("state") || JSON.stringify(defaultValue),
  );
  return valueFromStorage;
};

Потом передаем initialValue в useReducer вторым аргументом. Однако, useReducer, в отличие от useState, не поддерживает передачу функции в качестве опции. Вместо этого мы используем хук useMemo. Второй параметр в useMemo указывает, когда запомненная версия должна измениться. В нашем случае мы хотим, чтобы оно всегда было одинаковым, поэтому передаем пустой массив.

const [state, dispatch] = useReducer((state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, ...{ count: state.count + 1 } };
    case "DECREMENT":
      return { ...state, ...{ count: state.count - 1 } };
    case "CHANGE_NAME":
      const name = state.name === "Alex" ? "Roger" : "Alex";
      return { ...state, ...{ name } };
    default:
      return state;
  }
}, useMemo(initialValue, []));

Далее, в useState добавим новый useEffect в котором мы будем записывать значения в localStorage. Что бы не записывать значения в локальное хранилище после каждого рендеринга, мы предоставим второй аргумент и сообщим, что useEffect будет запускаться только в том случае, если изменился наш state.

useEffect(() => {
  window.localStorage.setItem("state", JSON.stringify(state));
}, [state]);

useCallback и React.memo

Исторически только компоненты класса могли расширять PureComponent или реализовывать свой собственный метод shouldComponentUpdate, чтобы контролировать, когда фактически вызывается его рендер. Однако теперь вы можете использовать React.memo HOC для обеспечения такого же типа управления функциональными компонентами. Это не относится к хукам, но был выпущено в React 16.6 и дополняет API хуков.

Для начала давайте разделим наш компонент на более мелкие компоненты Data и Buttons:

// Data.js

import React from "react";

export default function Data({ state }) {
  console.log("Data");
  return (
    <p>
      Hi {state.name} you clicked {state.count} times
    </p>
  );
}
// Button.js
import React from "react";

export default function Buttons({ increment, decrement, changeName }) {
  console.log("Buttons");
  return (
    <div>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={changeName}>Change name</button>
    </div>
  );
}

и импортируем их:

// index.js

// ...
import Data from "./Data";
import Buttons from "./Buttons";
// ...

const Counter = () => {
  // ...

  return (
    <div>
      <Data state={state} />
      <Buttons
        increment={increment}
        decrement={decrement}
        changeName={changeName}
      />
    </div>
  );
};

Теперь мы можем увидеть, что каждый раз, когда мы нажимаем любую кнопку, рендерится и Data, и Buttons компоненты; так как компонент Data изменился и в следствии этого идет пересборка всего Counter компонента. Buttons компонент у нас не меняться, поэтому давайте сделаем так, что бы он не пересобирался каждый раз, когда мы нажимаем на кнопки. Для это используем React.memo и useCallback.

React.memo - это функция, появившаяся в React 16.6, которая представляет собой компонент более высокого порядка, аналогичный PureComponent, но предназначенный для компонентов функций, а не для классов.

Она сравнивает props компонента и запускает рендер только в том случае, если они изменились. Чтобы использовать его, необходимо обвернуть наш компонент функцией memo.

// Button.js
import React, { memo } from "react";

export default memo(function ({ increment, decrement, changeName }) {
  console.log("Buttons");
  return (
    <div>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={changeName}>Change name</button>
    </div>
  );
});

Но, это еще не всё! Обертки функций increment, decrement, changeName постоянно меняются, поэтому при сравнении нам говорят, что мы внесли изменения.

Мы можем решить это с помощью еще одного React хука. Давайте сначала импортируем хук useCallback и обернем им обработчики increment, decrement, changeName.

// index.js
const increment = useCallback(() => dispatch({ type: "INCREMENT" }), []);
const decrement = useCallback(() => dispatch({ type: "DECREMENT" }), []);
const changeName = useCallback(() => dispatch({ type: "CHANGE_NAME" }), []);

useCallback вернет запомненную версию нашего обратного вызова.

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

Теперь, при нажатии на любую кнопку, рендорится только Data компонент, потому что Buttons компонент остается неизменным и запускается только один раз при начальном рендоринге.

Подробнее о Hooks можно познакомиться в документации.

Website, name & logo
Copyright © 2019. Alex Myzgin