19 - props + callbacks на примере плеера musicfun

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

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

Рефакторинг приложения "MusicFun": синхронизация компонентов

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

📚 Анализ исходного кода

1. Анализ компонентов

TracksList: "умный" (smart) компонент, который самостоятельно загружает данные через useEffect, хранит их в локальном состоянии (local state), и отвечает за выделение выбранного трека.

TracksList.tsx
export function TracksList() {
  const [tracks, setTracks] = useState(null)
  const [selectedTrackId, setSelectedTrackId] = useState(null)
 
  useEffect(() => {
    // ...
  }, [])
}

TrackDetail: также является "умным" компонентом. Он использует useEffect для загрузки деталей одного и того же трека, используя захардкоженный (жестко заданный) ID.

TrackDetail.tsx
export function TrackDetail() {
  const [selectedTrack, setSelectedTrack] = useState(null)
 
  useEffect(() => {
    // ...
  }, [])
}

2. Основная проблема: компоненты не синхронизированы

TracksList и TrackDetail являются "детьми" (children) компонента MainPage и не могут общаться напрямую. Когда пользователь выбирает трек в TracksList, TrackDetail продолжает отображать тот трек, ID которого был захардкожен.

3. Решение: синхронизация через родительский компонент

Для синхронизации двух дочерних компонентов необходимо поднять состояние вверх, в их общего родителя (MainPage).

MainPage.tsx
export function MainPage() {
  const [trackId, setTrackId] = useState(null)
 
  return (
    <>
      <TracksList
        onTrackSelected={(id: string) => {
          setTrackId(id)
        }}
      />
      <TrackDetail />
    </>
  )
}

Шаг 1: уведомление родителя о событии (callback)

1. Изменение в TracksList:

  • В TracksList при клике на трек, помимо обновления своего локального состояния, вызывается функция из props, например, props.onTrackSelected(track.id).
  • Чтобы избежать ошибок, если родитель не передал эту функцию, используется оператор опциональной последовательности (?.): props.onTrackSelected?.(track.id). Это позволяет безопасно вызвать функцию, только если она существует.

2. Изменение в MainPage:

  • В MainPage компонент TracksList передается колбэк-функция onTrackSelected, которая будет вызвана при выборе трека.
  • Чтобы MainPage мог хранить ID выбранного трека и передавать его в TrackDetail, в нем создается локальное состояние с помощью useState:
MainPage.tsx
const [selectedTrackId, setSelectedTrackId] = useState(null)
  • Теперь колбэк onTrackSelected обновляет это состояние: setSelectedTrackId(id).

Шаг 2: Передача данных второму дочернему компоненту (Props)

1. Изменение в MainPage:
  • Когда состояние selectedTrackId в MainPage меняется, React перерисовывает этот компонент.
  • Во время перерисовки MainPage передает актуальное значение selectedTrackId в TrackDetail через props: <TrackDetail trackId={selectedTrackId} />.
2. Изменение в TrackDetail:
  • TrackDetail начинает принимать trackId из props и использует его для загрузки данных вместо захардкоженного значения.
  • useEffect в TrackDetail теперь зависит от props.trackId, что заставляет его перезапускаться и загружать новый трек при изменении этого ID.

📚 Рефакторинг

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

1. Контролируемый компонент

  • Была рассмотрена идея сделать TracksList контролируемым компонентом. Локальное состояние для selectedTrackId было удалено из TracksList, и теперь он получает ID выбранного трека исключительно из props от MainPage.
  • Это устраняет дублирование состояния и делает MainPage единственным источником правды о том, какой трек выбран.

2. Декомпозиция TracksList

  • Элемент списка треков был вынесен в отдельный компонент TrackItem.
TrackDetail.tsx
export function TrackItem(){
  return (
    <li>
        <div>
              {/*{}*/}
        </div>
    <li/>
  )
}
  • Это делает компонент TracksList более компактным и читаемым, так как он теперь отвечает только за отрисовку списка, а не за разметку каждого отдельного элемента.

3. Оптимизация и именование

  • Именование колбэков: имя колбэка было изменено с onTrackSelected (событие произошло) на onSelect (намерение/действие), что более точно отражает его суть в новой архитектуре.
  • Оптимизация колбэков: чтобы не создавать новую функцию для каждого TrackItem при каждой перерисовке, TrackItem теперь сообщает родителю о выборе, передавая свой track.id в колбэк onSelect. Это позволяет создать одну функцию-обработчик для всех элементов списка.

4. Стиль кода

  • Деструктуризация props: вместо обращения props.trackId используется деструктуризация в параметрах функции: function TrackDetail({ trackId }). Это сокращает код, но может снижать читаемость для новичков.
  • Вынесение обработчиков: функции-обработчики событий (handleClick, handleResetClick и т.д.) были вынесены из JSX-разметки в отдельные константы внутри компонента для улучшения чистоты кода.

Итоговый результат 🚀. Создано полноценное рабочее приложение с правильной архитектурой, использующее props, колбэки, useEffect и useState. Проведен рефакторинг для улучшения структуры и читаемости.


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

Цель задания: Научиться синхронизировать состояние между компонентами через callback-функции и освоить принципы декомпозиции компонентов.

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

Задание 1

Из компонента MainPage.tsx удали компоненты Header, PageTitle и Footer чтобы не отвлекаться. В дальнейшем при работе с дизайном мы вернем данные компоненты

Задание 2

В компоненте TaskDetails.tsx selectedTaskId и boardId захардкоджены (не изменяются), соответственно при выборе новой задачи у нас данные всегда остаются одинаковыми. Твоя задача переписать код таким образом, чтобы детали трека при выборе менялись

  1. В MainPage.tsx должно быть 2 стейта (для хранения selectedTaskId и boardId)
  2. При клике на таску в компоненте TasksList.tsx передавай selectedTaskId и boardId через callbacks в MainPage.tsx
  3. Полученные на втором шаге selectedTaskId и boardId передавай в компонент TaskDetails.tsx

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

res19-1

Задание 3

Создай компонент TaskItem.tsx и вынеси в него код который относится непосредственно к таске

💡
❗ Названия колбеков могут отличаться. Называй так, как тебе понятно
  • передавай свойство isSelected чтобы компонент TaskItem.tsx не думал о деталях как ему выделять border
  • onTaskSelected - это callback-функция, которая вызовется при нажатии на таску и передаст родителю сразу 2 параметра: taskId и boardId. ❗Если не понимаешь как это сделать, то можешь сделать 2 callback-функции, первая будет передавать taskId, а вторая boardId
TasksList.tsx
export function TasksList(props) {
  /*...*/
 
  return (
    <div>
      {tasks.map((task: any) => {
        return (
          <TaskItem
            key={task.id}
            task={task}
            isSelected={task.id === props.selectedTaskId}
            onTaskSelected={props.onTaskSelected}
          />
        )
      })}
    </div>
  )
}

В результате декомпозиции все должно работать как и на предыдущем шаге 🚀

Задание 4

В компоненте TasksList.tsx добавь кнопку сброса выделения (reset), при нажатии на которую должно произойти следующее:

  1. Border выделенного трека должен стать черным
  2. Информация о треке в компоненте TaskDetails.tsx должна сброситься, чтобы пользователь увидел Task is not selected

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

res19-2

Задание 5

Пройдись по компонентам MainPage, TasksList, TaskItem, TaskDetails и вынеси функции из разметки

В результате декомпозиции все должно работать как и на предыдущем шаге 🚀

Задание 6 🌟

Дополнительное задание (по желанию) 🌟

Используй деструктуризацию пропсов во всех компонентах

Плюсы и минусы деструктуризации пропсов в React

Плюсы
  • Читаемость кода — код становится более чистым и понятным
  • Меньше повторений — не нужно писать props. перед каждым свойством
  • Явное указание используемых пропсов — сразу видно в сигнатуре функции, какие пропсы использует компонент
  • Возможность задать значения по умолчанию — можно указать дефолтные значения прямо в параметрах
  • Лучшая поддержка автодополнения — IDE лучше предлагает доступные свойства
  • Улучшенная документированность — понятно, какие данные ожидает компонент
  • Сокращение объема кода — меньше символов для написания
Минусы
  • Потеря контекста — в больших компонентах неясно, откуда берется переменная
  • Сложности с передачей всех пропсов дальше — нужен rest оператор ...restProps
  • Проблемы при рефакторинге — сложнее передать объект пропсов в другую функцию
  • Длинные списки параметров — при большом количестве пропсов сигнатура становится громоздкой
  • Усложнение условной деструктуризации — сложнее делать условную или вычисляемую деструктуризацию
  • Потеря ссылки на объект props — иногда нужен доступ ко всему объекту пропсов
  • Сложность динамического доступа — труднее обращаться к пропсам по вычисляемому ключу

В результате декомпозиции все должно работать как и на предыдущем шаге 🚀


💡

🔶 Помни: все приложения мира — это в основном List-Detail. Освоив этот паттерн, ты сможешь браться за тестовые задания и понимать архитектуру большинства современных веб-приложений.

🔶 Экспериментируй с состоянием: попробуй оставить состояние в дочернем компоненте, попробуй поднять наверх. Поломай что-то специально — посмотри, что произойдёт. Только через эксперименты приходит понимание.

🔶 Осваивай современный синтаксис: деструктуризация, optional chaining, callback-функции — это не "синтаксический сахар", это инструменты профессионального разработчика.

🔶 Не останавливайся: трудно, тяжело — передохни 1-2 дня максимум и возвращайся. Работай утром, когда мозг свежий. Не сжигай дофамин на соцсети — береги энергию для сложного познания.

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

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