Модуль 1 · React: первый проект и первый бот

Справочник по Angular и React · модуль 1 · первый проект и первый бот

1.3

1.3

Props: данные
от родителя

В этой главе разобрано, как передать данные в компонент при его вызове. Карточка заявки из главы 1.2 получает тему, кабинет, подробности и статус, и три одинаковые карточки становятся тремя разными заявками. Показано, как TypeScript описывает props и что он находит в редакторе: забытое свойство, опечатку в имени, недопустимый статус. Отдельно разобрано, почему страница при такой ошибке всё равно открывается и где ошибку нужно искать.

Категория
React
Уровень
Начальный
Что понадобится
Проект zayavki после главы 1.2 и запущенный npm run dev
Результат
Карточка RequestCard получает данные через props, на доске три разные заявки

§1Что такое props

Сравнение из жизни

В главе 1.2 компонент сравнивался с бланком заявления: постоянный текст и пустые поля. Бланк печатают один раз, а заполняет его каждый, кто подаёт заявление: один пишет «проектор, кабинет 214», другой — «интернет, кабинет 305». Бланк один, заявления разные.

Бланк заполняет сам заявитель, а компонент заполняет тот, кто его вызывает, — родительский компонент.

Props (от properties — свойства) — данные, которые компонент получает от родителя при вызове. Родитель записывает их как атрибуты тега: <RequestCard title="Нет интернета" room="305" />. React собирает все атрибуты в один объект и передаёт его функции компонента первым параметром. Компонент берёт значения из этого объекта и подставляет в свою разметку в фигурных скобках.

Один компонент, три вызова с разными props

App RequestCard RequestCard RequestCard title="Не работает проектор" title="Нет интернета" title="Замятие бумаги…" каб. 214 · Новая каб. 305 · В работе каб. 112 · Готово функция одна, данные приходят сверху при каждом вызове

App вызывает одну и ту же функцию RequestCard три раза и каждый раз передаёт свои title, room, details и status. Поэтому на доске появятся три разные карточки.

Props уже встречались в главе 1.2, только не назывались так: атрибуты className, htmlFor, rows — это props, которые React передаёт тегам HTML. Разница в том, что для своих компонентов имена props придумывает разработчик.

§2Тип props и распаковка

TypeScript должен знать, какие props ожидает компонент и какого они вида. Для этого описывается тип — перечень полей объекта с видом значения у каждого. Тип записывается словом type:

Тип props карточки
type RequestCardProps = {
  title: string
  room: string
  details: string
  status: string
}

RequestCardProps — имя типа; по договорённости его составляют из имени компонента и слова Props. В фигурных скобках перечислены поля: слева имя, после двоеточия вид значения. string означает строку — любой текст в кавычках. Все четыре поля обязательные: компонент нельзя вызвать, не передав каждое из них. По этому описанию VS Code подсказывает имена props при вызове компонента и подчёркивает красным пропущенные или лишние.

Распаковка объекта props

Сравнение из жизни

Посылку можно держать закрытой и каждый раз доставать из неё нужный предмет. А можно сразу распаковать и разложить предметы на столе: дальше берёшь каждый по названию.

Предметы из посылки уходят, а в коде объект props остаётся целым: распаковка только создаёт короткие имена для его полей.

Функция компонента получает props одним объектом. Обращаться к его полям можно через точку — props.title, props.room. Удобнее сразу распаковать объект в параметре функции: перечислить нужные поля в фигурных скобках. Ниже одна и та же карточка записана двумя способами, результат у них одинаковый.

Без распаковки: обращение через props.
function RequestCard(props: RequestCardProps) {
  return <h3>{props.title}</h3>
}
С распаковкой: поля сразу доступны по именам
function RequestCard({ title }: RequestCardProps) {
  return <h3>{title}</h3>
}

В обоих вариантах после двоеточия стоит RequestCardProps: так параметру назначается тип. Во втором варианте фигурные скобки в параметре означают «взять из полученного объекта поле title и положить в переменную с тем же именем». В проектах на React почти всегда используют второй вариант: по первой строке функции сразу видно, какие props компонент использует.

§3Карточка получает данные

Работа идёт в проекте zayavki, сервер разработки запущен, доска заявок из главы 1.2 открыта в браузере.

  1. Замените содержимое RequestCard.tsx

    Откройте src/components/RequestCard.tsx, выделите весь текст (Ctrl+A), вставьте код ниже и сохраните.

    zayavki/src/components/RequestCard.tsx
    type RequestCardProps = {
      title: string
      room: string
      details: string
      status: string
    }
    
    function RequestCard({ title, room, details, status }: RequestCardProps) {
      return (
        <article className="card">
          <div className="card__top">
            <span className="card__room">каб. {room}</span>
            <span className="card__status">{status}</span>
          </div>
          <h3>{title}</h3>
          <p>{details}</p>
        </article>
      )
    }
    
    export default RequestCard
    VS Code: RequestCard.tsx с типом RequestCardProps и распаковкой title, room, details, status
    Так должен выглядеть RequestCard.tsx: тип props в начале, распаковка в параметре функции

    После сохранения карточки на странице опустеют: в них останутся только «каб.» и рамка. Причина в том, что App пока вызывает карточку без props. В App.tsx появятся три красные ошибки; их исправляет следующий шаг.

  2. Передайте данные из App

    Откройте src/App.tsx, замените содержимое кодом ниже и сохраните.

    zayavki/src/App.tsx
    import Header from './components/Header'
    import RequestForm from './components/RequestForm'
    import RequestCard from './components/RequestCard'
    
    function App() {
      return (
        <>
          <Header />
          <main className="board">
            <RequestForm />
            <section className="list">
              <h2>Заявки</h2>
              <RequestCard
                title="Не работает проектор"
                room="214"
                details="Проектор включается, но изображения на экране нет."
                status="Новая"
              />
              <RequestCard
                title="Нет интернета"
                room="305"
                details="На всех компьютерах в кабинете не открываются сайты."
                status="В работе"
              />
              <RequestCard
                title="Замятие бумаги в принтере"
                room="112"
                details="Принтер пишет «замятие бумаги», лоток пустой."
                status="Готово"
              />
            </section>
          </main>
        </>
      )
    }
    
    export default App
    Доска заявок: три разные карточки — проектор каб. 214 Новая, интернет каб. 305 В работе, принтер каб. 112 Готово
    Так должна выглядеть страница: три разные заявки с разными статусами

В новой карточке каждое место, где раньше стоял постоянный текст, заменено значением из props. В строке кабинета каб. {room}: слово «каб.» постоянное, номер подставляется. В статусе — {status}, в заголовке — {title}, в описании — {details}. Сама разметка и классы не изменились, поэтому стили из главы 1.2 действуют как раньше.

В App.tsx каждый вызов <RequestCard> теперь занимает несколько строк: по одному атрибуту на строку, закрывающая косая черта /> — на отдельной строке. Так записывают теги с тремя и больше атрибутами, чтобы их было удобно читать. Значения записаны в кавычках, потому что все они строки. Номер кабинета тоже строка (room="214"): в типе room объявлен как string, ведь кабинет может называться и словом, например «актовый зал».

§4Что будет, если забыть свойство

Все четыре props в типе обязательные. Чтобы увидеть, что происходит при пропущенном свойстве, в App.tsx у второй карточки удалите строку room="305" и сохраните файл. Вызов карточки станет таким:

App.tsx · вторая карточка без room
          <RequestCard
            title="Нет интернета"
            details="На всех компьютерах в кабинете не открываются сайты."
            status="В работе"
          />
VS Code: RequestCard в строке 19 подчёркнут красным, в панели Problems ошибка Property room is missing
Редактор: вызов карточки подчёркнут, в панели Problems — ошибка ts(2741)

VS Code подчеркнул имя RequestCard в строке 19 красной волнистой линией, файл App.tsx в проводнике стал красным, а в строке состояния внизу появился счётчик ошибок. Полный текст ошибки виден в панели Problems (меню View → Problems или щелчок по счётчику ошибок): Property 'room' is missing in type '{ title: string; details: string; status: string; }' but required in type 'RequestCardProps'. Читается так: «свойство room отсутствует в переданном объекте, но требуется типом RequestCardProps». В скобках перечислено то, что передано, — title, details, status. Строка под ошибкой 'room' is declared here указывает, где в RequestCard.tsx объявлено это свойство.

Доска заявок: у второй карточки вместо номера кабинета пусто — только каб.
Страница при этой ошибке открывается: у второй карточки после «каб.» пусто

Страница при этом открылась, а во второй карточке после «каб.» пусто. Так происходит потому, что сервер разработки Vite типы не проверяет: он только переводит TypeScript в JavaScript, отбрасывая описания типов, — так быстрее. Проверку типов выполняют редактор и команда сборки. Если в этом состоянии запустить npm run build во втором терминале, сборка остановится на той же ошибке:

Терминал · npm run build
> zayavki@0.0.0 build
> tsc -b && vite build

src/App.tsx(19,12): error TS2741: Property 'room' is missing in type '{ title: string; details: string; status: string; }' but required in type 'RequestCardProps'.

Команда tsc -b нашла ошибку в строке 19, символ 12, и vite build не запустился: знак && выполняет вторую команду, только если первая прошла без ошибок (глава 1.1, §3). Отсюда правило работы: смотреть на ошибки в редакторе, даже если страница выглядит нормально. Счётчик в строке состояния VS Code должен показывать ноль.

Верните строку room="305" на место (или вставьте код App.tsx из шага 2) и сохраните. Подчёркивание исчезнет, во второй карточке снова появится номер кабинета.

§5Статус из трёх значений

Сравнение из жизни

На бумажном бланке поле «Статус» можно оставить пустым строчками, и кто-то напишет «Новая», кто-то «новая», а кто-то «Нов.». Если вместо строчек напечатать три квадратика — «Новая», «В работе», «Готово» — и попросить отметить один, разночтений не будет.

Квадратик на бумаге можно не отметить вовсе, а TypeScript для обязательного поля это не пропустит.

Сейчас status объявлен как string, то есть любая строка. Можно передать «Новый» или «готово» с маленькой буквы, и TypeScript ошибку не покажет. Но статусов у заявки всего три, и тип может это записать. Кроме того, новая заявка почти всегда имеет статус «Новая», и писать его при каждом вызове необязательно: у свойства можно задать значение по умолчанию.

Для этого в TypeScript есть три записи. Объединение 'Новая' | 'В работе' | 'Готово' — тип, значением которого может быть только одна из перечисленных строк; черта | читается как «или». Вопросительный знак после имени поля (status?:) делает свойство необязательным. Значение по умолчанию задаётся при распаковке знаком равенства: status = 'Новая' — если родитель статус не передал, будет «Новая».

  1. Замените содержимое RequestCard.tsx
    zayavki/src/components/RequestCard.tsx
    type Status = 'Новая' | 'В работе' | 'Готово'
    
    type RequestCardProps = {
      title: string
      room: string
      details: string
      status?: Status
    }
    
    function RequestCard({ title, room, details, status = 'Новая' }: RequestCardProps) {
      return (
        <article className="card">
          <div className="card__top">
            <span className="card__room">каб. {room}</span>
            <span className="card__status">{status}</span>
          </div>
          <h3>{title}</h3>
          <p>{details}</p>
        </article>
      )
    }
    
    export default RequestCard
    VS Code: RequestCard.tsx с типом Status, необязательным status и значением по умолчанию Новая
    Так должен выглядеть RequestCard.tsx: тип Status в первой строке, status? в типе props, значение по умолчанию в параметре
  2. Уберите статус у первой карточки

    В App.tsx у первой карточки удалите строку status="Новая" — статус «Новая» теперь подставится сам. Или замените содержимое файла кодом ниже и сохраните.

    zayavki/src/App.tsx
    import Header from './components/Header'
    import RequestForm from './components/RequestForm'
    import RequestCard from './components/RequestCard'
    
    function App() {
      return (
        <>
          <Header />
          <main className="board">
            <RequestForm />
            <section className="list">
              <h2>Заявки</h2>
              <RequestCard
                title="Не работает проектор"
                room="214"
                details="Проектор включается, но изображения на экране нет."
              />
              <RequestCard
                title="Нет интернета"
                room="305"
                details="На всех компьютерах в кабинете не открываются сайты."
                status="В работе"
              />
              <RequestCard
                title="Замятие бумаги в принтере"
                room="112"
                details="Принтер пишет «замятие бумаги», лоток пустой."
                status="Готово"
              />
            </section>
          </main>
        </>
      )
    }
    
    export default App
    Доска заявок: у первой карточки статус Новая, хотя в App он не передан
    Так должна выглядеть страница: у первой карточки статус «Новая» — значение по умолчанию

В первой строке RequestCard.tsx объявлен отдельный тип Status — его удобно назвать отдельно, потому что статус понадобится и в других компонентах. В типе props поле теперь записано как status?: Status: необязательное, одно из трёх значений. В параметре функции status = 'Новая' задаёт значение на случай, если статус не передан. Страница выглядит так же, как после §3, но первая карточка получила «Новая» уже без строки status в App.tsx.

Проверка: опечатка в статусе

В App.tsx у третьей карточки замените status="Готово" на status="Готов" и сохраните.

VS Code: строка status=Готов подчёркнута, в Problems ошибка Type Готов is not assignable to type Status | undefined. Did you mean Готово
Редактор: TypeScript не принимает «Готов» и предлагает «Готово»

Редактор подчеркнул строку 29, ошибка в панели Problems: Type '"Готов"' is not assignable to type 'Status | undefined'. Did you mean '"Готово"'? — «значение "Готов" нельзя присвоить типу Status или undefined. Может быть, имелось в виду "Готово"?». undefined в сообщении появился из-за вопросительного знака: необязательное свойство может быть и не передано. Если бы status оставался типа string, опечатка прошла бы незамеченной, и на карточке появился бы статус «Готов». Верните «Готово» и сохраните.

§6Данные идут сверху вниз

Props передаются в одну сторону: от родителя к ребёнку. В App записано, какие заявки показать, и оттуда данные передаются карточкам. Карточка получает props и только выводит их; изменить их она не может и не должна. Если бы карточка поменяла свой status, в App остался бы прежний статус. Данные на странице разошлись бы с данными в родителе — та же ошибка, что в витрине с ручным обновлением из главы 0.1.

Поэтому правило React такое: компонент относится к своим props как к значениям только для чтения. Когда карточке нужно что-то изменить — например, по кнопке «Взять в работу» сменить статус, — она сообщает об этом родителю, а родитель меняет свои данные и передаёт карточке новые props. Как хранить изменяемые данные и как компонент сообщает родителю о событии, разобрано в главе 1.4.

Остался ещё один повтор: три вызова <RequestCard> в App.tsx записаны вручную, по одному на заявку. Для доски, где заявок десятки, данные хранят списком и выводят карточки из него. Это предмет главы 1.5.

§7Частые ошибки

Сообщения ниже получены командой tsc -b в проекте zayavki; VS Code показывает те же сообщения в панели Problems.

Что видноПочемуЧто сделать
Property 'room' is missing in type '{ … }' but required in type 'RequestCardProps'При вызове не передано обязательное свойствоДобавить атрибут room="…" или сделать свойство необязательным: room?: string
Property 'tittle' does not exist on type 'IntrinsicAttributes & RequestCardProps'. Did you mean 'title'?Опечатка в имени свойства при вызовеИсправить имя на то, что подсказано после Did you mean
Type '"Готов"' is not assignable to type 'Status | undefined'. Did you mean '"Готово"'?Значение не входит в объединение допустимых строкВыбрать одно из значений типа Status
Type 'number' is not assignable to type 'string'Передано число room={214}, а в типе room: stringПередать строку room="214" или изменить тип, если по смыслу нужно число
'{' or JSX element expectedЗначение записано без кавычек и без скобок: room=214Строку — в кавычки room="214", выражение — в скобки room={214}
'RequestCard' cannot be used as a JSX componentПараметры функции перечислены без фигурных скобок: RequestCard(title, room, …). React передаёт один объект, а функция ждёт четыре отдельных параметраЗаписать распаковку в фигурных скобках: RequestCard({ title, room, … }: RequestCardProps)
Страница открывается, но в карточке пустое местоОшибка типа есть, но сервер разработки типы не проверяетСмотреть на счётчик ошибок в строке состояния VS Code и панель Problems

§8Проверьте себя

Что такое props и кто их задаёт?

Props — данные, которые компонент получает при вызове. Их задаёт родительский компонент, записывая атрибуты тега: <RequestCard title="…" room="…" />. React собирает атрибуты в объект и передаёт его функции компонента.

Чем отличаются записи function RequestCard(props: RequestCardProps) и function RequestCard({ title }: RequestCardProps)?

В первой функция получает весь объект props, и к полям обращаются через точку: props.title. Во второй объект распаковывается в параметре, и поле доступно сразу по имени title. Результат одинаковый.

Почему при забытом свойстве room страница открылась, а npm run build — нет?

Сервер разработки Vite не проверяет типы, а только переводит TypeScript в JavaScript, поэтому код запускается, и на месте room оказывается пусто. Команда сборки начинается с tsc -b, которая проверяет типы, находит ошибку и останавливает сборку.

Что означает запись status?: 'Новая' | 'В работе' | 'Готово'?

Свойство status необязательное (знак ?), а если передано, то может быть только одной из трёх строк (черта | означает «или»). Любая другая строка — ошибка в редакторе.

Карточке нужно сменить статус заявки. Можно ли поменять props внутри RequestCard?

Нет. Props только для чтения: данные принадлежат родителю. Карточка должна сообщить родителю о событии, а родитель изменит свои данные и передаст карточке новые props. Как это делается, разобрано в главе 1.4.

Шапка Header всегда показывает подпись «ИТ-отдел колледжа». Что нужно изменить, чтобы подпись задавал App?

Четыре изменения:

  1. описать тип props шапки: type HeaderProps = { subtitle: string };
  2. принять props в параметре: function Header({ subtitle }: HeaderProps);
  3. вывести {subtitle} вместо текста подписи;
  4. передать значение из App: <Header subtitle="ИТ-отдел колледжа" />.

Шпаргалка

Тип propstype CardProps = { title: string }
Приём propsfunction Card({ title }: CardProps)
Передача<Card title="Нет интернета" />
Необязательноеstatus?: Status
По умолчанию{ status = 'Новая' }
Одно из значений'Новая' | 'В работе' | 'Готово'

Итог главы

Props — данные, которые родитель передаёт компоненту атрибутами тега. React собирает их в объект, функция компонента получает его первым параметром и обычно сразу распаковывает: { title, room, details, status }. Тип props в TypeScript перечисляет поля и вид их значений; по нему редактор находит забытые свойства, опечатки в именах и недопустимые значения. Объединение строк ограничивает статус тремя значениями, знак ? делает свойство необязательным, а знак равенства в распаковке задаёт значение по умолчанию. Сервер разработки типы не проверяет, поэтому ошибки нужно смотреть в редакторе и при сборке. Props идут от родителя к ребёнку и внутри компонента не меняются. Доска заявок показывает три разные заявки из одного компонента RequestCard.

Источники

  1. react.dev — Passing Props to a Component
  2. react.dev — Using TypeScript
  3. TypeScript Handbook — Union Types
  4. vite.dev — TypeScript: Transpile Only
Справочник по Angular и React © Макаров Максим Николаевич