javascript

ReactJS: шпаргалка по хукам

  • четверг, 24 сентября 2020 г. в 00:31:00
https://habr.com/ru/post/520370/
  • Разработка веб-сайтов
  • JavaScript
  • Программирование
  • ReactJS




Доброго времени суток, друзья!

Представляю вашему вниманию справочник по основным хукам React: useState, useEffect, useLayoutEffect, useContext, useReducer, useCallback, useMemo и UseRef.

Источник вдохновения: React Hooks cheat sheet: Unlock solutions to common problems.

Цель справочника — краткий обзор назначения и возможностей каждого хука. После описания хука приводится код примера его использования и песочница для ваших экспериментов.

Полный набор хуков доступен в этом репозитории.

  1. Скачиваем репозиторий
  2. Устанавливаем зависимости: npm i
  3. Запускаем: npm start

Хуки находятся в директории «hooks». Основной файл — index.js. Для того, чтобы выполнить конкретный хук, необходимо раскомментировать соответствующие строки импорта и рендеринга.

Без дальнейших предисловий.

useState


useState позволяет работать с состоянием переменных внутри функционального компонента.

Состояние переменной

Для определения состояния переменной необходимо вызвать useState с начальным состоянием в качестве аргумента: useState(initialValue).

const DeclareState = () => {
  const [count] = useState(1);
  return <div>Состояние переменной - {count}.</div>;
};

Обновление состояния переменной

Для обновления состояния переменной необходимо вызвать функцию обновления возвращаемую useState: const [state, updater] = useState(initialValue).

Код:

const UpdateState = () => {
  const [age, setAge] = useState(19);
  const handleClick = () => setAge(age + 1);

  return (
    <>
      <p>Мне {age} лет.</p>
      <button onClick={handleClick}>Стать старше!</button>
    </>
  );
};

Песочница:

Несколько состояний переменных

В одном функциональном компоненте можно определять и обновлять состояния нескольких переменных.

Код:

const MultStates = () => {
  const [age, setAge] = useState(19);
  const [num, setNum] = useState(1);

  const handleAge = () => setAge(age + 1);
  const handleNum = () => setNum(num + 1);

  return (
    <>
      <p>Мне {age} лет.</p>
      <p>У меня {num} братьев и сестер.</p>
      <button onClick={handleAge}>Стать старше!</button>
      <button onClick={handleNum}>Больше братьев и сестер!</button>
    </>
  );
};

Песочница:

Использование объекта для определения состояния переменной

Кроме строк и чисел, в качестве начального значения могут использоваться объекты. Обратите внимание, что useStateUpdater необходимо передавать объект целиком, поскольку он заменяется, а не объединяется с предыдущим.

// setState (объединение объектов) - useState (замена объекта)
// предположим, что начальное состояние - {name: "Igor"}

setState({ age: 30 });
// новым состоянием будет
// {name: "Igor", age: 30} - объекты соединились

useStateUpdater({ age: 30 });
// новым состояние будет
// {age: 30} - объект был заменен

Код:

const StateObject = () => {
  const [state, setState] = useState({ age: 19, num: 1 });
  const handleClick = (val) =>
    setState({
      ...state,
      [val]: state[val] + 1,
    });
  const { age, num } = state;

  return (
    <>
      <p>Мне {age} лет.</p>
      <p>У меня {num} братьев и сестер.</p>
      <button onClick={() => handleClick('age')}>Стать старше!</button>
      <button onClick={() => handleClick('num')}>Больше братьев и сестер!</button>
    </>
  );

Песочница:

Инициализация состояния переменной с помощью функции

Начальное значение состояния переменной может определяться функцией.

const StateFun = () => {
  const [token] = useState(() => {
    const token = localStorage.getItem("token");
    return token || "default-token";
  });

  return <div>Токен - {token}</div>;
};

Функция вместо setState

Функция обновления, возвращаемая useState, может быть не только setState.

const [value, updateValue] = useState(0);
// оба способа вызова функции, указанные ниже, являются валидными
updateValue(1);
updateValue((prevVal) => prevVal + 1);

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

Код:

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

  return (
    <>
      <p>Значение счетчика равно {count}.</p>
      <button onClick={() => setCount(0)}>Сбросить</button>
      <button onClick={() => setCount((prevVal) => prevVal + 1)}>
        Плюс (+)
      </button>
      <button onClick={() => setCount((prevVal) => prevVal - 1)}>
        Минус (-)
      </button>
    </>
  );
};

Песочница:

useEffect


useEffect принимает функцию, отвечающую за дополнительные (побочные) эффекты.

Базовое использование

Код:

const BasicEffect = () => {
  const [age, setAge] = useState(19);
  const handleClick = () => setAge(age + 1);

  useEffect(() => {
    document.title = `Тебе ${age} лет!`;
  });

  return (
    <>
      <p>Обратите внимание на заголовок текущей вкладки браузера.</p>
      <button onClick={handleClick}>Обновить заголовок!</button>
    </>
  );
};

Песочница:

Удаление (отмена) эффекта

Распространенной практикой является удаление эффекта через некоторое время. Это можно сделать с помощью функции, возвращаемой эффектом, переданным useEffect. Ниже представлен пример с addEventListener.

Код:

const CleanupEffect = () => {
  useEffect(() => {
    const clicked = () => console.log("Клик!");
    window.addEventListener("click", clicked);

    return () => {
      window.removeEventListener("click", clicked);
    };
  }, []);

  return (
    <>
      <p>После клика по области просмотра в консоли появится сообщение.</p>
    </>
  );
};

Песочница:

Несколько эффектов

В функциональном компоненте может использоваться несколько useEffect.

Код:

const MultEffects = () => {
  // эффект номер раз
  useEffect(() => {
    const clicked = () => console.log("Клик!");
    window.addEventListener("click", clicked);

    return () => {
      window.removeEventListener("click", clicked);
    };
  }, []);

  // эффект номер два
  useEffect(() => {
    console.log("Второй эффект.");
  });

  return (
    <>
      <p>Загляните в консоль.</p>
    </>
  );
};

Песочница:


Обратите внимание, что вызов useEffect при повторном рендеринге может быть пропущен посредством передачи ему пустого массива в качестве второго аргумента.

Зависимости эффекта

Код:

const EffectDependency = () => {
  const [randomInt, setRandomInt] = useState(0);
  const [effectLogs, setEffectLogs] = useState([]);
  const [count, setCount] = useState(1)

  useEffect(() => {
    setEffectLogs((prevEffectLogs) => [
      ...prevEffectLogs,
      `Вызов функции номер ${count}.`,
    ]);
    setCount(count + 1)
  }, [randomInt]);

  return (
    <>
      <h3>{randomInt}</h3>
      <button onClick={() => setRandomInt(~~(Math.random() * 10))}>
        Получить случайное целое число!
      </button>
      <ul>
        {effectLogs.map((effect, i) => (
          <li key={i}>{"  ".repeat(i) + effect}</li>
        ))}
      </ul>
    </>
  );
};

Песочница:


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

Пропуск эффекта (зависимость в виде пустого массив)

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

Код:

const SkipEffect = () => {
  const [randomInt, setRandomInt] = useState(0);
  const [effectLogs, setEffectLogs] = useState([]);
  const [count, setCount] = useState(1);

  useEffect(() => {
    setEffectLogs((prevEffectLogs) => [
      ...prevEffectLogs,
      `Вызов функции номер ${count}.`,
    ]);
    setCount(count + 1);
  }, []);

  return (
    <>
      <h3>{randomInt}</h3>
      <button onClick={() => setRandomInt(~~(Math.random() * 10))}>
        Получить случайное целое число!
      </button>
      <ul>
        {effectLogs.map((effect, i) => (
          <li key={i}>{"  ".repeat(i) + effect}</li>
        ))}
      </ul>
    </>
  );
};

Песочница:


При нажатии кнопки useEffect не вызывается.

Пропуск эффекта (отсутствие зависимостей)

При отсутствии массива зависимостей эффект будет срабатывать при каждом рендеринге страницы.

useEffect(() => {
  console.log(
    "Данное сообщение будет выводится в консоль при каждом рендеринге."
  );
});

useContext


useContext избавляет от необходимости полагаться на потребителя контекста. Он имеет более простой интерфейс по сравнению с MyContext.Consumer и рендеринг пропсов. Ниже представлено сравнение использования контекста с помощью useContext и Context.Consumer.

// пример использования объекта Context
const ThemeContext = React.createContext("dark")

// использование потребителя контекста
function Button() {
    return (
        <ThemeContext.Consumer>
            {theme => <button className={thene}>Восхитительная кнопка!</button>}
        </ThemeContext.Consumer>
}

// использование useContext
import { useContext } from "react"

function ButtonHook() {
    const theme = useContext(ThemeContext)
    return <button className={theme}>Восхитительная кнопка!</button>
}

Код:

const ChangeTheme = () => {
  const [mode, setMode] = useState("light");

  const handleClick = () => {
    setMode(mode === "light" ? "dark" : "light");
  };

  const ThemeContext = React.createContext(mode);

  const theme = useContext(ThemeContext);

  return (
    <div
      style={{
        background: theme === "light" ? "#eee" : "#222",
        color: theme === "light" ? "#222" : "#eee",
        display: "grid",
        placeItems: "center",
        minWidth: "320px",
        minHeight: "320px",
        borderRadius: "4px",
      }}
    >
      <p>Выбранная тема: {theme}.</p>
      <button onClick={handleClick}>Поменять тему оформления</button>
    </div>
  );
};

Песочница:

useLayoutEffect


Поведение useLayoutEffect аналогично поведению useEffect, за некоторым исключением, о котором мы поговорим позже.

  useLayoutEffect(() => {
    // код
  }, [зависимости]);

Базовое использование

Вот пример использования useEffect, но с useLayoutEffect.

Код:

  const [randomInt, setRandomInt] = useState(0);
  const [effectLogs, setEffectLogs] = useState([]);
  const [count, setCount] = useState(1);

  useLayoutEffect(() => {
    setEffectLogs((prevEffectLogs) => [
      ...prevEffectLogs,
      `Вызов функции номер ${count}.`,
    ]);
    setCount(count + 1);
  }, [randomInt]);

  return (
    <>
      <h3>{randomInt}</h3>
      <button onClick={() => setRandomInt(~~(Math.random() * 10))}>
        Получить случайное целое число!
      </button>
      <ul>
        {effectLogs.map((effect, i) => (
          <li key={i}>{"  ".repeat(i) + effect}</li>
        ))}
      </ul>
    </>
  );
};

Песочница:

useLayoutEffect и useEffect

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

useReducer


useReducer может использоваться как альтернатива useState, однако, его назначение состоит в инкапсуляции сложной логики работы с состояниями, когда состояние зависит от предыдущего значения или имеется несколько состояний.

Базовое использование

В представленном ниже примере вместо useState используется useReducer. Вызов useReducer возвращает значение состояния и функцию dispatch.

Код:

const initialState = { width: 30 };

const reducer = (state, action) => {
  switch (action) {
    case "plus":
      return { width: Math.min(state.width + 30, 600) };
    case "minus":
      return { width: Math.max(state.width - 30, 30) };
    default:
      throw new Error("Что происходит?");
  }
};

const BasicReducer = () => {
  const [state, dispath] = useReducer(reducer, initialState);
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;

  return (
    <>
      <div
        style={{
          margin: "0 auto",
          background: color,
          height: "100px",
          width: state.width,
        }}
      ></div>
      <button onClick={() => dispath("plus")}>
        Увеличить ширину контейнера.
      </button>
      <button onClick={() => dispath("minus")}>
        Уменьшить ширину контейнера.
      </button>
    </>
  );
};

Песочница:

Отложенная («ленивая») инициализация состояния

useReducer принимает третий опциональный аргумент — функцию, возвращающую объект состояния. Данная функция вызывается с initialState в качестве второго аргумента.

Код:

const initializeState = () => ({
  width: 90,
});

// обратите внимание, как initializeState перезаписывает начальное значение
const initialState = { width: 0 };

const reducer = (state, action) => {
  switch (action) {
    case "plus":
      return { width: Math.min(state.width + 30, 600) };
    case "minus":
      return { width: Math.max(state.width - 30, 30) };
    default:
      throw new Error("Что происходит?");
  }
};

const LazyState = () => {
  const [state, dispath] = useReducer(reducer, initialState, initializeState);
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;

  return (
    <>
      <div
        style={{
          margin: "0 auto",
          background: color,
          height: "100px",
          width: state.width,
        }}
      ></div>
      <button onClick={() => dispath("plus")}>
        Увеличить ширину контейнера.
      </button>
      <button onClick={() => dispath("minus")}>
        Уменьшить ширину контейнера.
      </button>
    </>
  );
};

Песочница:

Имитация поведения this.setState

useReducer использует не такой строгий редуктор, как Redux. Например, второй аргумент, перадаваемый редуктору, не нуждается в свойстве type. Это предоставляет в наше распоряжение интресные возможности.

Код:

const initialState = { width: 30 };

const reducer = (state, newState) => ({
  ...state,
  width: newState.width,
});

const NewState = () => {
  const [state, setState] = useReducer(reducer, initialState);
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;

  return (
    <>
      <div
        style={{
          margin: "0 auto",
          background: color,
          height: "100px",
          width: state.width,
        }}
      ></div>
      <button onClick={() => setState({ width: 300 })}>
        Увеличить ширину контейнера.
      </button>
      <button onClick={() => setState({ width: 30 })}>
        Уменьшить ширину контейнера.
      </button>
    </>
  );
};

Песочница:

useCallback


useCallback возвращает сохраненный (кэшированный) колбэк.

Стартовый шаблон

Код:

const CallbackTemplate = () => {
  const [age, setAge] = useState(19);
  const handleClick = () => setAge(age < 100 ? age + 1 : age);
  const someValue = "some value";
  const doSomething = () => someValue;

  return (
    <>
      <Age age={age} handleClick={handleClick} />
      <Guide doSomething={doSomething} />
    </>
  );
};

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <p>Мне {age} лет.</p>
      <p>Нажми на кнопку </p>
      <button onClick={handleClick}>Стать старше!</button>
    </div>
  );
};

const Guide = React.memo((props) => {
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;
  return (
    <div style={{ background: color, padding: ".4rem" }}>
      <p style={{ color: color, filter: "invert()" }}>
        Следуй инструкциям максимально точно.
      </p>
    </div>
  );
});

Песочница:


В приведенном примере компонент Age обновляется и повторно отрисовывается при нажатии кнопки. Компонент Guide также рендерится повторно, поскольку пропсу doSomething передается новый колбэк. Несмотря на то, что в Guide используется React.memo для оптимизации производительности, он все равно перерисовывается. Как мы можем это исправить?

Базовое использование

Код:

const BasicCallback = () => {
  const [age, setAge] = useState(19);
  const handleClick = () => setAge(age < 100 ? age + 1 : age);
  const someValue = "some value";
  const doSomething = useCallback(() => someValue, [someValue]);

  return (
    <>
      <Age age={age} handleClick={handleClick} />
      <Guide doSomething={doSomething} />
    </>
  );
};

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <p>Мне {age} лет.</p>
      <p>Нажми на кнопку </p>
      <button onClick={handleClick}>Стать старше!</button>
    </div>
  );
};

const Guide = React.memo((props) => {
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;
  return (
    <div style={{ background: color, padding: ".4rem" }}>
      <p style={{ color: color, filter: "invert()" }}>
        Следуй инструкциям максимально точно.
      </p>
    </div>
  );
});

Песочница:


Встроенный useCallback

useCallback может использоваться как встроенная функция.

Код:

const InlineCallback = () => {
  const [age, setAge] = useState(19);
  const handleClick = () => setAge(age < 100 ? age + 1 : age);
  const someValue = "some value";

  return (
    <>
      <Age age={age} handleClick={handleClick} />
      <Guide doSomething={useCallback(() => someValue, [someValue])} />
    </>
  );
};

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <p>Мне {age} лет.</p>
      <p>Нажми на кнопку </p>
      <button onClick={handleClick}>Стать старше!</button>
    </div>
  );
};

const Guide = React.memo((props) => {
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;
  return (
    <div style={{ background: color, padding: ".4rem" }}>
      <p style={{ color: color, filter: "invert()" }}>
        Следуй инструкциям максимально точно.
      </p>
    </div>
  );
});

Песочница:

useMemo


useMemo возвращает сохраненное (кэшированное) значение.

Стартовый шаблон

Код:

const MemoTemplate = () => {
  const [age, setAge] = useState(19);
  const handleClick = () => setAge(age < 100 ? age + 1 : age);
  const someValue = { value: "some value" };
  const doSomething = () => someValue;

  return (
    <>
      <Age age={age} handleClick={handleClick} />
      <Guide doSomething={doSomething} />
    </>
  );
};

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <p>Мне {age} лет.</p>
      <p>Нажми на кнопку </p>
      <button onClick={handleClick}>Стать старше!</button>
    </div>
  );
};

const Guide = React.memo((props) => {
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;
  return (
    <div style={{ background: color, padding: ".4rem" }}>
      <p style={{ color: color, filter: "invert()" }}>
        Следуй инструкциям максимально точно.
      </p>
    </div>
  );
});

Песочница:

Данный шаблон идентичен стартовому шаблону useCallback, за исключением того, что someValue является объектом, а не строкой. Компонент Guide также повторно отрисовывается, несмотря на использование React.memo.

Но почему так происходит? Ведь объекты сравниваются по ссылкам, а ссылка на someValue меняется при каждом рендеринге. Есть идеи?

Базовое использование

Значение, возвращаемое doSomething, может быть сохранено с помощью useMemo. Это предотвратить ненужный рендеринг.

Код:

const BasicMemo = () => {
  const [age, setAge] = useState(19);
  const handleClick = () => setAge(age < 100 ? age + 1 : age);
  const someValue = () => ({ value: "some value" });
  const doSomething = useMemo(() => someValue, []);

  return (
    <>
      <Age age={age} handleClick={handleClick} />
      <Guide doSomething={doSomething} />
    </>
  );
};

const Age = ({ age, handleClick }) => {
  return (
    <div>
      <p>Мне {age} лет.</p>
      <p>Нажми на кнопку </p>
      <button onClick={handleClick}>Стать старше!</button>
    </div>
  );
};

const Guide = React.memo((props) => {
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;
  return (
    <div style={{ background: color, padding: ".4rem" }}>
      <p style={{ color: color, filter: "invert()" }}>
        Следуй инструкциям максимально точно.
      </p>
    </div>
  );
});

Песочница:

useRef

useRef возвращает объект ref. Значения этого объекта доступны через свойство «current». Данному свойству может присваиваться начальное значение: useRef(initialValue). Объект ref существует на протяжении жизненного цикла компонента.

Получение доступа к DOM

Код:

const DomAccess = () => {
  const textareaEl = useRef(null);
  const handleClick = () => {
    textareaEl.current.value =
      "Ты - человек, в данный момент изучающий хуки. Да, кэп снова в деле!";
    textareaEl.current.focus();
  };
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;

  return (
    <>
      <button onClick={handleClick}>
        Получить сообщение.
      </button>
      <label htmlFor="msg">
        После нажатия кнопки в поле для ввода текста появится сообщение.
      </label>
      <textarea ref={textareaEl} id="msg" />
    </>
  );
};

Песочница:

Переменные, похожие на экземпляры (дженерики)

Объект ref может содержать любое значение, а не только указатель на DOM-элемент.

Код:

const StringVal = () => {
  const textareaEl = useRef(null);
  const stringVal = useRef(
    "Ты - человек, в данный момент изучающий хуки. Да, кэп снова в деле!"
  );
  const handleClick = () => {
    textareaEl.current.value = stringVal.current;
    textareaEl.current.focus();
  };
  const color = `#${((Math.random() * 0xfff) << 0).toString(16)}`;

  return (
    <>
      <button onClick={handleClick}>
        Получить сообщение.
      </button>
      <label htmlFor="msg">
      После нажатия кнопки в поле для ввода текста появится сообщение.
      </label>
      <textarea ref={textareaEl} id="msg" />
    </>
  );
};

Песочница:

useRef можно использовать для хранения идентификатора таймера с целью его последующей остановки.

Код:

const IntervalRef = () => {
  const [time, setTime] = useState(0);
  const setIntervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      setTime((time) => (time = new Date().toLocaleTimeString()));
    }, 1000);

    setIntervalRef.current = id;

    return () => clearInterval(setIntervalRef.current);
  }, [time]);

  return (
    <>
      <p>Текущее время:</p>
      <time>{time}</time>
    </>
  );
};

Песочница:

Надеюсь статья статья вам понравилась. Благодарю за внимание.