Перейти к содержанию

Обратные вызовы между компонентами

Отдельный список умеет показывать переданные записи. Добавим действие «Убрать из подборки». Кнопка находится в карточке, но массив хранится выше, в Home. Нам нужен обратный вызов: дочерний компонент сообщит идентификатор выбранной записи, а родитель решит, как изменить состояние.

Это действие пока локальное. Мы не удаляем материал ProfessorWeb, не вызываем API и не сохраняем предпочтение пользователя. После обновления страницы исходная подборка вернётся. Такая граница позволяет спокойно разобрать обмен между компонентами до появления настоящей серверной операции.

Владелец массива принимает решение

В Home создаём состояние notes, начальное значение которого берётся из initialNotes. Функция hideNote получает идентификатор и строит новый массив без соответствующей записи. Для этого используется filter, а не splice. Старый массив остаётся прежним, новый передаётся React через setter.

Полный файл src/pages/Home.jsx этого этапа:

import {useState} from 'react';
import {initialNotes} from '../data/notes.js';
import LessonList from '../components/LessonList.jsx';
export default function Home() {
  const [notes, setNotes] = useState(initialNotes);
  function hideNote(id) {setNotes(current => current.filter(note => note.id !== id));}
  return <section id="catalog"><h1>Учебный каталог</h1>
    <LessonList title="Моя подборка" notes={notes} onHide={hideNote} />
    <button type="button" onClick={() => setNotes(initialNotes)}>Восстановить подборку</button>
  </section>;
}

Функциональная форма setNotes получает актуальный снимок current. Это полезно, когда новое значение зависит от предыдущего. Если два обновления попадут в одну последовательность обработки, каждое будет строиться на предоставленном React состоянии, а не на переменной из старого рендера. Здесь код остаётся коротким и явно выражает зависимость.

Проверка note.id !== id сохраняет все остальные записи и их порядок. Мы не ищем позицию в отображаемом массиве: такой индекс может отличаться от положения в полной подборке. Идентификатор связывает событие с записью, независимо от фильтра или будущей сортировки.

Действие Массив до события Массив после события
Убрать css-layout Три исходные записи HTML и JavaScript
Повторно убрать css-layout HTML и JavaScript Те же две записи
Восстановить подборку Любой локальный результат Три исходные записи

Повторное скрытие уже отсутствующей записи не должно затрагивать соседей. Filter снова создаст массив, но состав останется тем же. Это удобное свойство операции над идентификатором. Оно не является серверной идемпотентностью: HTTP-запросов здесь вообще нет.

Как кнопка передаёт намерение

Обновим LessonList. Появляется необязательный пропс onHide и кнопка, которая существует только при наличии этой функции:

export default function LessonList({title, notes, onHide}) {
  return <section><h2>{title}</h2>{notes.length === 0 ? <p>В подборке пока нет карточек.</p> :
    <ul className="cards">{notes.map(note => <li key={note.id} className="card"><article>
      <h3>{note.title}</h3><p>{note.topic} · {note.minutes} минут</p><p>{note.body}</p>
      {onHide && <button type="button" onClick={() => onHide(note.id)}>Убрать из подборки</button>}
    </article></li>)}</ul>}</section>;
}

В onClick передаётся функция-обёртка. Она вызывает onHide(note.id) в момент нажатия. Если написать onClick={onHide(note.id)}, вызов произойдёт уже при вычислении JSX. Результат вызова обычно окажется undefined, а массив может начать изменяться во время рендера. Это другая последовательность событий, а не сокращённая запись правильного обработчика.

Нельзя заменить идентификатор объектом события без изменения контракта. Если написать onClick={onHide}, React передаст аргумент события, тогда как Home ожидает строку. Условие фильтра будет сравнивать id записи с объектом и не уберёт нужную карточку. Обёртка точно определяет аргумент, о котором договорились компоненты.

Вызов onHide — обычный вызов JavaScript-функции. React не создаёт скрытый канал связи по названию пропса. Мы могли назвать его onRemoveFromSelection, но тогда нужно согласованно изменить оба файла. Префикс on помогает читателю кода заметить, что значение описывает реакцию на действие.

Почему дочерний компонент не меняет props

LessonList получает текущий массив, но не становится его владельцем. Удаление через notes.splice внутри карточки изменило бы объект, переданный родителем, и смешало бы отображение с хранением. Даже если текст исчезнет после другого рендера, сам механизм изменения будет зависеть от постороннего события.

Наш компонент вместо этого сообщает намерение. Home может сегодня выполнить filter, завтра сохранить список скрытых идентификаторов, а ещё позже отправить предпочтение в профиль пользователя. Разметка кнопки при этом не обязана знать, где именно хранятся данные. Такой интерфейс делает последующее развитие понятным.

Не следует копировать все notes в состояние LessonList, чтобы получить доступный setter. Два массива начнут расходиться. Кнопка восстановления в Home обновит свой список, но локальная копия дочернего компонента может остаться обрезанной. Проблему решает единый владелец данных, а не дополнительные синхронизирующие эффекты.

Правила неизменяемого обновления массивов изложены в документации React. Для нашего сценария filter подходит по назначению: он выбирает существующие элементы, не редактируя их. Изменение текста карточки потребовало бы map и создания нового объекта только для нужной записи.

Для такой кнопки не нужен доступ к DOM через querySelector. Если вручную удалить li из документа, данные React всё ещё содержали бы запись. Следующий рендер мог бы вернуть элемент, а пустое сообщение не появилось бы по правилам массива. Изменение состояния сохраняет согласованность модели и интерфейса: исчезновение карточки становится результатом новых входных данных, а не отдельной манипуляцией со страницей.

Локальное скрытие отличается от удаления

Надпись кнопки важна. «Удалить статью» обещала бы изменение содержимого каталога, которого здесь не происходит. «Убрать из подборки» точнее описывает наблюдаемый результат. Исходные учебные данные сохраняются, а кнопка восстановления возвращает первоначальный набор.

Если убрать все три карточки, LessonList покажет сообщение о пустой подборке. Это корректный результат последовательности действий. Пустое состояние не требует исключения, перезагрузки приложения или возврата исходных данных автоматически: пользователь явно выбрал такой набор.

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

Как проследить действие в компонентах

Начните с карточки css-layout и мысленно проведите путь: нажатие вызывает обёртку, обёртка передаёт строку, Home строит новый массив, React снова вызывает отображение с новым notes. Каждый шаг имеет конкретный вход и выход. Если карточка остаётся, можно проверить их последовательно, не разбирая весь проект сразу.

Неправильный идентификатор приводит к сохранению массива по составу. Неправильное имя пропса приводит к отсутствию кнопки, поскольку onHide не передан. Мутация массива может привести к отсутствию ожидаемого рендера. Эти случаи внешне похожи, но находятся в разных местах цепочки.

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

Оглавление серии