🚀 Итоговый обзор курса: повторяем всё
1. 🌐 Что такое Frontend и зачем нам React
1.1. 🎓 Что такое frontend разработка
Итак разберёмся что такое frontend приложение и как оно работает на основании этой диаграммы:

- Пользователь открывает браузер и вводит к примеру в поисковую строку url на котором находится сайт.
- Браузер выполняет запрос за этими данными которые находятся на сервере.
- В ответ на запрос сервер отправляет необходимые файлы, а именно
CSS,HTMLиJavaScript, которые необходимы для того, чтобы отобразить сайт пользователю. - Файлы есть, но чтобы пользователь увидел сам сайт браузер берёт эти файлы и выполняет работу с ними и отображает сайт, то есть запущенное фронтенд приложение.
📌 Frontend-приложение выполняется в браузере, а не на сервере.
Разберём простой пример с использованием чистого HTML
Чтобы запустить этот файл в VS Code вам необходимо использовать дополнительное расширение
Live Server который запустит локальный сервер и вы сможете с помощью его просмотреть этот html
файл на созданном порту к примеру http://127.0.0.1:5500/index.html который будет вам доступен
локально.
Почему этот файл не является frontend приложением - потому что это просто файл который имеет статические данные. Да мы можем использовать умный html элемент который может выполнить такую функцию как проигрывание аудио, но это сложно назвать приложением так как больше чем отображение захардкоженных данных он больше ничего не умеет.
Так что же такое frontend разработка? Прежде чем мы определимся с этим понятием разберём еще одну диаграмму:

HTMLстраница это все лишь текст да там есть свои правила написания и использования элементом, но это лишь TEXT файл который браузер может использовать.- Браузер загружает этот HTML документ и начинает его парсить.
- На основании парсинга браузер создаёт DOM дерево - совокупность JS объектов.
- Дальше браузер имея DOM на основании его может отрисовать UI - визуальную часть которую мы можем видеть.
И вот теперь мы как программисты можем манипулировать этим DOM, то есть JS объектами которые влияют на то что будет отображаться в браузере.
1.2. 🏗️ Работа с DOM c помощью JS
Мы можем изменять DOM динамически с помощью JS, браузер это наблюдает и выполняет перерисовку сайта.
Закомментируем часть элементов внутри index.html, создадим div элемент c id="root" и подключим
js нам для этого нужно создать файл front-end.js и подключить с помощью тега script.
Мы сейчас на нативном JS создадим наше фронтенд-приложение. Первое, что мы с вами должны запомнить на всю жизнь: у нас есть данные и есть алгоритм рендера этих данных. Данные первичны. От данных потом появляется разметка. Поэтому создадим объект с данными:
Далее нам необходим алгоритм отрисовки этих данных и для этого нам сперва нужно создать DOM элементы которые будем отображать. Для этого мы используем API браузера.
Далее нам нужно добавить этот элемент в DOM c помощью div с id="root" и добавить в него этот
элемент.
Создадим треки на основе данных:
Вручную писать код для манипуляции DOM-деревом сложно и неэффективно. Вместо того чтобы
фокусироваться на разработке плеера, приходится писать большое количество вспомогательного кода.
Чтобы избежать прямой работы с DOM, целесообразно использовать библиотеки и фреймворки, такие, как
React, Angular или Vue, которые существенно упрощают эту задачу.
Допишем треки:
Итак рассмотрим что у нас получилось

- У нас был
HTMLфайл который пустой - Браузер его загрузил, а показывать нечего
DOMнебольшой так как нечего визуализировать- Соответственно пользователь ничего не видит

JSподгружается в браузер тоже как текст- Браузер видит что это
JSкод так как подключен с помощью тега script и отдает на интерпретацию движку который есть в браузере, то есть парсится этотJSи выполняется - На основании чего у нас создаются какие-то
DOMузлы, которые создаются отдельно от главного DOM. - Далее нужно эти элементы добавить в главный
DOMи выполнится перерисовка и обновитсяUI.
Мы могли бы продолжить разработку плеера на чистом JavaScript: добавить обработчики событий, реализовать загрузку, создание и удаление треков, вручную управлять состоянием и выполнять запросы. Однако такой подход быстро усложняет код и делает разработку менее комфортной и более затратной по времени, так как большая часть усилий уходит на работу с DOM и вспомогательную логику. Чтобы упростить процесс разработки и сосредоточиться на более важных аспектах приложения, целесообразно использовать готовые фреймворки и библиотеки, которые решают эти задачи эффективнее и избавляют от необходимости реализовывать базовую инфраструктуру вручную.
1.3. 👨🏻🏫 Зачем нужен React
Рассмотрим пример с innerHtml:
При использовании innerHTML мы напрямую не создаём DOM-узлы в JavaScript. В этом случае браузер самостоятельно парсит HTML-строку, создаёт соответствующие DOM-объекты и выполняет отрисовку. Таким образом, парсинг и создание DOM-дерева перекладываются на браузер, а контроль над отдельными элементами теряется. В небольших приложениях это некритично, однако в сложных фронтенд-приложениях с высокой интерактивностью важно иметь явные ссылки на создаваемые элементы для управления состоянием и обработкой событий.
Современные фреймворки, такие как React, решают эту проблему, предоставляя разработчику декларативный API в виде JSX — синтаксиса, похожего на HTML, но являющегося JavaScript. React самостоятельно создаёт и управляет DOM-узлами, не полагаясь на браузерный парсинг строк. JSX компилируется в обычный JavaScript, что позволяет легко работать с событиями, данными и логикой приложения. При изменении состояния React не перерисовывает весь DOM, а точечно обновляет только необходимые узлы, выступая прослойкой между бизнес-логикой и реальным DOM. В этом и заключается ключевая эффективность и практическая ценность React
2. 📚 TypeScript, NodeJS, Vite
2.1. 🎓 TypeScript зачем он нужен
В современной фронтенд-разработке TypeScript используется как надстройка над JavaScript — дополнительный слой, который помогает писать более надёжный, предсказуемый и поддерживаемый код. Он не заменяет JavaScript, а расширяет его возможностями статической типизации и инструментами для контроля структуры данных. На практике разработка крупных веб-приложений без TypeScript сегодня практически невозможна из-за сложности и масштаба таких систем.

На этапе разработки (Compile Time) мы работаем с TypeScript и файлами.ts /.tsx (main.tsx, Playlist.tsx, index.ts). В этот момент TypeScript выполняет проверку типов, помогает определить структуру объектов и выявляет ошибки ещё до запуска приложения. JSX/TSX используется как удобный синтаксис для описания интерфейса внутри JavaScript-кода. После этого происходит компиляция (транспиляция): TypeScript-код преобразуется в обычный JavaScript.
На этапе выполнения (Run Time) браузер получает уже готовые JavaScript-файлы (main.js, Playlist.js, index.js) и выполняет их. Браузер не знает о TypeScript и типах — вся эта логика существует исключительно на этапе разработки и служит инструментом повышения качества и удобства разработки.
2.2. ⚙️ NodeJS
Хотя мы создаем приложения для браузера, нам необходима еще одна платформа — Node.js. Как и Google Chrome, Node.js построена на движке V8, поэтому она отлично «понимает» JavaScript.
Зачем фронтенд-разработчику Node.js? Наше итоговое приложение работает в браузере, но Node.js берёт на себя роль мощного инструмента для управления разработкой. Она помогает автоматизировать рутинные процессы и готовить код к продакшену.

Основные задачи, которые решает Node.js в нашем проекте:
- Управление зависимостями (Dependencies): Подгрузка внешних библиотек (например, React).
- Транспиляция: Преобразование современного кода или TypeScript в чистый JavaScript, который поймет любой браузер.
- Сборка (Bundling): Объединение множества файлов проекта в один или несколько оптимизированных бандлов.
- Минификация: Сжатие кода для уменьшения размера файлов и ускорения загрузки сайта.
- Tree Shaking: Удаление неиспользуемого кода из итоговой сборки.
2.3. 🦾 Vite: Автоматизация сборки и локальный сервер
Настройка всех инструментов сборки (транспиляции, минификации, бандлинга) вручную — задача крайне сложная. Чтобы не тратить на это время, существуют готовые решения. Одним из самых быстрых и популярных инструментов для React-разработки сегодня является Vite.

Что делает Vite? Vite — это инструмент, который объединяет в себе все необходимые утилиты и позволяет запустить проект буквально одной командой.
Процесс работы выглядит так:
- Исходный код: Мы пишем код в удобном для нас формате: файлы.ts,.tsx (TypeScript), CSS-модули и HTML.
- Команда npm run dev: Когда мы запускаем эту команду, Vite берёт наши исходники и «на лету» транспилирует и собирает их.
- Результат: На выходе получаются стандартные файлы (JS, HTML, CSS), которые гарантированно понимает любой современный браузер.
Локальный сервер разработки Помимо сборки кода, Vite выполняет еще одну критически важную функцию — он запускает локальный веб-сервер.
- localhost:5173: Vite выделяет специальный адрес (порт) на вашем компьютере.
- Связь с браузером: Когда вы вводите этот адрес в браузере, тот отправляет запрос на сервер Vite.
- Доставка кода: Сервер отдает собранные файлы в браузер, и ваше фронтенд-приложение «оживает».
Главный плюс: Вам не нужно быть экспертом по сборщикам. Достаточно установить Vite через NPM/NPX, выбрать нужные параметры при создании проекта, и всё будет работать «из коробки». Это значительно улучшает Developer Experience, позволяя сфокусироваться на написании кода, а не на конфигурационных файлах.
3. 🏗️ Создание приложения
Перед тем как создать приложение нужно чтобы был уставлен NodeJS который можно установить за этой
ссылкой.
Далее создаем приложение используя Vite с помощью это команды через терминал:
Далее Vite задаст несколько вопросов:
- Project name: Если вы хотите создать проект в текущей папке, введите точку (.).
- Select a framework: Выберите React.
- Select a variant: Выберите TypeScript. После этого Vite создаст структуру проекта.
После установки приложения нужно его открыть в VS Code или WebStorm и запустим приложение с
помощью команды в терминале:
Вы увидите ошибку в терминале так как нужно было прежде выполнить установку зависимостей и затем мы можем запустить приложение.
Команда dev — это псевдоним (скрипт), который описан в файле package.json в секции scripts. После запуска вы увидите в терминале адрес, по которому доступно ваше приложение, обычно http://localhost:5173/. Откройте его в браузере.
Разбор package.json Это главный файл проекта с точки зрения Node.js. В нём содержится ключевая информация:
- scripts: команды для управления проектом (dev, build, lint).
- dependencies: библиотеки, необходимые для работы приложения в браузере (например, react, react-dom).
- devDependencies: библиотеки, нужные только для процесса разработки (например, typescript, vite).
3.1. 🔹 Анализ React приложения
Хотя детальное изучение структуры будет происходить на протяжении всего курса, сейчас важно зафиксировать ключевые элементы, которые превращают набор файлов в работающее приложение.
- Ключевые файлы проекта
package.json:
- Главный файл с точки зрения
Node.js. Именно он превращает обычную папку в проект, позволяя запускать его с помощью модулей, пакетов и сборщикаVite. - index.html: Файл, который отправляется в браузер. Внутри находится практически пустой
<body>с единственным<div id="root">. Это «контейнер», в который React будет динамически внедрять (рендерить) весь интерфейс. В теге<script>здесь указана точка входа — файлmain.tsx. src/main.tsx: «Сердце» запуска приложения; именно с этого файла React начинает свою работу.
- Конфигурационные файлы
tsconfig.json: НастройкиTypeScript. Определяет правила компиляции и превращает проект в среду с типизацией.vite.config.ts: Настройки сборщикаVite. Здесь подключаются плагины и описываются правила сборки проекта для разработки и продакшена.
4. 🧩 JSX и Компоненты
4.1. 🔹 Почему расширение.tsx, а не.ts?
Именно здесь начинается "магия" React. Расширение файла .tsx говорит нам о том, что внутри используется JSX.
- JSX (JavaScript XML) — это синтаксис, который позволяет писать HTML-подобный код прямо внутри JavaScript.
- Это не строка: Важно понимать, что мы не ставим кавычки вокруг тегов. Это живой код, "HTML внутри JS".
- Транспиляция: Браузеры не понимают JSX. Инструменты (вроде Vite/Babel) превращают этот код в обычный JavaScript.
- Пример: Код
<App />превращается в вызов функцииjsx(...)илиReact.createElement(...). Вы можете проверить это в инструменте Babel JSX Online.
4.2. 🧱 Что такое компонент App?
Компонент — это фундаментальный строительный блок любого современного приложения (React, Vue, Angular).
- Внешний вид: Для разработчика использование компонента выглядит как написание кастомного
HTML-тега (например,
<App />). - Техническая суть: На самом деле компонент — это JavaScript-функция, которая возвращает JSX-разметку.
- Иерархия: Компонент
App— это главный корневой блок, который собирает в себе все остальные части интерфейса (хедер, контент, футер и т.д.).
4.3. 🧱 Важные правила написания компонентов
Правило одного узла Каждый компонент должен возвращать строго один родительский узел.
- Ошибка: Возвращать
<h1>и<div>на одном уровне без обертки. - Решение: Обернуть их в общий родитель.
React Fragment Чтобы не создавать лишних <div> в HTML-структуре (DOM), мы используем
Фрагменты.
- Синтаксис:
<>... </>(пустые угловые скобки). - Зачем: Они группируют элементы для React, но исчезают при отрисовке в браузере.
4.4. 🔹 Модульная система: Export и Import
Чтобы main.tsx мог использовать код из App.tsx, файлы должны обмениваться данными через систему
модулей.
Экспорт по умолчанию (Default Export)
- Код:
export default App; - Особенность: При импорте можно дать компоненту любое имя.
- Минус: Это часто приводит к путанице в больших проектах (непонятно, что именно мы импортируем), поэтому этот способ не рекомендуется.
Именованный экспорт (Named Export)
- Код:
export const App = () => {... } - Импорт: Обязательно используются фигурные скобки:
import { App } from './App';. - Плюс: Имя при импорте строго совпадает с именем при экспорте. Это делает код предсказуемым и безопасным.
Важное примечание про index.html:
В HTML-файле скрипт подключается с атрибутом type="module". Это сообщает браузеру, что
используется современный стандарт ES-Modules (ESM). Браузер получает одну точку входа
(main.tsx) и сам распутывает клубок всех импортов.
5. 🧠 Управление состоянием с useState
Мы закрываем main.tsx и концентрируемся на компоненте App. Здесь мы встречаем useState —
хук, который позволяет React хранить значение, изменяемое во времени.
- Принцип работы: Мы делегируем React задачу хранить данные (например, число 0) в своей внутренней "ячейке памяти".
- Интерфейс: Хук возвращает нам две вещи:
- Само значение (актуальные данные).
- Функцию-сеттер (инструмент для изменения этого значения).
🔸 Реактивность и Декларативный подход
Главная идея: Разметка зависит от данных. Если данные меняются, компонент должен "выплюнуть" новую разметку.
Мы не говорим браузеру, как менять DOM (найди элемент, поменяй текст). Мы описываем, каким
интерфейс должен быть в зависимости от текущего состояния (count). Это и есть декларативный
подход.
React реактивно реагирует на вызов функции-сеттера: он понимает, что данные изменились, и запускает процесс обновления.
🔸 Жизненный цикл обновления (Render Cycle)
Давайте проследим, что происходит пошагово, когда мы кликаем на кнопку счетчика:
- Событие: Клик вызывает функцию-сеттер (
setCount). - Обновление данных: React обновляет значение в своей внутренней памяти.
- Триггер рендера: React понимает, что состояние изменилось, и повторно вызывает функцию
компонента
App. - Новые данные: При новом вызове
useStateвозвращает уже обновленное значение (например,1вместо0). - Генерация JSX: Компонент возвращает новую структуру JSX с актуальными цифрами.
- Commit фаза: React сравнивает новый JSX со старым, находит отличия (Diffing) и точечно обновляет реальный DOM браузера.
🧩 Синтаксис JSX: Фигурные скобки
Внутри JSX мы смешиваем HTML и JavaScript.
- Без скобок:
count— это просто текст (слово "count"). - С фигурными скобками:
{count}— это обращение к переменной JavaScript. - В пропсах: То же самое касается атрибутов, например
onClick. Чтобы передать туда функцию, мы используем{... }.
🔸 Важно: Что такое "Рендеринг"?
В контексте React "Рендеринг" ≠ Рисование пикселей на экране.
- Render (Рендеринг): Это просто вызов функции компонента. Компонент запускается, отрабатывает логику и возвращает JSX. Это происходит часто (например, при каждом клике).
- Commit (Фиксация): Это момент, когда React берёт результат рендера и применяет его к реальному DOM-дереву.
Итог: useState — это наш инструмент управления бизнес-логикой (данными), а JSX — это
рендер-алгоритм, который описывает вид приложения на основе этих данных.
6. 🧩 Переносим плеер в JSX
6.1. 🧩 HTML => JSX
Используем пример который в начале разбирали где мы писали простой HTML файл и скопируем затем вставим в новый App.tsx файл и увидим что все работает как будто это просто HTML но нет это JS.
6.2. 🧩 JS => JSX
Итак перенесем часть JS и адаптируем в JS используя массив с треками и метод map который поможет нам создать треки.
Итак как сам React в данном примере не перерисовывает так как он получает статические данные в ввиде массива и нужно использовать инструменты React а именно useState в который мы передадим массив и будем уже иметь возможность с помощью React интерактивно взаимодействовать с этими данными
7. 📌 Render algorithm vs Data structure
У нас есть структура данных и есть алгоритм, который с этой структурой работает. Например, у нас есть массив треков — это структура данных. На основании этой структуры мы должны отрисовать список треков в интерфейсе.

Также треки могут находится в разных состояниях, и на основании этих данных мы можем отображать
разный результат, то есть описывать алгоритм работы с данной структурой. Например, если
const tracks = [], мы отображаем сообщение о том, что треков нет. Если const tracks = null,
отображаем состояние загрузки.
Допустим, мы хотим выделить трек и показать, что он выбран. В этом случае нужно задать вопрос: каким
образом мы должны работать с данными, чтобы изменилась отрисовка UI. Один из вариантов — добавить в
объект трека дополнительное поле, например selected: true. Изменяя значение этого поля, мы можем
управлять отображением выбранного состояния в интерфейсе.

Таким образом, одна из ключевых задач — правильно подбирать структуру данных, чтобы алгоритму было просто и эффективно выполнять свою работу, а UI — корректно реагировать на изменения состояния.
8. 🧠 TS типизация для useState + условный рендеринг
Если закомментировать треки, TypeScript будет показывать ошибку о том, что таких элементов не
существует. Это происходит потому, что у нас отсутствует явная типизация данных. TypeScript,
опираясь на значение, переданное в useState, предположил, что у трека есть поля title и url.
Однако после удаления этих данных они больше не существуют, а типы по-прежнему не определены, из-за
чего возникают ошибки.
Эти проблемы можно избежать, если заранее описать типы и явно указать их при работе с состоянием. Это делает поведение кода предсказуемым и защищает от подобных ошибок на этапе компиляции.
Мы описали useState таким образом, что он может хранить массив треков. Для этого мы использовали
дженерик, с помощью которого явно указали тип данных состояния. В результате у нас исчезает ошибка
типизации — точнее, пропадает ошибка о том, что поля title или url не существуют.
Подробнее работу с дженериками и типизацией useState вы можете изучить в курсе React: путь Самурая
без альтернатив.
Далее можно переходить к обработке ситуации, когда у нас отсутствуют треки и необходимо корректно отобразить это состояние в интерфейсе.
Также было отмечено, что состояние может быть не только массивом треков или пустым массивом, но и
значением null. Поэтому мы расширяем типизацию и добавляем ещё одно допустимое значение — null.
Это позволяет корректно описать состояние загрузки и явно зафиксировать все возможные варианты данных, с которыми может работать компонент.
Итак, мы описали алгоритм поведения и отображения разметки при разных состояниях useState.
- Если состояние содержит пустой массив, мы отображаем span с текстом
NO TRACKS. - Если useState равен
null, мы отображаем состояние загрузки. - Если в состоянии присутствуют треки, мы отображаем список треков.
Таким образом, логика отображения напрямую зависит от состояния данных и остаётся простой, предсказуемой и легко расширяемой.
9. ⏱️ Hook useEffect
🔸 Сценарий: Загрузка приложения
Когда мы открываем приложение (например, "Music Fun Player"), первым делом мы видим заголовок и надпись "Loading...". Так устроено абсолютное большинство фронтенд-приложений:
- Мы показываем интерфейс-заглушку (лоадер).
- Данные запрашиваются с сервера.
- Пока идет запрос, пользователь видит индикатор загрузки.
🌐 Где делать запрос на сервер?
Мы не имеем права выполнять запросы, запускать таймеры или работать с localStorage прямо
внутри тела функции-компонента.
- Правило: Компонент должен быть "чистой" функцией рендеринга. Его задача — просто вернуть JSX на основе входных данных.
- Решение: Для выполнения "побочных эффектов" (Side Effects) существует второй главный хук — useEffect.
Синтаксис: Он принимает два аргумента: колбэк-функцию (сам эффект) и массив зависимостей.
- Массив
[](пустой): Это инструкция для React: "Выполни этот код только один раз, когда компонент будет отрисован впервые".
🔸 Пошаговый разбор жизненного цикла
Давайте проследим хронологию событий при загрузке данных:
1. 🔸 Первый рендер (Render Phase)
useStateинициализируется значениемnull.- React регистрирует
useEffect, но пока не выполняет его. - Компонент возвращает JSX для состояния "Loading...".
2. 🔸 Фаза фиксации (Commit Phase)
- React обновляет реальный DOM.
- Пользователь видит на экране текст "Loading...".
3. 🔸 Выполнение эффекта (Passive Effects)
- Сразу после того, как
commitпрошел, React запускает колбэк изuseEffect. - Внутри происходит запрос к API. Когда данные получены, мы вызываем сеттер (
setTracks).
4. 🔸 Второй рендер
- Вызов сеттера сообщает Реакту, что данные изменились.
- Компонент вызывается заново. Теперь
useStateвозвращает массив треков. useEffectна этот раз игнорируется, так как массив зависимостей[]пуст и не изменился.
5. 🔸 Финальный коммит
- React сравнивает старый и новый JSX.
- Реальный DOM обновляется, и пользователь видит список музыки.
🔸 Mount, Update, Unmount
Мы можем воспринимать жизнь компонента через три стадии:
- Mount (Вмонтирование): Компонент "рождается" и вставляется в DOM впервые. Этому
соответствует
useEffectс пустым массивом[]. - Update (Обновление): Компонент перерисовывается из-за изменения пропсов или стейта.
- Unmount (Размонтирование): Компонент удаляется со страницы (умирает).
10. 🌐 Про Back-end API, musicfun api, apihub, swagger
Работа современного фронтенд-приложения строится на постоянном диалоге с сервером. Когда пользователь открывает страницу, он сначала видит лишь оболочку (скелет приложения), в то время как за кулисами происходит сложный процесс получения данных.
🔸 Swagger: Как фронтенд понимает бэкенд
Для того чтобы этот диалог был успешным, разработчикам нужен «общий язык» или контракт. Эту роль выполняет Swagger-документация. Это не просто справочник, а детальное описание того, какие эндпоинты (адреса) нам доступны, какие параметры нужно передать и, самое главное, в каком виде придет ответ.
Для работы с реальными данными (например, в Music Fun API) используется персональный API Key. Это ваш пропуск в систему, который необходимо держать в секрете. В самом интерфейсе Swagger можно нажать кнопку Authorize, ввести свой ключ и протестировать запрос, чтобы заранее увидеть структуру данных, которые нам предстоит обработать.

Все необходимые ссылки:
11. 📌 Fetch + TS
Основной инструмент для получения данных — встроенная функция fetch. Однако ее нельзя вызывать
просто так внутри компонента, иначе запрос будет улетать при каждой перерисовке. Поэтому мы
оборачиваем его в хук useEffect.
Логика процесса выглядит следующим образом: мы вызываем fetch, указываем URL и передаем настройки
авторизации в заголовках (headers). Получив «сырой» ответ, мы сначала превращаем его в JSON, а
затем извлекаем полезную нагрузку (например, json.data) и сохраняем её в локальный стейт через
функцию-сеттер (например, setTracks)
🟦 Роль TypeScript в работе с данными
Чтобы приложение было стабильным, нам нельзя доверяться серверу «на слово». Мы должны точно описать структуру ожидаемых данных с помощью типов TypeScript. Это создаёт защитный барьер: если структура API изменится или мы совершим опечатку в названии поля (например, напишем titile вместо title), TypeScript сразу подсветит это как ошибку.
Мы создаем иерархию типов, которая в точности повторяет вложенность объектов из Swagger:
- Сначала описываем мелкие детали (например, Attachment с его URL).
- Затем описываем атрибуты трека (TrackAttributes), куда входят заголовки и массив вложений.
- И, наконец, объединяем всё в тип Track, где есть уникальный ID и атрибуты.
12. 📌 Как работает React, FiberNode (теория)
12.1. ⚙️ Lifecycle Mount Mode

Разберём жизненный цикл компоненты App при монтировании:
Reactсоздаёт для компоненты объектFiberNode, который будет хранить "состояния" этой компоненты. То естьReactготовит место для хранения всех необходимых данных компоненты.- Затем
Reactвызывает функцию-компоненту, начинаетсяrender phase. - Происходит инициализация начальных состояний, например, которые мы определили с помощью
useState. - Фиксируются действия, которые нужно будет выполнить позже, например, какая функция из
useEffectдолжна быть вызвана. - Компонента возвращает
JSX— объектReactElement, который может содержать другие объекты. Этот объект возвращается вReact. - Затем
Reactпереходит к следующей фазе —commit phase, когда на основе полученной информации создаётсяDOM. - Можно увидеть визуальный результат на странице.
- После визуализации
Reactприступает к выполнению зарегистрированных ранее в очередь функций. В нашем примере был зарегистрированuseEffect, который теперь выполняется. - Внутри
useEffectмы делаем запрос к серверу черезfetchи получаем ответ. - После получения ответа мы обновляем состояние с помощью
setTracks(serverTracks), фиксируя новое значение вFiberNode.
12.2. 🔃 Lifecycle Update Mode

Итак, мы зафиксировали новое значение, и tracks изменились. React видит, что данные, влияющие на
отрисовку, изменились, поэтому нужно создать новый результат на основе новых данных, и React
переходит к этапу обновления:
- React повторно вызывает компоненту
App, чтобы обновить DOM, который видит пользователь. На этот разuseEffectне будет вызван, так как он срабатывает только один раз при монтировании. - После этого React снова переходит в
render phase, где формируется новый обновлённыйReactElement. - Затем начинается
commit phase, и React обновляет DOM на основе новой информации. - Теперь можно увидеть визуальный результат после обновления.
13. 📌 Логика выделения трека, onClick
Для реализации выделения конкретного трека (например, добавления рамки при клике) нам требуется новое состояние. Мы создаем «ячейку памяти», где будем хранить идентификатор (ID) выбранного элемента.
Так как при старте приложения ничего не выбрано, начальным значением будет null. Мы используем
TypeScript, чтобы разрешить хранить в этой переменной либо строку, либо ничего:
🎨 Динамическая стилизация вибранного трека
Когда мы превращаем массив данных в список элементов с помощью метода .map(), мы можем внедрить
логику проверки для каждого отдельного пункта.
Внутри итерации мы сравниваем: «Является ли ID текущего трека тем самым ID, который лежит в
selectedTrackId?».
- Если да — мы подготавливаем объект стилей с рамкой:
style={{ border: '1px solid orange' }}. - Если нет — стили остаются пустыми.
Чтобы это заработало программно, мы вешаем обработчик onClick на элемент. При клике вызывается
функция-сеттер setSelectedTrackId(track.id), которая меняет состояние. React видит это
изменение, перерисовывает компонент, и оранжевая рамка «перепрыгивает» на нужный элемент.
Почему важны ключи (key prop)
При рендеринге списков React требует, чтобы у каждого корневого элемента внутри .map() был
уникальный атрибут key. Чаще всего для этого используется ID из базы данных.
Зачем это нужно React?
- Идентификация: Без ключей
Reactне понимает, какой элемент списка изменился, а какой остался прежним. Ему пришлось бы перерисовывать весь список целиком при любом обновлении данных. - Производительность: С ключом
key={track.id}Reactполучает «паспорт» элемента. При обновлении он видит: «Так, трек №105 остался на месте, у него просто сменился цвет рамки. Обновлю-ка я только этот маленький кусочекDOM». - Стабильность: Ключи предотвращают ошибки с состоянием (например, когда вы ввели текст в поле ввода в одном элементе списка, а при добавлении нового элемента этот текст «уплыл» к соседнему компоненту).
14. 📌 Реализация List–Details pattern

Исходные данные
tracks(массив объектов) - Хранит список всех треков для отображения.selectedTrackId(идентификатор выбранного трека, который может быть null или фактическимID) - ХранитIDвыбранного трека. Еслиnull, ни один трек не выбран.

Задача
При клике на трек в списке List, необходимо выделить его (например, рамкой) и показать информацию
о нем в блоке Detail.

Далее реализуется паттерн "List–Detail". Помимо хранения selectedTrackId целесообразно иметь
отдельную ячейку состояния для подгруженных деталей выбранного трека: selectedTrack, которая может
быть объектом с деталями или null.
В момент клика по треку, помимо установки selectedTrackId, выполняется запрос к эндпоинту
/tracks/{track.id} за деталями. До получения ответа можно сбросить selectedTrack в null, чтобы
показать индикацию загрузки. После получения ответа данные сохраняются в selectedTrack.
Условный рендеринг (List–Detail):
- Если
selectedTrackIdнет — показываем "Трек не выбран". - Если
selectedTrackIdесть, ноselectedTrackещёnull— показываем "Loading...". - Если
selectedTrackприсутствует — показываем детали: заголовок и текст песни.
15. 🧱 Что такое компонент? Декомпозиция приложения
Компонент — это строительный блок, который может состоять из нескольких мелких компонентов. Такой подход позволяет превратить сложный и объёмный фрагмент кода в один цельный блок.
Зачем это нужно?
Во-первых, чтобы разработчику было проще ориентироваться в большом объёме кода. Мы можем
сгруппировать связанные между собой строки, функции, данные и логику в единый блок — там, где есть
high cohesion (высокая связанность).
Во-вторых, компонент получает имя, отражающее его назначение. Например:
- список треков —
TrackList; - карточка трека —
TrackDetail; - шапка сайта —
Header.
На странице может быть несколько таких фрагментов, каждый из которых удобно выделить в отдельный компонент.
Наконец, компоненты снижают ментальную нагрузку и дают возможность многократного использования. В них может быть заложена определённая функциональность, которую легко применять повторно в разных местах проекта.
15.1. 🔹 React Component
React Component — это функция, имя которая начинается с заглавной буквы и которая возвращает какой
либо JSX.
К примеру, у нас есть компонент App, который мы импортируем в главный файл main.tsx. Внутри App
инкапсулирована (спрятана) логика и структура сайта: например, отображение списка треков или выбор
конкретного трека.
Файл main.tsx ничего об этом не знает. Его задача — запустить процесс: отрендерить компонент App,
в котором уже заложены функциональность и структура JSX-тегов для отображения на странице.
15.2. 🧱 Создаем новый компонент
Итак создадим новый компонент MainPage прям в файле main.tsx вместо компоненты App.
15.3. 🔹 Создаем структуру приложения
Мы хотим создать понятную структуру сайта, поэтому разделим его на отдельные блоки (компоненты). Это упрощает восприятие кода и позволяет легко понимать, какой элемент за что отвечает.

Итак перенесем код с App.tsx в созданные компоненты.
Создадим для каждой компоненты отдельно файл
16. 📌 Поднятие стейта, props
На данном этапе компоненты не могут напрямую взаимодействовать друг с другом. Каждая компонента имеет собственное состояние и не зависит от остальных. Однако мы можем вынести часть данных на уровень выше — в общий родительский компонент. Такой подход называется поднятием состояния (lifting state up). В этом случае данные хранятся в родительской компоненте и передаются дочерним компонентам через свойства.
Компонента, которая владеет данными, управляет их изменением и передаёт их дочерним компонентам, является информационным экспертом. Такую компоненту принято называть контейнерной компонентой.

16.1. 🧱 Добавляем props в компоненты
Props (сокращение от properties — «свойства») — это механизм для передачи данных от родительского компонента к дочернему в React и других подобных фреймворках (Vue, Svelte и т.д.).
17. ⏱️ Deps для useEffect, Inversion of control, callback
Когда мы передаём пустой массив зависимостей в useEffect, эффект выполняется только один раз — при
монтировании компоненты.
При этом сама компонента может перерисовываться в двух основных случаях: если изменяется её локальное состояние или если меняются пропсы, передаваемые от родительской компоненты.
React отслеживает эти изменения и, на основе обновлённых данных, выполняет повторный рендер, в результате чего пользовательский интерфейс отображает актуальное состояние данных.
Когда родительская компонента передаёт новые пропсы, например selectedTrackId, их значение может изменяться. Однако если UI напрямую не зависит от этого пропса, повторный рендер сам по себе не приведёт к выполнению побочных эффектов.
В результате при изменении selectedTrackId useEffect выполняется повторно, и запрашиваются новые актуальные данные для выбранного текущего трека.
Итак, выносим состояние selectedTrackId в MainPage — переносим useState на уровень основной компоненты. Далее через props передаём в компонент Playlist функцию изменения состояния (setter). С помощью компоненты Playlist мы будем изменять текущее значение selectedTrackId.
- Данные передаются в компоненты
PlaylistиTrackDetailsчерез props, чтобы компонент понимал, что именно ему нужно отображать. - В компонент
Playlistпередаётся функция выбора трека (onTrackSelect). - Когда пользователь выбирает трек, компонент вызывает
onTrackSelectи передаёт выбранный трек наверх, где он сохраняется и используется дальше.
18. 📌 Анализ перерисовок
- В каждый компонент добавляем
console.log, чтобы увидеть, когда и почему он перерисовывается. - Дальше меняем состояние (state) и смотрим в консоли:
- какие компоненты перерисовались;
- в каком порядке;
- что именно стало причиной: изменение state или изменение props.
- Цель — понять, какие перерисовки возникают при изменении состояния, и отделить:
- ожидаемые перерисовки (нормальная реакция UI),
- лишние перерисовки (которые можно уменьшить оптимизациями).
🔸 Ререндеры при первой загрузке

- Порядок отрисовки в консоли:
MainPagePlaylistTrackDetails- снова
Playlist
-
Значит,
Playlistперерисовывается несколько раз. -
Возможные причины ререндера
Playlist: -
изменились props (потому что родитель обновился и передал новые данные);
-
изменилось внутреннее состояние внутри
Playlist(черезuseState). -
По логам видно:
MainPageотрисовался только один раз, значит родитель не триггерит дополнительные ререндерыPlaylist. -
Вывод:
Playlistперерисовывает сам себя из-за изменения своего состояния. Типичный пример: -
сначала компонент рендерится с пустыми/начальными данными;
-
затем подгружаются треки → обновляется state (например, список
tracks); -
после этого
Playlistрендерится ещё раз уже с заполненным списком.
🔸 Ререндеры при выборе трека

-
При выборе трека перерисовывается
MainPage, потому что внутри него меняется состояние:selectedTrackId. -
Из-за ререндера
MainPageон перерисовывает дочерние компоненты: -
Playlist -
TrackDetailsи передаёт им обновлённыйselectedTrackIdчерез props. -
Дальше наблюдаем «лишние» вызовы:
-
TrackDetailsотрисовывается ещё раз — это нормально, потому что внутри него меняется своё состояние (например, подгрузка/обновление деталей трека). -
Playlistтоже отрисовывается ещё раз — это подозрительно, потому что по логике он не обязан перерисовываться повторно только из-за того, что детали трека обновились. -
Вывод на этом шаге:
-
ререндер
MainPage→ ожидаемо тянетPlaylistиTrackDetails; -
второй ререндер
TrackDetails→ ожидаем из-за его внутреннего состояния; -
второй ререндер
Playlist→ причина неочевидна, нужно отдельно проверить, что именно его триггерит.
🔸 Почему Playlist даёт лишний ререндер?
-
В
Playlistесть локальное состояние, которое хранит детали трека: -
const [_, setSelectedTrack] = useState<TrackDetailsResourse | null>(null) -
Но детали трека уже запрашивает и отображает отдельный компонент (
TrackDetails), поэтому вPlaylistэто состояние не нужно. -
Из-за этого в
Playlistесть лишняя работа при клике по треку: -
handleSelectTrackделает правильное действие: вызываетprops.onTrackSelect(trackId)и сообщает родителю, какой трек выбран. -
Но дальше он делает лишний запрос за деталями и кладёт результат в локальный
setSelectedTrack(...). -
Это приводит к дополнительному
setStateвнутриPlaylist→ и как следствие к лишнему ререндеруPlaylist, который нам и мешает при анализе. -
Ещё одна проблема в текущем запросе:
-
запрос собирается по
props.selectedTrackId, хотя только что был выбранtrackId; -
из-за асинхронности обновления props это может быть не тот id (запрос может уйти за предыдущим треком).
-
Что нужно сделать по логике:
-
Playlistотвечает только за список и выбор трека; -
при клике он должен только вызвать
onTrackSelect(trackId); -
запрос деталей и хранение деталей должны оставаться в
TrackDetails.
🔸 Повторный анализ ререндеров после правок

-
При первом открытии:
-
рендерится
MainPage, затемPlaylistиTrackDetails; -
в
PlaylistсрабатываетuseEffect: делается запрос за треками; -
треки сохраняются в состояние
tracks→Playlistперерисовывается уже с данными; -
TrackDetailsповторно не перерисовывается и не делает запрос, потому чтоselectedTrackIdещё нет (ничего не выбрано). -
При выборе трека:
-
обновляется
selectedTrackIdв родителе → перерисовываетсяMainPage; -
вместе с ним перерисовываются
PlaylistиTrackDetails(это ожидаемо, потому что изменились props); -
так как появился
selectedTrackId, вTrackDetailsзапускаетсяuseEffectи выполняется запрос за деталями трека; -
сервер возвращает данные → они сохраняются в состояние
TrackDetails→TrackDetailsперерисовывается второй раз уже с деталями. -
Главное изменение:
-
Playlistбольше не перерисовывается лишний раз, потому что внутри него больше нет лишнего обновления состояния, связанного с деталями трека.
19. 📌 Архитектура front-end, Data Access Layer
Если посмотреть на текущее состояние приложения, то видно, что компоненты у нас занимаются сразу несколькими задачами. Они отрисовывают интерфейс, управляют состоянием, делают запросы к бэкенду и на основе ответа сервера решают, что показать пользователю. Это нормальный этап развития приложения, но со временем такой подход начинает мешать.

По своей сути компонент — это презентационный слой. Его главная задача — показать UI и обработать взаимодействие пользователя. Когда в компоненте появляется слишком много другой логики, он становится «толстым» и хуже читается.
Если разложить приложение по зонам ответственности, можно выделить несколько ключевых ролей:
- отрисовка пользовательского интерфейса;
- управление данными и состоянием;
- работа с сервером и API-запросами.
Проблема возникает тогда, когда все эти роли живут внутри одного компонента. Код становится перегруженным, а изменения — более рискованными.
Первый шаг к более чистой архитектуре — вынести API-логику в отдельный слой:
- туда уходит весь код, связанный с запросами (
fetch, URL, заголовки); - там же описываются типы данных, которые возвращает сервер;
- компонент перестаёт знать детали того, как именно получаются данные.
В результате компоненту остаётся простая и понятная роль: когда ему нужны данные, он обращается к API-слою и получает готовый результат, не вникая в детали запроса.
Так мы постепенно приходим к разделению ответственности: каждый слой занимается своим делом, а код становится более понятным, предсказуемым и удобным для развития.
19.1. 🧱 Убираем работу с API из компонентов.
Весь код, который отвечает за запросы к серверу, мы переносим в отдельное место.
- Создаём папку
dal(Data Access Layer). - Внутри неё создаём файл
api.ts. - В этот файл переносим:
- всю логику с
fetch; - URL-адреса;
headersиAPI-KEY.
Компоненты больше не делают запросы напрямую. Вместо этого мы экспортируем из api.ts понятные
функции, например:
getTracks()— для получения списка треков;getTrack(trackId)— для получения деталей конкретного трека.
В итоге компоненту не важно, как именно получаются данные. Он просто вызывает нужную функцию из API-слоя и работает уже с результатом.
19.2. 🌐 Что изменилось после выноса API-слоя
После выноса запросов в dal/api.ts компоненты Playlist и TrackDetails стали «тоньше»: кода в
них стало меньше, и главное — из них ушло то, что не относится к их роли.
Важно понимать: «толстый компонент» — это не про количество строк. Это про то, что внутри компонента появляется логика, которая не связана с тем, зачем компонент вообще создавался. Компонент не должен знать детали того, как правильно собирать запрос, какие там URL, заголовки и ключи. Его задача — интерфейс: понять, когда данные нужны, и отрисовать результат.
Теперь компонент работает проще: когда ему нужны данные, он вызывает функцию вроде getTrack(...)
из API-слоя. А что происходит внутри этой функции — компоненту не важно. Там может быть реальный
запрос на сервер, может быть чтение из localStorage, может быть локальная база в браузере или даже
просто возврат заранее подготовленного объекта. В этом и смысл разделения на слои: UI зависит от
понятного интерфейса (функций), а детали получения данных спрятаны в отдельном модуле.
Дополнительно в API-слое стало удобно централизовать общие вещи:
- можно вынести
API_KEYв одну константу и переиспользовать; - можно вынести
headersцеликом в одну переменную и использовать в каждом запросе, вместо того чтобы копировать одно и то же.
Дальше делаем небольшой рефакторинг: создаём общий headers в api.ts, подключаем его во всех
запросах и проверяем, что после изменений приложение работает так же, как и раньше.
20. 📌 Архитектура front-end, Business Logic Layer
После выноса dal/api.ts мы отделили один важный аспект приложения: доступ к данным
(Persistence / Data Access Layer). Это место, которое знает, откуда и как брать данные: по HTTP, из
localStorage, из памяти, через service worker — неважно. Компоненту это не интересно: он не должен
знать ни транспорт, ни протокол, ни детали получения данных.
Но кроме доступа к данным есть ещё один отдельный аспект, который с ростом приложения становится ещё
важнее — управление состоянием. Сейчас стейт маленький: несколько useState, пара запросов. Но
дальше он начнёт разрастаться, появятся правила, ограничения, зависимости, и тогда смешивать UI и
логику данных внутри компонентов станет болезненно.
Когда мы говорим про управление состоянием, мы имеем в виду не просто «положить в useState», а
держать данные в порядке:
- соблюдать бизнес-правила и ограничения;
- поддерживать корректные переходы состояния;
- управлять загрузкой, ошибками, зависимостями между данными;
- обеспечивать предсказуемое поведение UI на основе этих данных.

Идея по цепочке выглядит так:
- компонент говорит: «мне нужен список треков / плейлистов»;
- state-слой решает, что делать: когда грузить, как хранить, что считать текущим состоянием;
- если нужно — state-слой обращается в
dalи забирает данные.
И тут важный момент: в React управление состоянием всё равно завязано на хуки (useState,
useEffect). Чтобы отделить логику управления данными от логики отрисовки, мы оформляем её в
кастомные хуки.
Кастомный хук — это функция, которая начинается с use... и внутри использует базовые хуки React
(или другие кастомные хуки). Мы выносим туда логику: загрузку, обновления состояния, обработку
ошибок, выбор активного трека и т.д., а компонент оставляем максимально простым.
Ключевая идея всей перестройки такая: самый “толстый” слой должен быть там, где живёт логика данных и правил, а компонент — это тонкий презентационный слой, который просто отображает состояние и вызывает события.
21. 📌 Кастомные хуки
Мы переходим к следующему шагу архитектуры: создаём кастомные хуки и выносим туда то, что не
относится к отрисовке UI. Идея в том, чтобы при “проваливании” в компонент (например, в MainPage)
мы видели в основном JSX и алгоритм рендера, а не кучу useState/useEffect.
Хук — это функция React вроде useState, useEffect. Кастомный хук — наша функция,
которая начинается с use... и внутри использует другие хуки. Его цель — собрать и изолировать
логику управления данными/состоянием, чтобы компоненты оставались презентационными.
21.1. 🔹 Кастомный хук для MainPage
Начинаем с компонента MainPage: там есть JSX и есть логика управления состоянием (например,
выбранный трек и обработчик выбора). Эту логику мы выносим в кастомный хук.
- Создаём кастомный хук (например,
useMainPage). - Переносим в него все
useState/useEffectи связанные функции. - Из хука возвращаем наружу то, что нужно компоненту для работы:
selectedTrackIdhandleTrackSelect
Внутри MainPage остаётся простая картина: мы вызываем хук, забираем нужные значения и используем
их в JSX.
Кастомный хук не “делится” состоянием между компонентами. Это просто функция-обёртка:
состояние создаётся для того компонента, который вызывает хук. Если другой компонент вызовет
useTrackSelection, он получит своё отдельное состояние.
21.2. 🔹 Кастомный хук для Playlist
В хук переносим useState и useEffect, потому что они напрямую связаны: эффект загружает треки и
сразу сохраняет их в локальное состояние. У них высокая связность, поэтому держим их вместе.
После переноса логика “закрывается” внутри хука, и компоненту не хватает данных для рендера. Нам
нужны только треки, поэтому хук возвращает { tracks }. setTracks наружу не отдаём — он остаётся
внутренним и используется только внутри хука.
Так поток данных становится чище: Playlist отображает данные, но не управляет ими напрямую.
21.3. 🔹 Кастомный хук для TrackDetails
Переходим к компоненту TrackDetails и делаем тот же архитектурный шаг — выносим бизнес-логику в
кастомный хук.
Кастомный хук — это функция, начинающаяся с use, которая инкапсулирует логику работы со
state и эффектами, но сама по себе не является компонентом и не получает props автоматически.
Для начала создаём хук useTrackDetails (позже для консистентности переименуем в useTrack) и
переносим туда:
useState— для хранения деталей трека;useEffect— для загрузки данных.
После переноса смотрим на компонент и понимаем, чего ему теперь не хватает. Компоненту нужен выбранный трек, значит именно его мы возвращаем из хука.
Хук — это обычная функция. Он не знает, что такое props.
Поэтому мы явно передаём в хук только то, что ему действительно нужно —
selectedTrackId: string | null. Передавать весь объект пропсов — плохая практика.
В итоге схема становится такой:
TrackDetails— чистый UI;- он вызывает
useTrack(selectedTrackId)и говорит: «дай мне трек для этого ID»; - если трек ещё не загружен — хук возвращает
null; - внутри хука
useEffectзагружает данные и обновляет state, вызывая ререндер.
Компоненту не важно, откуда берётся трек: из API, localStorage, IndexedDB или хоть «из
космоса». Этим занимается бизнес-логика.
19.4. 🔹 Переносим кастомные хуки в папку bll
На этом этапе мы вводим отдельную папку bll — Business Logic Layer:
- переносим туда все кастомные хуки;
- хуки называем в
camelCaseс маленькой буквы (useTrack,useTracks,useTrackSelection); - компоненты оставляем в
PascalCase.
- useTracks.ts
- useTrack.ts
- useTrackSelection.ts
- TrackDetails.tsx
- Playlist.tsx
- MainPage.tsx
...
...
В BLL нет JSX, поэтому файлы должны быть с расширением .ts, а не .tsx.
В итоге у нас появляется несколько независимых кастомных хуков, каждый из которых отвечает за управление состоянием и загрузку данных, а компоненты остаются тонкими презентационными слоями.
20. 📌 Ещё раз про архитектуру, типы, анализ кода
На этом этапе мы окончательно договариваемся, что кастомные хуки относятся к бизнес-логике. Именно здесь сосредоточена работа со state: его хранение, обновление, реакции на изменения и загрузка данных в нужный момент.
Бизнес-логика отвечает за когда и как загружать данные: при первом монтировании компонента,
при изменении id, при повторных обновлениях. Компоненту важен результат, а не сам процесс.
Например:
- загрузка списка треков происходит один раз — при первом монтировании;
- загрузка деталей трека происходит каждый раз, когда меняется
selectedTrackId; - это контролируется зависимостями в
useEffect, а не самим компонентом.
Мы можем не привязываться напрямую к понятию «жизненного цикла», но на практике мы всё равно мыслим именно так: компонент родился → эффект сработал → данные загрузились → UI обновился.
20.1. 🔹 Три слоя приложения
В итоге у нас формируется чёткая структура из трёх слоёв:
-
UI Layer
-
компоненты;
-
JSX;
-
алгоритм рендера;
-
никаких деталей про API и бизнес-правила.
-
Business Logic Layer (BLL)
-
кастомные хуки;
-
управление состоянием;
-
решения когда и что загружать;
-
импортирует Data Access Layer;
-
ничего не знает про компоненты.
-
Data Access Layer (DAL)
-
работа с API;
-
fetch, URL, headers, API-key; -
типы данных, приходящие с бэкенда;
-
не имеет импортов вообще.
Data Access Layer не знает ни про UI, ни про бизнес-логику. Это независимый модуль, который можно вынести в другой проект и переиспользовать.
Направление зависимостей строго однонаправленное:
- UI → Business Logic → Data Access
UI может напрямую обратиться к DAL — мы так делали раньше. Иногда это допустимо для быстрых решений. Но архитектурно мы сознательно разделяем эти слои, чтобы приложение масштабировалось без боли.
20.2. 🔹 Типы данных
Типы треков мы также переносим в Data Access Layer. Они отражают контракт с бэкендом и логично живут рядом с API-кодом. Это подчёркивает идею, что:
- структура данных приходит извне;
- остальное приложение подстраивается под DAL, а не наоборот.
В итоге компоненты становятся тонкими, бизнес-логика — явной и управляемой, а доступ к данным — изолированным и переиспользуемым.
- useTracks.ts
- useTrack.ts
- useTrackSelection.ts
- api.ts
- types.ts
- TrackDetails.tsx
- Playlist.tsx
- MainPage.tsx
...
21. 🧱 Компонент TrackItem
Внутри Playlist у нас отрисовывается каждый трек, и логика получается шумной: стили, сравнения,
обработчики клика — всё вперемешку. Поэтому мы выносим отдельный строительный блок TrackItem.
🔸 Как передаём выделение выбранного трека
Есть два варианта, как объяснить TrackItem, выбран он или нет:
- передать
selectedTrackIdи внутриTrackItemсравниватьtrack.id === selectedTrackId; - или проще для
TrackItem: передать сразу готовый флагisSelected: boolean.
Мы выбираем второй вариант: решение принимаем “снаружи”, а TrackItem просто отображает себя
правильно.
TrackItem не обязан знать про “другие треки”. Ему достаточно получить isSelected и применить
стиль, если нужно.
🔸 Как поднимаем клик наверх
TrackItem должен сообщить наверх, что по нему кликнули. Для этого добавляем проп:
onTrackSelect(trackId: string): void
И дальше важная оптимизация по смыслу: если Playlist ничего не делает “в середине”, то он может
не создавать лишние обёртки на каждую итерацию, а просто пробросить callback дальше.
То есть вместо handleClick на каждом map:
- мы делегируем
onTrackSelectпрямо вTrackItem, - а
TrackItemвызывает его, передавая свойtrack.id.
🔸 Про key в списке
key ставится на элемент, который создаётся в цикле (TrackItem в месте map). Внутри самого
TrackItem key не нужен — там мы его не пишем.
key всегда задаётся в месте map, а не внутри дочернего компонента.
🔸 Файловая структура
После того как TrackItem готов, выносим его в UI-слой отдельным файлом.
- Playlist.tsx
- TrackItem.tsx
- TrackDetails.tsx
- MainPage.tsx
- useTracks.ts
- useTrack.ts
- useTrackSelection.ts
- api.ts
- types.ts
После рефакторинга проверяем, что всё работает: треки грузятся, выделение выбранного трека есть, клик корректно поднимается наверх.
22. 🎨 Module CSS, clsx
Последний важный момент в этом модуле — стилизация компонентов и работа с модульными CSS. Фронтенд — это не только логика и архитектура, но и внешний вид. Дизайнеры рисуют макеты, а мы должны уметь аккуратно и безопасно переносить стили в код.
Мы уже работали с обычными CSS-файлами: импортируем файл — сборщик понимает, что его нужно включить в итоговую сборку. Проблема такого подхода в том, что все CSS-классы глобальные. Если в разных файлах появится один и тот же класс, начинается конфликт, и становится непонятно, какие стили в итоге применятся.
Существуют методологии вроде BEM, которые решают эту проблему через строгие правила нейминга. Это рабочий подход, но он требует дисциплины и быстро усложняет код. Хочется, чтобы уникализация происходила автоматически.
CSS Modules — это способ сделать классы локальными для конкретного компонента. Сборщик автоматически генерирует уникальные имена классов, и конфликты исчезают.
🎨 Как работает модульный CSS
Когда мы переименовываем файл в формат *.module.css, мы даём понять сборщику, что это не обычный
CSS. Такой файл при импорте превращается в объект, где:
- ключи — это имена классов, которые мы описали в CSS;
- значения — автоматически сгенерированные уникальные строки.
В DOM попадает именно эта уникальная строка, а не исходное имя класса. За счёт этого:
- одинаковые имена классов в разных компонентах не конфликтуют;
- стили жёстко привязаны к конкретному компоненту.
Если посмотреть на элемент в DevTools, мы увидим не track или selected, а “абракадабру” с хэшем.
Это нормально и именно этого мы добиваемся.
🧱 Один компонент — один файл стилей
Мы придерживаемся простого и удобного правила:
- один UI-компонент → один CSS Module.
Например:
Playlistотвечает только за список →Playlist.module.css;TrackItemотвечает за отображение одного трека →TrackItem.module.css.
Так стили инкапсулируются вместе с компонентом и не «протекают» наружу.
🎨 Управление состояниями через классы, а не inline-стили
Ранее выделение выбранного трека делалось через inline-стили. Это работает, но:
- inline-стили имеют более высокий приоритет и легко ломают CSS;
- они засоряют JSX и усложняют чтение компонента.
Более правильный подход — управлять внешним видом через классы:
- базовый класс применяется всегда;
- дополнительный класс добавляется при выполнении условия (например,
isSelected).
Таким образом:
- логика выбора остаётся в коде;
- визуальное оформление — в CSS;
- компонент остаётся чистым и читаемым.
В CSS Modules лучше использовать имена классов в camelCase, чтобы удобно обращаться к ним как к
свойствам объекта и не получать неожиданные преобразования.
Когда условий становится много, ручная сборка строк с классами начинает мешать. В реальных проектах для этого используют специальные утилиты, но сама идея остаётся той же: никаких inline-стилей, только классы и модульный CSS.
Так мы сохраняем архитектурную чистоту и на уровне логики, и на уровне визуального оформления.
22.1. 🔹 clsx и декларативная работа с классами
Когда в компонентах появляется много условий для стилизации, быстро начинается хаос: if / else,
ручные конкатенации строк, проверки на true / false, плюсики, пробелы — всё это засоряет код и
отвлекает от сути.
Поэтому мы подключаем небольшую утилиту — clsx.
clsx — это библиотека, которая помогает декларативно собирать className на основе условий,
без ручных if и конкатенаций строк.
Идея очень простая: мы описываем какие классы хотим получить и при каких условиях, а не как их склеивать.
Мы передаём в clsx объект:
- ключи — это классы;
- значения — условия, при которых класс должен попасть в итоговую строку.
Если значение true — класс добавляется. Если false — класс игнорируется.
Таким образом:
- базовый класс (например,
track) всегда включён; - дополнительный класс (например,
selected) включается только при выполнении условия.
В итоге мы получаем одну итоговую строку className, не думая про пробелы, порядок и конкатенацию.
Это переход от императивного стиля (“если так — прибавь строку”) к декларативному стилю (“я хочу такие классы при таких условиях”).
И это очень важно концептуально. Декларативность — ключевая идея React:
- в JSX мы описываем что хотим видеть, а не как это рисовать;
- императивный код скрыт внутри React;
- точно так же императивная логика склейки классов скрыта внутри
clsx.
🎨 CSS Modules и отсутствие конфликтов
Дальше мы усиливаем эту идею с помощью CSS Modules. Мы можем спокойно использовать одинаковые
имена классов в разных компонентах, например track, потому что:
- каждый
.module.cssпревращается в объект со своими уникальными значениями; - итоговые классы получают разные хэши;
- конфликты полностью исключены.
Даже если:
- в
TrackItemесть классtrack; - и в
TrackDetailsесть классtrack;
они никогда не пересекутся, потому что:
- имена вычисляются на основе пути к файлу;
- сборщик автоматически добавляет уникальный хэш.
В DevTools мы увидим разные значения классов, хотя в коде они называются одинаково. И это именно то, чего мы хотим.
🎨 Итог по стилизации
- CSS Modules дают локальность и защиту от конфликтов.
- clsx даёт чистый, декларативный способ управлять классами.
- Компоненты не захламлены стилями и условиями.
- Мы не думаем о неймингах вроде
track-wrapper,track-container,track-widget. - Мы называем классы по смыслу и доверяем инфраструктуре.
Так мы сохраняем архитектурную чистоту не только в логике, но и в визуальном слое приложения.
23. 🕹️ Git, система контроля версий
В качестве результата первого модуля мы задеплоим наше приложение на настоящий хостинг. Делать это будем не «вручную», а профессионально — через CI/CD pipeline. Но прежде чем к этому прийти, нужно заложить базу. И начинается она со знакомства с системой контроля версий Git. Не пугайтесь — будем разбираться постепенно.
❔Зачем вообще нужен Git
Система контроля версий решает одну ключевую проблему: она позволяет не терять историю проекта. У нас есть текущее состояние приложения — набор файлов и их содержимое. Как только мы что-то меняем (добавляем строку, удаляем файл, делаем рефакторинг), версия приложения меняется.
Если Git не использовать, мы не можем:
- вернуться назад и посмотреть, что было раньше;
- понять, где именно мы что-то сломали;
- безопасно экспериментировать с кодом.
Git даёт возможность буквально «путешествовать во времени» по версиям проекта. Это особенно важно, потому что ошибки и неудачные изменения — нормальная часть разработки.
🤼♂️ Git и командная разработка

В реальной разработке над проектом почти всегда работает несколько разработчиков. Работа идёт параллельно:
- есть версия 1;
- коллективно появляется версия 2;
- дальше разработчики расходятся по разным направлениям;
- кто-то делает одну фичу, кто-то — другую;
- какие-то ветки оказываются тупиковыми и отбрасываются;
- в итоге рабочие изменения объединяются в новую версию.
Именно для этого Git и существует: версионирование истории, работа в ветках, объединение изменений, откаты, восстановление состояний.
‼️ Git ≠ GitHub
В рамках курса мы используем Git — систему контроля версий. Это локальный инструмент, который устанавливается на компьютер.
Git — это не GitHub. GitHub мы разберём позже. Сейчас нам нужен именно Git.
🔸 Инициализация репозитория
После установки Git мы можем сказать, что текущая папка — это не просто папка, а Git-репозиторий. Для этого:
- открываем терминал в папке проекта;
- инициализируем репозиторий.
С этого момента папка становится «умной»: Git начинает отслеживать изменения и хранить историю.
IDE (например, WebStorm) сразу это понимает и начинает помогать работать с Git через интерфейс.
🔸 Untracked файлы и .gitignore
После инициализации Git сообщает, что есть неотслеживаемые (untracked) файлы — файлы проекта, которые Git пока не наблюдает.
При этом в репозитории не должно быть:
node_modules— папка восстанавливается черезnpm install;.idea— локальные настройки IDE;dist/ build-папок.
Git их не показывает, потому что в проекте есть файл .gitignore — он говорит Git, какие файлы
и папки нужно игнорировать.
📚 Git status и реальные термины
Команда git status показывает текущее состояние репозитория. Важно понимать реальные термины Git:
- untracked — файлы, которые Git ещё не отслеживает;
- modified — файлы, которые уже отслеживаются и были изменены;
- staged — файлы, подготовленные к коммиту.
IDE часто упрощают эти названия, из-за чего возникает путаница. Важно ориентироваться именно на термины Git.
🔸 Staging и коммит
Перед тем как зафиксировать версию проекта, есть два шага:
- Добавить файлы в staging area — подготовить их к коммиту.
- Сделать commit — сохранить слепок текущего состояния проекта.
Коммит — это точка, к которой Git может в любой момент вернуться. После коммита рабочее дерево становится чистым — изменений нет.
🔸 Изменения после коммита
Как только файл был закоммичен, он становится постоянно отслеживаемым. Любые изменения в нём Git
помечает как modified.
Эти изменения можно:
- закоммитить как новую версию;
- или откатить, если правка была ошибочной.
🔸 История и rollback
Git позволяет:
- посмотреть, какие изменения были сделаны;
- сравнить текущую версию с предыдущей;
- откатить неудачные изменения и вернуть рабочее состояние.
Даже если что-то случайно сломали или удалили, история никуда не пропадает — Git всё помнит.
24. 🐈⬛ GitHub
Локальный репозиторий Git хранится на компьютере. В случае поломки ПК или сброса системы все
данные могут быть потеряны. Удалённый репозиторий в облачном хранилище, например, на GitHub,
решает эту проблему:
- обеспечивает резервное копирование (бэкап) исходного кода и всей истории изменений;
- позволяет совместную работу, когда другие участники команды могут клонировать репозиторий, вносить изменения и синхронизировать их с общим хранилищем;
- гарантирует, что код будет сохранён и доступен даже при утрате локальной копии.

GitHub — это крупнейший хостинг Git-репозиториев. Он предоставляет удалённое хранилище кода
и истории версий, позволяя работать с Git не только локально, но и через интернет. GitHub не
является Git-репозиторием — это удалённое хранилище, где размещаются репозитории, созданные
с помощью Git. Хотя GitHub является самым популярным хостингом Git-репозиториев, существуют
альтернативы: GitLab, Bitbucket, а также self-hosted Git-сервера.
GitHub ≠ Google Drive — он не синхронизирует автоматически, а позволяет осознанно
контролировать версии и коммиты.
Удаленные репозитории могут быть:
- публичными: видны всем пользователям, каждый может клонировать репозиторий себе локально.
- приватными: доступ ограничен, требует явного предоставления доступа другим разработчикам.
🔸 Создание удалённого репозитория
Для начала работы необходимо создать аккаунт или авторизоваться на GitHub.
- В профиле выбрать →
Repositories→New repository. - Далее задайте имя, например
youtube-lesson. - Выберите, какой репозиторий хотите создать(публичный или приватный).
- Нажать
Create repository.

🎉 Готово! Вы создали новый, возможно первый свой репозиторий.
GitHub покажет инструкцию с командами и ссылку на репозиторий. Чтобы отобразить ссылку, нажмите на
HTTPS:

🔸 Связь локального и удалённого репозитория
Чтобы связать наш локальный репозиторий с GitHub, используем команду:
origin— стандартное имя удалённого репозитория.
Чтобы проверить, что мы действительно связали наши репозитории выполним команду:
💾 Отправка изменений на GitHub (Push)
Для первой отправки всех наших изменений из локального хранилища на GitHub используем команду:
Git требует авторизацию. Это происходит, потому что публичность дает доступ только на
чтение.
Если репозиторий публичный — любой человек может:
- просматривать содержимое,
- скачивать,
- клонировать.
❗ Но изменить его — может только владелец или специально приглашённые участники.
В userName указываем свой ник для gitHub, а в password необходимо положить
Personal Access Token (PAT). Этот токен необходимо создать в личном кабинете:
GitHub → ⚙️ Settings → Developer Settings → Personal access tokens → Fine-grained tokens → Generate new token.
Задаем имя для нашего токена, например youtube-lesson и выбираем срок действия. Для безопасности
лучше ограничить срок действия токена.

Далее выбираем доступ к нужным репозиториям (только к публичным, ко всем или какому-то
конкретному) и пока предоставим все permissions из списка, открывающего при нажатии на
Add permissions:

В permissions для Contents изменим read-only на read and write:

Нажимаем Generate token, токен появляется в списке и можно его скопировать:

И теперь снова пробуем запушить изменения и подставить userName и token:
🚀 Все данные запушились на gitHub!
При последующих push не нужно будет указывать дополнительные флаги, выполняем команду: git push. Авторизация также не будет запрашиваться.
Когда мы выполняем команду git commit -m изменения сохраняются только локально. Чтобы
отправить их в удаленный репозиторий необходимо после этого выполнить команду git push!
🔸 Откат изменений
Самый простой способ отменить изменения — сделать новый коммит с нужными правками.
История останется последовательной, но код вернётся к прежнему виду.
🔸 Клонирование репозитория
Так как у нас теперь есть удаленный репозиторий со всеми необходимыми данными, нам не страшно
потерять локальную версию. Мы можем клонировать наш репозиторий с gitHub.
Открываем в нужной нам папке терминал и выполняем команду clone с указанием ссылки на репозиторий:
Адрес берем не из адресной строки, а из Code:

✅ Результат:
- Создаётся новая папка с именем репозитория.
- Скачивается вся история коммитов.
- Автоматически настраивается remote origin.
25. ⚙️ CI/CD, GitHub Actions, GitHub Pages
После того как мы научились коммитить и пушить изменения в удалённый репозиторий, возникает логичный вопрос: какое вообще отношение Git имеет к деплою и хостингу?
Короткий ответ — самое прямое. Практически все современные CI/CD pipeline начинаются именно с Git.
🔸 Как фронтенд-приложение попадает в браузер

На локальной машине всё выглядит просто:
- мы открываем браузер;
- заходим на
localhost:5173; - видим работающее приложение.
Но важно понимать, что именно туда попадает и каким образом.
Фронтенд-приложение — это набор HTML, CSS и JavaScript файлов. Чтобы браузер их получил, нужен сервер, который:
- слушает запросы на определённом порту;
- отдаёт эти файлы в ответ.
Когда мы запускаем проект в dev-режиме (npm run dev):
- Vite поднимает локальный сервер;
- на лету компилирует TypeScript, TSX, CSS Modules;
- отдает браузеру готовые HTML / CSS / JS.
Это dev-режим.
🔸 Build ≠ Dev
Если нам не нужен локальный сервер, а нужно просто получить готовые файлы, которые можно разместить на хостинге, используется другой шаг — build.
Команда build:
- не поднимает сервер;
- компилирует и транспилирует проект;
- кладёт результат в отдельную папку (
dist).
Во время build:
- TypeScript проверяется строго;
- любые ошибки ломают сборку;
- код минифицируется и оптимизируется.
В результате появляется папка dist, в которой лежит:
index.html;- ассеты (JS, CSS);
- полностью готовая продакшен-версия приложения.
Папка dist — это то, что реально нужно хостингу.
🌐 Статика и сервер
Важно понимать: Vite больше не нужен, чтобы приложение работало.
Любой сервер, который умеет раздавать статические файлы, может:
- взять содержимое
dist; - отдавать его браузеру по HTTP;
- и приложение будет работать.
Это и есть фактический результат сборки фронтенда.
🌿 Где здесь Git и CI/CD

Теперь собираем всю цепочку целиком:
- Мы пишем код локально.
- Коммитим изменения.
- Пушим их в удалённый Git-репозиторий.
- На пуш реагирует CI/CD сервер.
- CI/CD сервер:
- клонирует репозиторий;
- устанавливает зависимости;
- запускает build;
- забирает папку
dist; - отправляет её на хостинг.
- Хостинг начинает раздавать новую версию приложения пользователям.
Это и есть:
- CI (Continuous Integration) — непрерывная интеграция кода в репозиторий;
- CD (Continuous Delivery / Deployment) — автоматическая доставка результата на хостинг.
🌿 GitHub Pages и GitHub Actions
В рамках курса в качестве хостинга используется GitHub Pages — бесплатный статический хостинг от GitHub.
Важно зафиксировать архитектуру:
- GitHub (репозиторий) — хранит код;
- GitHub Actions — CI/CD сервер;
- GitHub Pages — хостинг.
Это три разных сервиса, даже если они находятся на одном сайте.
👨🏻🏫 Роль GitHub Actions
GitHub Actions — это полноценный CI/CD сервер, который:
- поднимает виртуальную машину;
- делает
git clone; - выполняет
npm install; - запускает
npm run build; - деплоит результат на GitHub Pages.
Мы не билдим продакшен локально и не копируем файлы руками. Всё делает автоматизированный pipeline.
🕹️ Активация GitHub Pages
Для запуска всего процесса необходимо:
- зайти в настройки репозитория (
Settings); - активировать GitHub Pages;
- выбрать вариант деплоя через GitHub Actions.
После этого:
- GitHub понимает, что нужно использовать CI/CD;
- появляется вкладка Actions, где будут отображаться все запуски pipeline;
- деплой будет происходить автоматически при пуше.
🔸 Итоговая картина
- Git — точка входа для всего pipeline.
- Build — превращает исходники в продакшен-файлы.
- CI/CD сервер — автоматизирует процесс.
- Хостинг — раздаёт результат пользователям.
Так устроено 90% современных фронтенд-проектов — от небольших стартапов до BigTech.
Дальше мы будем пошагово настраивать этот pipeline и видеть, как после каждого пуша появляется новая версия приложения в интернете.
26. 🌿 GitHub Actions, workflows
Чтобы активировать GitHub Actions как CI/CD-сервер, одного переключателя в интерфейсе недостаточно. Нужна явная инструкция: что именно GitHub должен делать при каждом обновлении репозитория. Эта инструкция хранится в проекте в виде workflow-файла.
В корне репозитория создаём структуру:
.github/workflows/
Важно: у .github точка в начале обязательна — это часть имени папки.
Внутри workflows создаём файл, например:
deploy.yml
YAML — формат конфигурации (по смыслу близкий к JSON: ключи, значения, массивы). Его часто
используют DevOps-инженеры для описания пайплайнов и инфраструктуры.
Workflow — сценарий, который можно запускать автоматически (по событиям) или вручную. В нашем случае он нужен для деплоя.
🔸 Логика пайплайна: что происходит от кода до хостинга
Наша цель — чтобы деплой запускался автоматически при событии push в ветку main.
Цепочка выглядит так:
- Мы пишем код локально и делаем
commit. - Делаем
pushв удалённый репозиторий. - GitHub фиксирует событие
pushи запускает workflow. - GitHub Actions поднимает отдельную виртуальную машину (условно “одноразовый сервер”).
- На этой машине выполняются шаги пайплайна: установка зависимостей, сборка, публикация результата.
- Собранная версия попадает на хостинг (GitHub Pages) и становится доступной пользователям.
Важно: GitHub Actions и GitHub Pages — разные сервисы, хотя настраиваются через один интерфейс GitHub.
Workflow не выполняется внутри репозитория и не выполняется “на GitHub как на одном компьютере”.
GitHub Actions выдаёт чистую среду (например, Ubuntu). В ней нужно явно выполнить всё необходимое:
- получить исходники проекта (по сути —
git clone); - подготовить Node.js нужной версии;
- поставить зависимости;
- собрать приложение;
- передать результат в GitHub Pages.
Поэтому в workflow описываются окружение и последовательность шагов.
🔸 Структура workflow: событие → job → steps
В workflow обычно фиксируются три уровня:
- Когда запускаемся: например,
pushвmain. - Какая работа выполняется: job (у нас одна, логично назвать
deploy). - Какие шаги внутри job: steps, которые выполняются по очереди.
Термины “workflow” и “pipeline” часто используют почти как синонимы. Формально workflow — описание, а pipeline — процесс выполнения. На этом уровне разницы почти нет.
🔸 Что делает Deploy workflow (по шагам)
Ниже — смысл шагов без погружения в синтаксис.
🔸 1) Checkout репозитория
CI-машина должна получить ваш код. Это эквивалент “скачай репозиторий и открой проект”.
🟩 2) Node.js нужной версии
Фиксируем версию Node (например, 20), чтобы сборка была стабильной и одинаковой везде.
🔸 3) Установка зависимостей
Запускается установка пакетов — без этого проект не соберётся.
🔸 4) Сборка (build)
Запускается сборка, и на выходе появляется папка dist — готовый продакшен-бандл (HTML/CSS/JS),
который можно раздавать пользователям.
🔸 5) Загрузка артефактов
Папка dist отправляется в специальное место, откуда GitHub Pages сможет её забрать.
🌿 6) Деплой на GitHub Pages
GitHub Pages забирает артефакты и публикует сайт. После этого новая версия становится доступна по публичному URL.
🔸 Финальный шаг: commit + push запускают деплой
После добавления workflow-файла в репозиторий остаётся стандартный путь:
- закоммитить изменения,
- запушить в
main.
Именно этот push становится триггером: GitHub Actions запускает workflow, выполняет шаги и деплоит
приложение на GitHub Pages.
26. 📌.env, переменные окружения
Когда приложение уже оказалось на GitHub Pages, почти всегда всплывает первый «взрослый» момент: секреты.
🔸 Ошибка, которую нельзя повторять
Можно совершить типичную, но серьёзную ошибку: запушил API-key в репозиторий.
Почему это плохо:
- API-key обычно считается секретом и не должен быть публичным.
- Если ключ окажется в открытом репозитории, его могут скопировать и использовать.
- На продакшене бэкенд часто режет запросы или блокирует ключи, которые «светились» в публичном доступе.
Если вы тоже запушили ключ — не “удаляйте строку и успокойтесь”. Перегенерируйте ключ на стороне API, чтобы старый перестал работать.
🔸 Два окружения: dev и prod
У нас фактически два режима:
- Dev-окружение: ключ нужен, чтобы удобно разрабатывать.
- Prod-окружение: ключ либо не нужен, либо должен быть под контролем окружения, а не в коде.
Для таких различий используют переменные окружения.
🔸 Что такое.env
.env — это файл с парами ключ=значение, без кавычек. Всё считается строкой.
Особенность Vite:
- Чтобы переменная читалась в приложении, нужен префикс
VITE_. - Например, ключ для API оформляют как переменную окружения с
VITE_....
🌿 Важно про Git:.env не должен улетать в репозиторий
Здесь есть два популярных подхода:
-
Не коммитить
.envвообще Тогда.envдобавляют в.gitignore. -
Коммитить
.env, но без секретов (часто на фронтенде так делают, но секреты тогда выносят иначе)
Если вы добавили файл в отслеживание до того, как внесли его в .gitignore, он будет продолжать
считаться отслеживаемым. Gitignore не “откатывает” историю.
🔸 Ситуация из жизни: файл уже попал в staging
Типичный сценарий:
- IDE предложила «добавить новый файл в Git» — вы согласились.
.envоказался в staged.- Потом вы добавили
.envв.gitignore, но Git всё равно продолжает его видеть.
Как это выглядит по смыслу:
- staged — файл “готов к коммиту”;
- modified — изменения “есть, но ещё не подготовлены”.
Если .env попал в staged по ошибке — его нужно убрать из staged, иначе он улетит в commit.
🔸 Чтение переменной окружения в приложении
Дальше логика такая:
- API-key больше не хардкодится в коде.
- Он читается из переменной окружения.
- Но в продакшене
.envможет не быть → значение будетundefined.
И вот здесь важный момент: нельзя бездумно добавлять заголовок с пустым значением.
Правильная идея на уровне алгоритма:
- создаём объект headers;
- если переменная окружения существует — добавляем
api-key; - если нет — не добавляем этот header вообще.
Пустой заголовок с undefined — это не “всё равно”. Это может ломать запросы и давать неочевидные баги.
Также это решение удобно тем, что:
- логика вычисляется один раз при загрузке модуля;
- значение ключа не меняется «по жизни» приложения;
- значит, нет смысла собирать headers каждый раз заново.
🌿 Повторный деплой через GitHub Actions
После правок:
- коммитим изменения;
- пушим в
main; - GitHub Actions автоматически запускает workflow деплоя.
Дальше — вкладка Actions:
- видим workflow;
- проваливаемся внутрь;
- видим job;
- внутри job — шаги пайплайна.
Если workflow упал:
- смотрим на шаг, где ошибка;
- читаем лог;
- фиксим причину и пушим снова.
🔸 Где найти ссылку на задеплоенный сайт
Самый надёжный путь:
- Repository → Settings → Pages
- Там будет сообщение о последнем деплое и ссылка
Visit site.
Итог: приложение доступно по домену GitHub Pages (поддомен GitHub), без покупки собственного домена.
27. ⚡ Vite.config.ts, base
После деплоя вы открываете сайт через Visit site, видите белую страницу, лезете в DevTools — и там
404 / ABORTED: скрипты не подгружаются.
🔸 Почему так происходит
У GitHub Pages адрес проекта обычно выглядит так:
- домен GitHub (
username.github.io) - плюс сегмент
/<repo-name>/, потому что сайт живёт внутри репозитория, а не в корне домена.
А вот собранный index.html по умолчанию часто ссылается на ассеты как на корневые:
/assets/index.js/assets/index.css
В результате браузер пытается загрузить ассеты с корня домена, а реальный корень вашего
приложения — /<repo-name>/. Поэтому и получаются 404.
⚡ Фикс: указать base path в Vite
Нужно объяснить Vite, что приложение будет жить не в /, а в /<repo-name>/. Для этого в
vite.config задают базовый путь (base), равный имени репозитория.
После этого:
- вы коммитите изменения
- пушите в
main - GitHub Actions запускает workflow
- происходит новый build и deploy
- ссылки в
index.htmlстановятся корректными
28. 📌 Domain для apihub.it-incubator.io
Когда UI уже работает, следующая проблема проявляется в Network:
- запросы к API улетают
- в ответ прилетает ошибка вида Too many requests / domain is incorrect / ограничения по домену
Смысл в том, что API может быть настроен так, что:
- в dev запросы разрешены (localhost)
- в prod домен должен быть явно добавлен в настройках API (allowlist), иначе запросы блокируются

Поэтому в продакшене вы видите ошибки, пока не добавите ваш домен GitHub Pages в разрешённые.


