Видеоуроки, интерактивный редактор и сохранение прогресса — бесплатно, сразу после входа.
ВойтиСоздать аккаунт — бесплатноЗакончили урок?
Войдите, чтобы отмечать прогресс
В этом решении мы разбиваем список контактов на независимые компоненты, создавая модульную структуру с чёткими границами ответственности.
ContactsHeader объединяет заголовок со счётчиком и поле поиска. Мы выделили шапку отдельно, потому что это независимый функциональный блок с собственной логикой. Счётчик count показывает количество отфильтрованных контактов, а не всех - это важно для UX, чтобы пользователь видел результаты поиска. Поле ввода управляется через controlled component - значение приходит через prop search, изменения отправляются через onChange.
ContactAvatar отображает эмодзи-аватар и цветной индикатор статуса. Мы вынесли аватар в компонент, потому что это визуально обособленная часть с особой логикой позиционирования - индикатор накладывается поверх аватара. Динамический класс className={`status ${status}`} создаёт status online или status offline, что позволяет CSS применять разные цвета для точки - зелёный для онлайн, серый для офлайн.
ContactInfo отвечает за отображение имени и текста статуса. Мы выделили информацию в компонент, потому что текст статуса требует форматирования - для онлайн показывается просто "online", для офлайн - "был(а) X назад". Логика форматирования инкапсулирована внутри компонента через переменную statusText. Тернарный оператор выбирает правильный вариант текста в зависимости от статуса.
Мы вычисляем текст статуса перед рендером, а не в JSX, чтобы сделать код читаемым. Для онлайн-пользователей показывается "online", для офлайн - шаблонная строка с lastSeen. Значение lastSeen может быть "2 часа назад", "1 день назад" и т.д. Слово "был(а)" учитывает род - в реальном приложении это можно улучшить через поле gender.
MessageButton - это простая кнопка действия. Мы выделили кнопку отдельно для переиспользуемости и единообразия - если понадобится изменить текст, иконку или стиль, правки будут в одном месте. Компонент получает готовую функцию onMessage через props и просто вызывает её при клике. Это делегирование ответственности - кнопка не знает, что произойдёт, она просто уведомляет родителя.
ContactItem собирает вместе аватар, информацию и кнопку. Мы создали этот компонент, чтобы инкапсулировать структуру одного контакта. Компонент получает весь объект contact и распределяет его поля между дочерними компонентами. Стрелочная функция () => onMessage(contact.id) создаёт замыкание с id контакта - при клике на кнопку id будет передан в обработчик.
NoContacts отображает сообщение, когда список пустой. Мы выделили пустое состояние в компонент для переиспользуемости и централизации текста. Это улучшает UX - вместо пустого экрана пользователь видит понятное объяснение. Компонент не принимает props, но в будущем можно добавить разные варианты сообщений или предложения действий.
ContactsList - это контейнер для списка с обработкой пустого состояния. Мы выделили список отдельно, чтобы инкапсулировать логику условного рендеринга - показывать список или сообщение. Тернарный оператор проверяет contacts.length > 0. Если массив не пустой, вызывается map для создания ContactItem для каждого контакта. Если пустой, отображается NoContacts.
Тернарный оператор выбирает между двумя разными интерфейсами. Условие contacts.length > 0 проверяет, есть ли элементы в массиве. Это работает и для начального состояния, и для результатов поиска - если ничего не найдено, показывается сообщение. Такой подход лучше, чем отображать пустой <div>, потому что даёт пользователю обратную связь.
Фильтрация происходит в главном компоненте при каждом рендере. Метод filter создаёт новый массив, содержащий только контакты, чьи имена соответствуют запросу. toLowerCase() применяется к имени и строке поиска, делая поиск регистронезависимым - "анна", "Анна" и "АННА" дадут одинаковый результат. Метод includes проверяет, содержится ли подстрока в строке - это позволяет искать по части имени.
Мы приводим обе строки к нижнему регистру перед сравнением. Это стандартный паттерн для поиска без учёта регистра. Если пользователь введёт "мар", найдутся "Мария" и "Марина". Без toLowerCase() "мар" не нашёл бы "Мария", потому что регистры не совпадают.
Отфильтрованный массив - это вычисляемое значение, а не отдельное состояние. Мы не используем useState для filteredContacts, потому что это производное значение от contacts и search. При изменении любого из них React перерендерит компонент, и фильтрация выполнится заново. Это проще и надёжнее, чем синхронизировать два состояния.
Функция находит контакт по id и показывает alert. Метод find возвращает первый элемент, для которого условие c.id === contactId истинно. В реальном приложении вместо alert здесь была бы навигация на страницу чата, открытие модального окна или изменение состояния для отображения чата. Мы используем find, потому что нам нужен весь объект контакта, а не просто проверка существования.
Компонент App собирает шапку и список вместе. Структура простая - два больших блока. Всё состояние (contacts, search) и логика (фильтрация, обработчик сообщений) остаются в App. В ContactsHeader передаём значение поиска, функцию изменения и количество отфильтрованных контактов. В ContactsList передаём уже отфильтрованный массив - компонент списка не знает про фильтрацию, он просто отображает то, что получил. Функция setSearch передаётся напрямую как onChange, потому что сигнатуры совпадают.