24 - Стейт менеджер на хуках / Business Logic Layer

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

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

🏗️ Стейт менеджер на хуках / Business Logic Layer

Автор конспекта: Стогниева Виктория

🎯 Введение: Проблема "толстых" компонентов

В начале пути React-разработчика часто возникает ситуация, когда один компонент становится слишком большим и сложным. Такой компонент называют "толстым" (fat) — он берет на себя слишком много ответственности:

  1. 🧩 Отрисовка UI (визуализация) — генерирует JSX-разметку, видимую пользователю.
  2. 🔄 Управление состоянием (state management) — хранит и обновляет данные (например, список треков).
  3. 🌐 Выполнение запросов к API (API requests) — взаимодействует с сервером.
💡

React — это лишь UI-библиотека. Она должна эффективно рендерить интерфейс, а не решать все задачи приложения.

📉 Проблема: Смешение задач делает код:

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

⚙️ 1. Фундаментальный принцип: Разделение ответственности (Separation of Concerns)

🧠 Определение: Каждая часть системы должна решать одну четко определённую задачу.

💡 Вместо одного "толстого" компонента мы создаём несколько специализированных модулей, работающих вместе.

🔑 Преимущества:

  • 🎯 Фокусировка внимания — можно сосредоточиться на конкретной задаче.
  • 🧩 Легкий рефакторинг — изменения в API не затрагивают UI.
  • 🔁 Переиспользование логики — один и тот же хук можно применять в разных компонентах.
  • 🧪 Упрощение тестирования — UI и бизнес-логику тестируют отдельно.
💡
Данные первичные. Если с ними всё хорошо — интерфейс отрисуется правильно.

🧱 2. Три слоя архитектуры React-приложения

Иерархия зависимостей:

UI → Бизнес-логика → Доступ к данным

Это обеспечивает устойчивость архитектуры — внутренние изменения не ломают внешний интерфейс.

🎨 2.1. Слой UI (Presentation Layer)

  • 📋 Задача: отображение данных и реакция на действия пользователя.
  • 🧱 Состав: React-компоненты с JSX.
  • Результат: “тонкие” компоненты — не содержат логику, только визуализацию.
TrackList.tsx
 
export function TrackList() {
    const {tracks, refresh} = useTracks()
 
    if (tracks == null) return <div>Loading...</div>
 
    const handleRefreshClick = () => {
        refresh()
    }
 
    return <div>
        <button onClick={handleRefreshClick}>refresh</button>
        <ul>{tracks.map((t) => <li key={t.id}>{t.attributes.title}</li>)}</ul>
    </div>
}
 

⚙️ 2.2. Слой бизнес-логики (Business Logic Layer)

  • 🧠 Задача: управление состоянием и реализация бизнес-правил.
  • 🪄 Реализация: кастомные хуки (useTracks, useCounter и т.д.).
  • 🧩 Альтернативные названия: domain, state layer, application logic layer.
useTracks.tsx
import { useEffect, useState } from "react"
import { getTracksViaAPI } from "../da1/api.tax"
 
export function useTracks() {
  const [tracks, setTracks] = useState<Array<any> | null>(null)
 
  useEffect(() => {
    getTracksViaAPI().then((json: any) => setTracks(json.data))
  }, [])
 
  return {
    tracks: tracks,
    refresh: () => {
      setTracks(null)
      getTracksViaAPI().then((json: any) => setTracks(json.data))
    },
  }
}

🌐 2.3. Слой доступа к данным (Data Access Layer)

  • 🔗 Задача: взаимодействие с сервером (API).
  • 🧱 Состав: отдельные функции или объекты для запросов (getTracksViaAPI).
  • 🕶️ Преимущество: UI не знает о структуре API, токенах и URL — всё инкапсулировано.
api.tsx
export const getTracksViaAPI = () => {
  return fetch("https://musicfun.it-inculator.app/api/1.0/playlists/tracks", {
    headers: {
      "api-key": "df73a86e-20f4-dbfr-8fa7-9c28f8d2e817",
    },
  }).then((res) => res.json())
}

🪄 3. Кастомные хуки: Инструмент бизнес-логики

Кастомный хук — это способ вынести состояние и эффекты из компонентов, сохранив переиспользуемость.

📘 Правила:

  1. Это обычная JS-функция.
  2. Название начинается с use (например, useTracks).
  3. Внутри можно использовать другие React-хуки (useState, useEffect и т.д.).

🧩 Важно: React связывает состояние не с хуком, а с компонентом, который его вызывает. Поэтому два компонента с одним хуком создают независимые состояния.

🔍 Пример:

useTracks:

  • хранит состояние с треками (useState);
  • загружает данные (useEffect);
  • возвращает готовые данные компоненту.

⚖️ Сравнение:

До рефакторингаПосле рефакторинга
useState и useEffect внутри компонентаКомпонент вызывает useTracks()
Ответственность: UI + логика + APIТолько UI
Сложный кодМинимум кода

Компонент стал тонким. Мозг не перегружен деталями. Только UI — это фантастика!

🧩 4. Практический пример: Рефакторинг компонента Counter

🧱 Исходная версия:

Компонент Counter хранит всё внутри себя (useState, onClick).

Counter.tsx
import {useState} from "react";
 
  export const Counter = () => {
    const [count, setCount] = useState(0);
 
    const handleClick = () => {
      setCount(count + 1)
    }
 
    return <button onClick={handleClick}>{count}</button>;
  }

🔧 Этапы рефакторинга:

  1. ✂️ Создаём хук useCounter — переносим useState и increment() внутрь.
  2. 🛡️ Инкапсулируем бизнес-логику — хук возвращает не setCount, а increment().
  3. ⏱️ Добавляем автосброс — в хуке через useEffect и setInterval.
  4. ⚙️ Делаем хук гибким — добавляем параметр startValue.
Counter.tsx
const useCounter = (startValue: number = 0) => {
  const [count, setCount] = useState(startValue)
 
  useEffect(() => {
    setInterval(() => {
      setCount(0)
    }, 3000)
  }, [])
 
  return {
    count,
    inc: () => {
      setCount(count + 1)
    },
  }
}
💡

Теперь Counter занимается только отображением, а бизнес-логикой полностью внутри хука.

Counter.tsx
export const Counter = () => {
    const { count, inc } = useCounter()
 
    return <button onClick={inc}>{count}</button>;
};

🧩 5. Практический пример: Рефакторинг MusicFan

useTracks.tsx
import { useState, useEffect } from "react"
 
export function useTracks() {
  const [tracks, setTracks] = useState<Array<TrackListItemOutput> | null>(null)
 
  useEffect(() => {
    console.log("effect")
    getTracks().then((json) => setTracks(json.data))
  }, [])
 
  return {
    tracks,
  }
}
useTrackSelection.tsx
import { useState } from "react"
 
export function useTrackSelection() {
  const [trackId, setTrackId] = useState<string | null>(null)
 
  return {
    trackId,
    setTrackId,
  }
}
useTrackDetail.tsx
import { useState, useEffect } from "react"
 
export function useTrackDetail(trackId: string | null) {
  const [selectedTrack, setSelectedTrack] = useState<GetTrackDetailsOutputData | null>(null)
 
  useEffect(() => {
    if (!trackId) {
      setSelectedTrack(null)
      return
    }
 
    getTrack(trackId).then((json) => setSelectedTrack(json.data))
  }, [trackId])
 
  return {
    trackDetails,
  }
}

🧭 Заключение: Преимущества правильной архитектуры

  • Снижение когнитивной нагрузки — меньше деталей, легче понимать код.
  • 🚀 Быстрое развитие —добавление фич без ломки архитектуры.
  • 🏢 Готовность к enterprise-проектам — масштабируемая структура.
  • 🤖 Совместимость с AI и командной работой — чистая архитектура понятна как людям, так и помощникам.
💡
Это не учебный пример, а реальная практика production-разработки.

💎 Итог

Хорошая архитектура React-приложения строится на трёх китах:

  1. Разделение ответственности.
  2. Три слоя — UI, бизнес-логика, данные.
  3. Кастомные хуки как инструмент связи между слоями.

Делайте компоненты тонкими, а архитектуру — умной.


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

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

Задание 1

По аналогии как в видео необходимо доработать основное приложение Trelly, над которым мы закончили работать в 22 домашнем задании

1.1. TasksList

  • Создай директорию bll на одном уровне с ui и dal.
  • В bll директории создай файл useTasks.ts
  • Из компонента TasksList.tsx вынеси логику в кастомный хук useTasks.ts
folder structure
src/
├── bll/
   ├── useTasks.ts
├── dal/
   ├── api.ts
├── ui/
   ├── TaskDetails.tsx
├── App.tsx
└── main.tsx
Пример структуры
useTasks.ts
export function useTasks() {
  // Твой код
}
TasksList.tsx
export function TasksList(props: Props) {
  const { tasks } = useTasks()
 
  /*...*/
}

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

1.2. TaskDetails

  • В bll директории создай файл useTaskDetails.ts
  • Из компонента TaskDetails.tsx вынеси логику в кастомный хук useTaskDetails.ts
  • Переименуй selectedTask в taskDetails, чтобы название переменной было актуально и более семантично
Пример структуры
useTaskDetails.ts
export function useTaskDetails(selectedTaskId: string | null, boardId: string | null) {
  const [taskDetails, setTaskDetails] = useState<TaskDetailsData | null>(null)
 
  /*...*/
 
  return { taskDetails }
}
TaskDetails.tsx
export function TaskDetails(props: Props) {
  const { selectedTaskId, boardId } = props
 
  const { taskDetails } = useTaskDetails(selectedTaskId, boardId)
 
  /*...*/
}

Итоговый результат. Вынесли бизнес логику в кастомный хук. При клике на таску подгружаются ее детали 🚀

1.3. MainPage

  • В bll директории создай файл useTaskSelection.ts
  • Из компонента MainPage.tsx вынеси логику в кастомный хук useTaskSelection.ts
Пример структуры
useTaskSelection.ts
export function useTaskSelection() {
  /*...*/
}
MainPage.tsx
export function MainPage() {
  const { selectedTaskId, setSelectedTaskId, boardId, setBoardId } = useTaskSelection()
 
  /*...*/
}

Итоговый результат. Вынесли бизнес логику в кастомный хук. Все работает как и прежде 🚀


Задание 2

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

У тебя есть базовый хук useCounter с автоматическим сбросом. Твоя задача - добавить методы для уменьшения счётчика и сброса к начальному значению.

Исходный код
CounterPage.tsx
import { useEffect, useState } from "react"
 
export const Counter = () => {
  const { count, inc } = useCounter(0)
 
  return <button onClick={inc}>{count}</button>
}
 
const useCounter = (startValue = 0) => {
  const [count, setCount] = useState(startValue)
 
  useEffect(() => {
    setInterval(() => {
      setCount(0)
    }, 7000)
  }, [])
 
  const inc = () => {
    setCount(count + 1)
  }
 
  return { count, inc }
}
Что нужно сделать
  1. Добавь в хук useCounter два новых метода:
  • dec - уменьшает счётчик на 1
  • reset - сбрасывает счётчик к начальному значению startValue
  1. Обнови компонент Counter:
  • Отображай текущее значение счётчика в <h2>
  • Добавь три кнопки: "Увеличить", "Уменьшить", "Сбросить"

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


Задание 3

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

Добавь возможность настраивать шаг увеличения/уменьшения.

Что нужно сделать
  1. Добавь в хук useCounter второй параметр startStep (по умолчанию 1):
  • Методы inc и dec должны изменять счётчик на величину step
  1. Обнови компонент Counter:
  • Добавь еще один useState в котором будет храниться новый шаг
tsx
const [step, setStep] = useState(startStep)
  • Добавь кнопку "Установить шаг 5". При нажатии на кнопку пользователь увидит alert сообщающий о том, что шаг изменился на 5 и после этого счетчик должен изменять значение счетчика с новым шагом
Пример структуры
CounterPage.tsx
const useCounter = (startValue: number = 0, startStep: number = 1) => {
  const [step, setStep] = useState(startStep)
 
  // Твой код здесь
 
  return { count, inc, dec, reset, changeStep }
}
 
export const Counter = () => {
  const { count, inc, dec, reset, setCount, changeStep } = useCounter(0)
 
  return (
    <div>
      <h2>{count}</h2>
      {/* Кнопки: Увеличить, Уменьшить, Сбросить, Установить шаг 5 */}
    </div>
  )
}

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


Задание 4

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

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

Что нужно сделать
  1. Добавь в хук useCounter третий параметр autoResetTime (по умолчанию 0):
  • Если autoResetTime > 0, счётчик автоматически сбрасывается через указанное время в миллисекундах
  • Если autoResetTime === 0 или null, автосброс отключен

autoResetTime должно принимать значение в секундах, а не милисекундах

  1. Создай два компонента для демонстрации:

Компонент Counter:

  • Использует хук с параметрами: startValue: 0, startStep: 1, autoResetTime: 3
  • Отображает текст "⏰ Автосброс через 3 сек"

Компонент CounterWithoutAutoReset:

  • Использует хук с параметрами: startValue: 5, startStep: 5, autoResetTime: 0
  • Отображает текст "🔒 Без автосброса"
  1. Создай страницу CounterPage с обоими компонентами
Подсказки
  • Используй условие внутри useEffect для проверки autoResetTime > 0
  • Только если условие выполняется, запускай setInterval
Пример структуры
CounterPage.tsx
const useCounter = (startValue: number = 0, startStep: number = 1, autoResetTime: number = 0) => {
  // Твой код здесь
 
  useEffect(() => {
    // Запускай setInterval только если autoResetTime > 0
  }, [])
 
  return { count, inc, dec, reset, setCount }
}
 
export const Counter = () => {
  // Твой код здесь
  return (
    <div>
      <h2>{count}</h2>
      <h3>⏰ Автосброс через 3 сек</h3>
      {/* Кнопки */}
    </div>
  )
}
 
export const CounterWithoutAutoReset = () => {
  // Твой код здесь
  return (
    <div>
      <h2>{count}</h2>
      <h3>🔒 Без автосброса</h3>
      {/* Кнопки */}
    </div>
  )
}
 
export const CounterPage = () => {
  return (
    <div>
      <Counter />
      <CounterWithoutAutoReset />
    </div>
  )
}

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


Задание 5 ⭐

⭐ Дополнительное задание со звездочкой. Проделывать по желанию. В дальнейших уроках про это будет рассказано

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

Исправь утечку памяти в хуке useCounter, добавив очистку интервала.

Проблема

Сейчас в useEffect запускается setInterval, но он никогда не останавливается. Это приводит к утечке памяти - интервал продолжает работать даже после размонтирования компонента.

Что нужно сделать
  1. Добавь cleanup function в useEffect
  2. Сохрани результат setInterval в переменную intervalId
  3. Верни функцию из useEffect, которая вызывает clearInterval(intervalId)
Подсказки
  • Cleanup function вызывается автоматически при размонтировании компонента
  • Прочитай про это в документации React: Synchronizing with Effects
💡

Важно: Всегда очищай side-эффекты (интервалы, подписки, таймеры) в cleanup function, чтобы избежать утечек памяти и неожиданного поведения.


💡

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

🔶 Тестирование становится проще. Вместо того чтобы тестировать одну и ту же логику в 5 разных компонентах, ты пишешь тесты один раз — для кастомного хука. Баг нашёлся? Фиксишь в одном месте, и он исчезает везде. Это как чинить водопровод в подвале, а не латать протечки на каждом этаже. 🔧

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

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