23 - Как вынести код в свой кастомный React Хук

Оценить качество материала и подачу материала автором видео:

Front-end

Трудоустройтесь middle front-end разработчиком на React JS (TypeScript) за 12-16 месяцев обучения с ежедневной менторской поддержкой в формате видео 1 на 1 и коммерческими проектами в портфолио

Перейти на курс
Front-end

Back-end

Трудоустройтесь middle back-end разработчиком за 12-16 месяцев обучения с ежедневной менторской поддержкой в формате видео 1 на 1 и коммерческими проектами в портфолио

Перейти на курс
Back-end

Карьерный бустер

Получите коммерческий опыт на реальных стартапах, прокачайте tech & soft навыки, научитесь работать в команде, проходить собеседования и получите первую работу в IT!

Перейти на курс
Карьерный бустер

Основы Front-end

Сделайте первый шаг в IT, освоив базовые знания разработки и научившись создавать небольшие проекты на JavaScript

Перейти на курс
Основы Front-end

Основы Back-end

Сделайте первый шаг в IT, освоив базовые знания разработки. Без опыта. Без математики. Только практика: JavaScript, SQL, Node JS, база данных

Перейти на курс
Основы Back-end

Концепция кастомных хуков в React

Автор конспекта: Елизавета Савинова

В этом уроке разбирается структура данных «стек» и объясняется, как устроены кастомные хуки в React на простом примере. Кроме того, идет попытка "изобрести" упрощённую версию React с нуля — чтобы понять, как он работает внутри.

Что такое структура данных?

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

Разные алгоритмы требуют разные структуры данных. В этом смысле структура данных как "коробочка" для элементов — с определёнными правилами, как туда и оттуда брать данные.

Рассмотрим примеры структур данных:

  • Массив — место, где данные хранятся в определённом порядке и к ним можно обратиться по индексу.
  • Очередь (FIFO — первый пришёл, первый ушёл) — данные обрабатываются строго в порядке поступления, как очередь в магазине.
  • Стек (LIFO — последний пришёл, первый ушёл) — данные кладутся и забираются с одного конца, пример — стопка тарелок, верхняя тарелка будет взята первой.

Что такое стек и как он связан с React

В JavaScript и React стек часто используется для отслеживания вызовов функций — call stack.

При вызове функций они "складываются" в стек: первая функция лежит внизу, последняя — сверху.

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

Как устроен React (упрощённо)

  • Представим React как объект с методом render — он получает компонент (функцию), вызывает его, получает HTML-строку и вставляет её в DOM.
  • Компоненты в React — это функции, которые возвращают разметку (JSX или, в упрощённом случае, строку HTML).
  • Вызовы функций компонентов складываются в стек вызовов, что помогает React понять, где именно какой хук используется.
html
<!doctype html>
<html lang="en">
  <body>
    <div id="root"></div>
 
    <script>
      const React = {
        render: (component) => {
          const htmlString = component()
          document.getElementById("root").innerHTML = htmlString
        },
      }
 
      function Playlists() {
        return "I am Playlists"
      }
 
      React.render(Playlists)
    </script>
  </body>
</html>

Это HTML файл, который содержит простую реализацию React-подобного рендеринга. В коде создается объект React с методом render, который принимает компонент-функцию, вызывает её и вставляет результат в элемент с id="root". Также определена функция-компонент Playlists, которая возвращает строку "I am Playlists".

Как работают хуки и кастомные хуки?

  1. useState — базовый хук: Позволяет компоненту хранить состояние — например, число, строку. React связывает это состояние с конкретным компонентом через стек вызовов, зная точное место вызова хука.
html
<!doctype html>
<html lang="en">
  <body>
    <div id="root"></div>
 
    <script>
      const React = {
        render: (component) => {
          const htmlString = component()
          document.getElementById("root").innerHTML = htmlString
        },
      }
 
      function useState(initValue) {
        // react save this value for component
        return [initValue, () => {}]
      }
 
      function Playlists() {
        const [value] = useState(10)
 
        return "I am Playlists: " + value
      }
 
      React.render(Playlists)
    </script>
  </body>
</html>

Основные изменения:

  • Добавлена функция useState, которая принимает начальное значение и возвращает массив с этим значением и пустой функцией
  • В компоненте Playlists теперь используется useState(10), из которого деструктурируется value
  • Компонент возвращает строку с конкатенацией значения: 'I am Playlists: ' + value

Это простая имитация React Hook useState, хотя пока без функциональности изменения состояния.

  1. Кастомный хук:
  • Это функция, имя которой начинается с use, и которая внутри может вызывать другие хуки (например, useState, useEffect).
  • Кастомные хуки не "живут" отдельно — они всегда вызываются внутри компонента, фактически добавляя дополнительные "слои" в стек вызовов.
  • React при этом правильно связывает состояние и эффекты с конкретным компонентом, даже если хук вызван внутри кастомного хука.
html
<!doctype html>
<html lang="en">
  <body>
    <div id="root"></div>
 
    <script>
      const React = {
        render: (component) => {
          const htmlString = component()
          document.getElementById("root").innerHTML = htmlString
        },
      }
 
      function useCustomHook() {
        const [value] = useState(10)
 
        return value
      }
 
      function useState(initValue) {
        fiberNode.state = initValue
        // react save this value for component
        return [initValue, () => {}]
      }
 
      function Playlists() {
        const value = useCustomHook()
 
        return "I am Playlists: " + value
      }
 
      React.render(Playlists)
    </script>
  </body>
</html>
  • Добавлена новая функция useCustomHook(), которая использует useState(10) внутри себя и возвращает value
  • В функции useState появилась строка fiberNode.state = initValue; (хотя fiberNode не определен в коде)
  • Компонент Playlists теперь использует useCustomHook() вместо прямого вызова useState

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

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

Пример с двумя счётчиками:

Создаём два компонента, использующих один и тот же кастомный хук с useState: каждый будет иметь своё независимое состояние.

tsx
import { useState } from "react"
 
function App() {
  return (
    <>
      <Counter />
      <Counter />
    </>
  )
}
 
function Counter() {
  const [value, setValue] = useState(0)
  return <div onClick={() => setValue(value + 1)}>{value}</div>
}
 
export default App
  1. Импортируется хук useState из React
  2. Компонент App рендерит два компонента Counter
  3. Компонент Counter использует useState для управления состоянием
  4. При клике на div значение увеличивается на 1
  5. Каждый экземпляр Counter будет иметь своё независимое состояние

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

React не смешивает состояния, несмотря на одинаковую функцию компонента, потому что для каждого вызова создаётся своя ячейка в памяти (FiberNode).

tsx
import { useState } from "react"
 
function App() {
  return (
    <>
      <Counter />
      <Age />
    </>
  )
}
 
function useCounter(initValue: number) {
  const [value, setValue] = useState(initValue)
  return { value, setValue }
}
 
function Counter() {
  const { value, setValue } = useCounter(0)
  return <div onClick={() => setValue(value + 1)}>{value}</div>
}
 
function Age() {
  const { value, setValue } = useCounter(0)
  return (
    <div>
      age: {value} <button onClick={() => setValue(value + 1)}>+</button>
    </div>
  )
}
 
export default App

Custom hook useCounter инкапсулирует логику управления состоянием, компоненты Counter и Age теперь переиспользуют эту логику вместо дублирования useState. Это делает код более модульным и легче масштабируется при добавлении новых счётчиков.

Вынесем больше логики в наш кастомный хук:

tsx
import { useState, useEffect } from "react"
 
function App() {
  return (
    <>
      <Counter />
      <Age />
    </>
  )
}
 
function useCounter(initValue: number, ms: number) {
  const [value, setValue] = useState(initValue)
 
  useEffect(() => {
    setInterval(() => {
      setValue(initValue)
    }, ms)
  }, [])
 
  return { value, inc: () => setValue(value + 1) }
}
 
function Counter() {
  const { value, inc } = useCounter(10, 4000)
  return <div onClick={() => inc()}>{value}</div>
}
 
function Age() {
  const { value, inc } = useCounter(2, 10000)
  return (
    <div>
      age: {value} <button onClick={() => inc()}>+</button>
    </div>
  )
}
 
export default App

Новая версия имеет:

  1. Custom hook useCounter теперь принимает ms (миллисекунды) для автоматического сброса значения по таймеру
  2. Добавлен useEffect с setInterval - значение автоматически сбрасывается через указанный интервал
  3. Функция inc() для увеличения значения вместо прямого setValue
  4. Компоненты используют разные параметры: Counter(10, 4000) и Age(2, 10000)

Почему это важно?

Понимание того, как React "смотрит" на вызовы хуков через стек вызовов и хранит состояние для каждого компонента — ключ к правильному использованию хуков.

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

Такой взгляд углубляет понимание работы React, а не просто помогает использовать его как "чёрный ящик".


🏠 Домашнее задание

Цель задания: Научиться создавать и работать с кастомными хуками

Задание 1

Описание задачи

У тебя есть готовый компонент LightSwitch с логикой переключения. Твоя задача - создать кастомный хук useToggle и вынести в него логику управления состоянием.

Исходный код
TogglePage.tsx
import { useState } from "react"
 
export const LightSwitch = () => {
  const [isOn, setIsOn] = useState(false)
 
  const toggle = () => {
    setIsOn(!isOn)
  }
 
  return (
    <div>
      <h2>{isOn ? "💡 Свет включен" : "🌙 Свет выключен"}</h2>
      <button onClick={toggle}>Переключить свет</button>
    </div>
  )
}
 
export const TogglePage = () => {
  return (
    <div>
      <LightSwitch />
    </div>
  )
}
Что нужно сделать
  1. Создай кастомный хук useToggle, который:
  • Принимает начальное значение initialValue типа boolean
  • Возвращает объект с двумя свойствами: isOn и toggle
  1. Перенеси логику из компонента LightSwitch в хук useToggle

  2. Используй созданный хук в компоненте LightSwitch

Итоговый результат 🚀 res23-0


Задание 2

Описание задачи

Теперь расширь функциональность хука useToggle и используй его в нескольких компонентах.

1. Добавь в хук setIsOn

Хук useToggle теперь должен возвращать объект с тремя свойствами:

  • isOn - текущее булево состояние
  • toggle - функция для переключения состояния
  • setIsOn - функция для установки конкретного значения
2. Создай компонент VisibilityToggle

Компонент должен:

  • Использовать хук useToggle с начальным значением false
  • Показывать/скрывать секретное сообщение "🎉 Это секретное сообщение!"
  • Иметь две кнопки: "Показать" и "Скрыть"
3. Добавь компонент на страницу

Добавь VisibilityToggle в компонент TogglePage рядом с LightSwitch.

Пример структуры
TogglePage.tsx
function useToggle(initialValue: boolean) {
  // Добавь setIsOn в возвращаемый объект
}
 
export const VisibilityToggle = () => {
  // Твой код здесь
 
  return (
    <div>
      <h2>Секретное сообщение</h2>
      {/*...*/}
    </div>
  )
}
 
export const TogglePage = () => {
  return (
    <div>
      <LightSwitch />
      <VisibilityToggle />
    </div>
  )
}

Итоговый результат 🚀 res23-1


Задание 3

Описание задачи

Дорабатай хук useToggle, добавив функцию сброса состояния к начальному значению.

1. Добавь метод reset в хук useToggle

Хук теперь должен возвращать объект с четырьмя свойствами:

  • isOn - текущее булево состояние
  • toggle - функция для переключения состояния
  • setIsOn - функция для установки конкретного значения
  • reset - функция для сброса состояния к начальному значению
2. Создай компонент NotificationSwitch

Компонент должен:

  • Использовать хук useToggle с начальным значением true
  • Отображать текст "🔔 Уведомления включены" или "🔕 Уведомления выключены"
  • Иметь три кнопки:
  • "Переключить" - переключает состояние
  • "Включить" - устанавливает true
  • "Сбросить по умолчанию" - возвращает к начальному состоянию
3. Добавь компонент на страницу

Добавь NotificationSwitch в компонент TogglePage.

Пример структуры
TogglePage.tsx
function useToggle(initialValue: boolean) {
  // Добавь метод reset
}
 
export const NotificationSwitch = () => {
  // Твой код здесь
 
  return (
    <div>
      <h2>Настройки уведомлений</h2>
      {/* Добавь текст и кнопки */}
    </div>
  )
}
 
export const TogglePage = () => {
  return (
    <div>
      <LightSwitch />
      <VisibilityToggle />
      <NotificationSwitch />
    </div>
  )
}

Итоговый результат 🚀 res23-2


Задание 4

Описание задачи

Создай кастомный хук useText для работы с текстовыми строками. Этот хук должен управлять текстом и предоставлять методы для его изменения.

1. Создай кастомный хук useText

Хук должен:

  1. Принимать один параметр:
  • initialText - начальное значение текста (по умолчанию пустая строка "")
  1. Возвращать объект с пятью свойствами:
  • text - текущий текст
  • setText - функция для установки нового текста
  • clear - функция для очистки текста (устанавливает пустую строку)
  • toUpperCase - функция для преобразования текста в ВЕРХНИЙ РЕГИСТР
  • toLowerCase - функция для преобразования текста в нижний регистр
2. Создай компонент TitleEditor

Компонент должен:

  1. Использовать хук useText с начальным значением "Заголовок статьи"
  2. Отображать текущий текст в заголовке <h2>
  3. Иметь четыре кнопки:
  • "ВЕРХНИЙ РЕГИСТР" - преобразует текст
  • "нижний регистр" - преобразует текст
  • "Изменить на 'Новый заголовок'" - устанавливает новый текст
  • "Очистить" - очищает текст
3. Создай компонент GreetingCard

Компонент должен:

  1. Использовать хук useText с начальным значением "Привет!"
  2. Отображать текст внутри div с эмодзи: "💬 {text}"
  3. Иметь четыре кнопки:
  • "ГРОМКО" - преобразует в верхний регистр
  • "тихо" - преобразует в нижний регистр
  • "Сказать 'Добро пожаловать!'" - устанавливает новый текст
  • "Молчать" - очищает текст
4. Создай страницу TextPage

Компонент TextPage должен отображать оба компонента.

Подсказки
  • Используй useState для хранения текущего текста
  • Методы toUpperCase и toLowerCase - это встроенные методы строк в JavaScript
  • Все функции должны изменять состояние через setState
Пример структуры
TextPage.tsx
import { useState } from "react"
 
function useText(initialText: string = "") {
  // Твой код здесь
 
  return { text, setText, clear, toUpperCase, toLowerCase }
}
 
export const TitleEditor = () => {
  // Твой код здесь
 
  return (
    <div>
      <h2>{text || "Пусто"}</h2>
      {/* Добавь кнопки */}
    </div>
  )
}
 
export const GreetingCard = () => {
  // Твой код здесь
 
  return (
    <div>
      <h2>💬 {text || "..."}</h2>
      {/* Добавь кнопки */}
    </div>
  )
}
 
export const TextPage = () => {
  return (
    <div>
      <TitleEditor />
      <GreetingCard />
    </div>
  )
}

Итоговый результат 🚀 res23-4


💡

🔶 Помни: кастомные хуки — это не усложнение, а упрощение будущей жизни. Когда логика повторяется в третий раз (а она повторится), ты правишь код в одном месте, а не ищешь одинаковые useState по всему проекту. Сегодняшние 10 минут на создание хука экономят завтрашние часы копипасты.

🔶 Код-ревью скажет спасибо. Когда ты пишешь useToggle вместо копирования 10 строк логики в каждый компонент, твой код становится читаемым как книга. Коллега откроет файл и сразу поймёт: "Ага, здесь используется переключатель". Не нужно разбираться в деталях — название хука всё объясняет. 📖

Боевой маршрут (React Путь Самурая: без альтернатив)

Видеоурок - 25 видео из 30