16 - Что такое компонент и для чего нам декомпозиция UI?

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

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

Принципы декомпозиции

Автор конспекта: Stanislav

📚 Введение

Компонент — это строительный блок, который может состоять из нескольких мелких компонентов. Такой подход позволяет превратить сложный и объёмный фрагмент кода в один цельный блок.

Зачем это нужно?

Во-первых, чтобы разработчику было проще ориентироваться в большом объёме кода. Мы можем сгруппировать связанные между собой строки, функции, данные и логику в единый блок — там, где есть high cohesion (высокая связанность).

Во-вторых, компонент получает имя, отражающее его назначение. Например:

  • список треков — TrackList;
  • карточка трека — TrackDetail;
  • шапка сайта — Header.

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

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

1. React Component

🔗

React Component — это функция, имя которая начинается с заглавной буквы и которая возвращает какой либо JSX.

К примеру, у нас есть компонент App, который мы импортируем в главный файл main.tsx. Внутри App инкапсулирована (спрятана) логика и структура сайта: например, отображение списка треков или выбор конкретного трека.

App.tsx
import { useEffect, useState } from "react"
 
export function App() {
  //...
}

Файл main.tsx ничего об этом не знает. Его задача — запустить процесс: отрендерить компонент App, в котором уже заложены функциональность и структура JSX-тегов для отображения на странице.

main.tsx
import { createRoot } from "react-dom/client"
import "./index.css"
import { App } from "./App.tsx"
 
const rootEl = document.getElementById("root")
const reactRoot = createRoot(rootEl!)
reactRoot.render(<App />)

2. Создаем новый компонент

И так создадим новый компонент MainPage прям в файле main.tsx вместо компоненты App.

main.tsx
import { createRoot } from "react-dom/client"
import "./index.css"
 
const rootEl = document.getElementById("root")
const reactRoot = createRoot(rootEl!)
reactRoot.render(<MainPage />)
 
function MainPage() {
  return <div>Main page</div>
}

3. Создаем структуру приложения

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

main.tsx
import { createRoot } from "react-dom/client"
import "./index.css"
 
const rootEl = document.getElementById("root")
const reactRoot = createRoot(rootEl!)
reactRoot.render(<MainPage />)
 
function MainPage() {
  return (
    <div>
      <Header />
      <SidebarMenu />
      <PageTitle />
      <TrackList />
      <TrackDetail />
      <Footer />
    </div>
  )
}

И так получаем ошибки так как таких компонент не существует поэтому создадим компоненты

main.tsx
//...
 
function Header() {
  return <div>Header</div>
}
function SidebarMenu() {
  return <div>SidebarMenu</div>
}
function PageTitle() {
  return <div>PageTitle</div>
}
function TrackList() {
  return <div>TrackList</div>
}
function TrackDetail() {
  return <div>TrackDetail</div>
}
function Footer() {
  return <div>Footer</div>
}

И так мы создали структуру и уже можем увидеть что все отобразилось на экране

les16-01

Далее вынесем компоненты в отдельную папку components к примеру с помощью виделения компоненты и затем с помощью клавиши f6 или сочетания клавиш fn + f6, ну или вручную можно создать компонент внутри папки components.

      • Footer.tsx
      • Header.tsx
      • PageTitle.tsx
      • SidebarMenu.tsx
      • TrackDetail.tsx
      • TrackList.tsx
    • main.tsx
    • ...
  • 4. Состояние в компонентах

    Добавим useState в компонентах TrackList и TrackDetail

    TrackList.tsx
    import { useState } from "react"
     
    export function TrackList() {
      const [counter, setCounter] = useState(0)
     
      return <div onClick={() => setCounter(counter + 1)}>TrackList {counter}</div>
    }
    TrackDetail.tsx
    import { useState } from "react"
     
    export function TrackDetail() {
      const [counter, setCounter] = useState(0)
     
      return <div onClick={() => setCounter(counter + 1)}>TrackList {counter}</div>
    }

    И также вызовем компонент TrackDetail в MainPage дважды.

    main.tsx
    import { createRoot } from "react-dom/client"
    import "./index.css"
     
    const rootEl = document.getElementById("root")
    const reactRoot = createRoot(rootEl!)
    reactRoot.render(<MainPage />)
     
    function MainPage() {
      return (
        <div>
          <Header />
          <SidebarMenu />
          <PageTitle />
          <TrackList />
          <TrackDetail />
          <TrackDetail />
          <Footer />
        </div>
      )
    }

    В примере выше мы вызываем компонент TrackList и дважды — TrackDetail.

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

    Поэтому:

    • для TrackList будет создан один counter;
    • для каждого вызова TrackDetail создаётся свой независимый counter.

    В итоге, если кликнуть по первой отрисованной TrackDetail, значение счётчика изменится только у неё. Вторая отрисовка останется со своим отдельным состоянием.

    les16-02

    Таким образом, каждое использование компонента формирует собственный экземпляр состояния. Сколько раз мы не вызовем TrackDetail, для каждого рендера React создаст отдельное состояние, которое хранится и управляется внутри компоненты через FiberNode.

    5. Переносим логику и отрисовку треков в TrackList

    Постепенно анализируя, какие данные и разметка нужны для отображения треков, мы переносим всю соответствующую логику и JSX из компоненты App в специально созданный компонент TrackList.

    TrackList.tsx
    import { useEffect, useState } from "react"
     
    export function TrackList() {
      const [tracks, setTracks] = useState(null)
     
      useEffect(() => {
        console.log("effect")
     
        fetch("https://musicfun.it-incubator.app/api/1.0/playlists/tracks", {
          headers: {
            "api-key": "XXX",
          },
        })
          .then((res) => res.json())
          .then((json) => setTracks(json.data))
      }, [])
     
      if (tracks === null) {
        return (
          <div>
            <h1>Musicfun</h1>
            <span>loading...</span>
          </div>
        )
      }
     
      if (tracks.length === 0) {
        return (
          <div>
            <h1>Musicfun</h1>
            <span>No tracks</span>
          </div>
        )
      }
     
      return (
        <ul>
          {tracks.map((track) => {
            return (
              <li
                key={track.id}
                style={
                  {
                    // border: track.id === selectedTrackId ? "1px solid orange" : "none",
                  }
                }
              >
                <div
                  onClick={() => {
                    // setSelectedTrackId(track.id)
                  }}
                >
                  {track.attributes.title}
                </div>
                <audio src={track.attributes.attachments[0].url} controls></audio>
              </li>
            )
          })}
        </ul>
      )
    }

    Теперь мы можем увидеть, как треки отображаются внутри компоненты TrackList. Вся логика и JSX, связанные с треками, инкапсулированы (спрятаны) внутри этой компоненты.

    Компонент MainPage при этом не знает ничего о запросах на сервер и деталях работы с треками — она просто использует TrackList как готовый блок.

    les16-3

    Продолжим работу с компонентом и перенесем состояние для выбора трека которое было при нажатии на title трека.

    TrackList.tsx
    import { useEffect, useState } from "react"
     
    export function TrackList() {
      const [tracks, setTracks] = useState(null)
      const [selectedTrackId, setSelectedTrackId] = useState(null)
     
      useEffect(() => {
        console.log("effect")
     
        fetch("https://musicfun.it-incubator.app/api/1.0/playlists/tracks", {
          headers: {
            "api-key": "XXX",
          },
        })
          .then((res) => res.json())
          .then((json) => setTracks(json.data))
      }, [])
     
      if (tracks === null) {
        return (
          <div>
            <h1>Musicfun</h1>
            <span>loading...</span>
          </div>
        )
      }
     
      if (tracks.length === 0) {
        return (
          <div>
            <h1>Musicfun</h1>
            <span>No tracks</span>
          </div>
        )
      }
     
      return (
        <ul>
          {tracks.map((track) => {
            return (
              <li
                key={track.id}
                style={{
                  border: track.id === selectedTrackId ? "1px solid orange" : "none",
                }}
              >
                <div
                  onClick={() => {
                    setSelectedTrackId(track.id)
                  }}
                >
                  {track.attributes.title}
                </div>
                <audio src={track.attributes.attachments[0].url} controls></audio>
              </li>
            )
          })}
        </ul>
      )
    }

    6. Загрузка деталей трека

    Для лучшего отображения треков и деталей трека на странице объединим в один блок TrackList и TrackDetail.

    main.tsx
    //...
     
    function MainPage() {
      return (
        <div>
          <Header />
          <SidebarMenu />
          <PageTitle />
          <div style={{ display: "flex" }}>
            <TrackList />
            <TrackDetail />
          </div>
          <Footer />
        </div>
      )
    }

    Далее выносим из компоненты App всю логику, связанную с TrackDetail. Так как состояние selectedTrackId мы перенесли в TrackList, в этой точке мы теряем к нему доступ. Чтобы временно сохранить работоспособность и проверить корректность запросов, создаём заглушку в виде переменной selectedTrackId.

    TrackDetail.tsx
    import { useEffect, useState } from "react"
     
    export function TrackDetail() {
      const [selectedTrack, setSelectedTrack] = useState(0)
      const selectedTrackId = "88133ec1-f82d-4fbb-b53b-5138b6fc7b90"
     
      useEffect(() => {
        if (!selectedTrackId) {
          return
        }
     
        fetch("https://musicfun.it-incubator.app/api/1.0/playlists/tracks/" + selectedTrackId, {
          headers: { "api-key": "XXX" },
        })
          .then((res) => res.json())
          .then((json) => {
            setSelectedTrack(json.data)
          })
      }, [selectedTrackId])
     
      return (
        <div>
          <h2>Details</h2>
          {!selectedTrack && !selectedTrackId && "Track is not selected."}
          {!selectedTrack && selectedTrackId && "Loading..."}
          {selectedTrack && (
            <div>
              <h3>{selectedTrack.attributes.title}</h3>
              <h4>Lyrics</h4>
              <p>{selectedTrack.attributes.lyrics ?? "no lyrics"}</p>
            </div>
          )}
        </div>
      )
    }

    И можем увидеть такой результат

    les16-04

    7. Компонент PageTitle

    При дальнейшем разборе компонента TrackList замечаем, что в нем лишним оказалось имя страницы. Вынесем его в отдельный созданный компонент PageTitle, чтобы TrackList отвечал только за отображение треков, а имя страницы обрабатывалось отдельно.

    TrackList.tsx
    //..
     
    export function TrackList() {
      //...
     
      if (tracks === null) {
        return (
          <div>
            <h1>Musicfun</h1>
            <span>loading...</span>
          </div>
        )
      }
     
      if (tracks.length === 0) {
        return (
          <div>
            <h1>Musicfun</h1>
            <span>No tracks</span>
          </div>
        )
      }
     
      return (
        //...
      )
    }
    PageTitle.tsx
    export function PageTitle() {
      return <h1>Musicfun</h1>
    }
    💡

    Заключение: Декомпозирование кода на компоненты играет ключевую роль. Оно упрощает чтение и поддержку проекта: сегодня в компоненте может быть одна строка, а завтра — десятки. Когда код разделён на компоненты, структура приложения становится прозрачнее, легче находить нужные части и вносить изменения, не усложняя остальную кодовую базу.


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

    Цель задания:

    • Освоить принципы декомпозиции React-приложений и научиться выделять независимые компоненты с высокой внутренней связанностью и низкой связанностью между собой.

    • Разбить большой компонент App на отдельные компоненты, следуя принципам высокой связанности (high cohesion) внутри компонентов и низкой связанности (low coupling) между компонентами.

    Задача 1

    Создать структуру главной страницы

    Создай компонент MainPage, который будет содержать общую структуру приложения

    folder structure
    src/
    ├── components/
       ├── Header.tsx
       ├── Footer.tsx
       ├── TasksList.tsx
       ├── TaskDetails.tsx
    ├── App.tsx
    └── main.tsx
    main.tsx
    /*...*/
     
    createRoot(document.getElementById("root")!).render(<MainPage />)
     
    function MainPage() {
      return (
        <div>
          <Header />
          <PageTitle />
          <div style={{ display: "flex", gap: "30px" }}>
            <TasksList />
            <TaskDetails />
          </div>
          <Footer />
        </div>
      )
    }

    Задача 2

    Создание компонент

    Создай заглушки для компонент по аналогии с Header.tsx и убедись, что компоненты отрисовываются

    Header.tsx
    export const Header = () => {
      return <div>Header</div>
    }

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

    res16-1

    Задача 3

    PageTitle

    В компоненте PageTitle.tsx замени div на заголовок первого уровня и в качестве текста напиши Trelly

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

    Задача 4

    TasksList

    Перенеси код из компонента App.tsx в TasksList.tsx, который должен содержать:

    • Состояние tasks
    • Логику загрузки всех задач
    • Состояние selectedTaskId для отслеживания выбранной задачи
    • Отрисовку списка задач
    • Временно закомментируй setBoardId(task.attributes.boardId)

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

    res16-3

    Задача 5

    TaskDetails

    Перенеси код из компонента App.tsx в TaskDetails.tsx, который должен содержать:

    • Состояние selectedTask
    • Логику загрузки выбранной задачи
    • Отображение деталей выбранной задачи

    ❗ Поскольку тема пропсов и колбэков будет в следующих уроках, пока что в TaskDetails создай константы selectedTaskId и boardId. Значения возьми из network вкладки

    • const selectedTaskId = '4f310604-82b5-4afd-b9a4-ddf12dfac0a3'
    • const boardId = '13923117-72de-4788-a7f0-4c42f162a5ab'

    res16-4

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

    res16-5

    Критерии успеха

    1. ✅ Каждый компонент содержит только связанную между собой логику
    2. ✅ В TasksList находится только логика работы со списком задач
    3. ✅ В TaskDetails находится только логика работы с деталями задачи
    4. ✅ Компоненты можно использовать независимо друг от друга
    5. ✅ Код легко читается и понимается
    6. ✅ При открытии любого компонента сразу понятно, за что он отвечает

    💡

    🔶 Это домашнее задание основано на реальном процессе разработки: каждый день тысячи разработчиков сталкиваются с необходимостью разбивать большие компоненты на мелкие. От этого умения зависит читаемость кода и возможность работы в команде.

    🔶 Навык декомпозиции - это то, что отличает junior-разработчика от middle. Компании готовы платить больше тем, кто умеет структурировать код так, чтобы его могли легко поддерживать другие программисты.

    🔶 Каждый компонент, который ты выделишь в этом задании, - это маленький шаг к пониманию архитектуры. Через несколько месяцев ты будешь проектировать целые приложения, а начинается всё с умения правильно разбить один большой компонент.

    🔶 Не переживай, если поначалу покажется сложным определить границы компонентов. Это приходит с опытом. Главное - начать экспериментировать и не бояться создавать "слишком много" мелких компонентов.

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

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