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

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

1.4

1.4

Состояние
и события

В этой главе разобрано, как страница на React отвечает на действия пользователя. У карточки заявки появляется кнопка «Взять в работу», и по нажатию статус меняется на экране. Сначала показано, как подключить к кнопке функцию-обработчик и проверить её в консоли браузера. Затем — опыт с обычной переменной, которая меняется, а страница остаётся прежней, и хук useState, с которым новый статус появляется на экране. В конце состояние переносится из карточки в App, чтобы шапка доски показывала число новых заявок.

Категория
React
Уровень
Начальный
Что понадобится
Проект zayavki после главы 1.3, запущенный npm run dev, браузер Chrome, Edge или Яндекс Браузер
Результат
Кнопка в карточке переводит заявку из «Новая» в «В работе» и «Готово», шапка показывает число новых заявок

§1Событие и обработчик

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

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

В лифте реакция на кнопку заложена при сборке. В React функцию, которая выполнится при нажатии, пишет разработчик.

Событие — то, что происходит на странице и о чём браузер может сообщить программе: щелчок мышью, нажатие клавиши, ввод текста в поле, отправка формы. Обработчик события — функция, которую браузер вызывает, когда событие произошло. В React обработчик подключают к тегу атрибутом, имя которого начинается с on: onClick — щелчок, onChange — изменение поля, onSubmit — отправка формы. Значение атрибута — сама функция в фигурных скобках: <button onClick={handleClick}>.

Функцию-обработчик обычно объявляют внутри компонента, перед return. По договорённости её имя начинается с handle («обработать») и продолжается названием события: handleClick, handleSubmit. Так по имени видно, что функция вызывается браузером в ответ на действие пользователя.

Кнопка в карточке

В первой версии обработчик печатает сообщение в консоль браузера, страница при этом не меняется. По сообщению в консоли видно, что щелчок дошёл до функции.

  1. Добавьте стили кнопки

    Откройте src/index.css, вставьте в самый конец файла правила ниже и сохраните.

    zayavki/src/index.css · в конец файла
    .card button {
      margin-top: 12px;
      padding: 6px 12px;
      border: 1px solid #2f6fde;
      border-radius: 6px;
      background: #ffffff;
      color: #2f6fde;
      font: inherit;
      font-size: 14px;
      cursor: pointer;
    }
    
    .card button:disabled {
      border-color: #c4cad8;
      color: #8a93a6;
      cursor: default;
    }

    Первое правило оформляет кнопку внутри карточки: синяя рамка, белый фон, отступ сверху. Второе, с :disabled, задаёт серый вид выключенной кнопки; выключенная кнопка понадобится в §3.

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

    Откройте src/components/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) {
      function handleClick() {
        console.log('Нажата кнопка в заявке:', title)
      }
    
      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>
          <button onClick={handleClick}>Взять в работу</button>
        </article>
      )
    }
    
    export default RequestCard
    VS Code: RequestCard.tsx с функцией handleClick в строках 11–13 и кнопкой onClick в строке 23
    Так должен выглядеть RequestCard.tsx: функция handleClick перед return, кнопка в конце карточки
  3. Откройте консоль браузера

    В браузере с доской заявок нажмите F12 (на macOS — Cmd+Option+J) и выберите вкладку Console. Нажмите кнопку «Взять в работу» в каждой из трёх карточек.

    Доска заявок с кнопками Взять в работу, внизу консоль браузера с тремя сообщениями Нажата кнопка в заявке
    Так должен выглядеть результат: под каждой заявкой кнопка, в консоли три сообщения — по одному на нажатие

В функции handleClick одна команда — console.log. Она печатает в консоль браузера свои аргументы через пробел: строку 'Нажата кнопка в заявке:' и значение title. Консоль — панель инструментов разработчика, куда попадают сообщения программы и ошибки; на странице эти сообщения не видны. Справа от каждого сообщения указан файл и строка, где оно напечатано: RequestCard.tsx:12.

У каждой карточки свой обработчик с её собственным title: функция RequestCard вызывается три раза, и при каждом вызове создаётся отдельная handleClick. Поэтому в консоли три разных названия заявок. При открытии страницы в консоли может быть и сообщение React с предложением установить расширение React DevTools; для работы в этой главе оно не требуется.

Функция без скобок

В onClick={handleClick} после имени функции нет круглых скобок. Запись без скобок передаёт кнопке саму функцию, и браузер вызовет её при щелчке. Запись onClick={handleClick()} вызывает функцию сразу, при отрисовке карточки, и передаёт кнопке её результат. Что из этого получается, показано в §3.

Щелчок доходит до обработчика. Следующий шаг — изменение статуса заявки в обработчике.

§2Опыт с обычной переменной

Изменяемые данные в JavaScript хранят в переменной, объявленной словом let. Опыт проверяет, можно ли так хранить статус: переменная выводится в разметке и меняется в обработчике. Замените содержимое 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) {
  let current = status

  function handleClick() {
    current = 'В работе'
    console.log('current =', current)
  }

  return (
    <article className="card">
      <div className="card__top">
        <span className="card__room">каб. {room}</span>
        <span className="card__status">{current}</span>
      </div>
      <h3>{title}</h3>
      <p>{details}</p>
      <button onClick={handleClick}>Взять в работу</button>
    </article>
  )
}

export default RequestCard

Изменились три места. В строке 11 объявлена переменная current («текущий статус») с начальным значением из props. В обработчике ей присваивается 'В работе', и новое значение печатается в консоль. В разметке на месте статуса стоит {current}. Нажмите дважды «Взять в работу» в первой карточке.

Доска заявок: у первой карточки статус по-прежнему Новая, в консоли сообщение current = В работе с отметкой 2
Опыт с переменной: в консоли current = В работе, на карточке по-прежнему «Новая»

В консоли сообщение current = В работе; кружок с цифрой 2 слева означает, что одинаковое сообщение напечатано два раза подряд. Значит, обработчик выполнился и переменная изменилась, но статус на карточке остался «Новая».

Причина в том, как React строит страницу. Разметку карточки возвращает функция RequestCard, и React вызывает её, когда отрисовывает компонент. Вызов функции компонента называется отрисовкой (в документации — render). После отрисовки React переносит полученную разметку в страницу и больше функцию не вызывает, пока не получит сигнал, что данные изменились. Присваивание current = 'В работе' такого сигнала не даёт: переменная поменялась, но функция RequestCard не вызвана заново, и разметка на странице осталась от первой отрисовки.

Если бы React и вызвал функцию заново, статус всё равно вернулся бы к «Новая»: строка let current = status выполняется при каждом вызове и создаёт переменную с начальным значением. Обычная переменная существует только внутри одного вызова функции.

Что показывает линтер

Эту ошибку находит линтер Oxlint, установленный в проект в главе 1.1. Откройте второй терминал (Terminal → New Terminal; первый занят сервером разработки) и выполните команду:

Терминал
npm run lint
Терминал VS Code: npm run lint выводит два предупреждения react(immutability) для строки 14 RequestCard.tsx
Линтер выводит два предупреждения для строки 14 — присваивания в обработчике

Оба предупреждения относятся к строке 14, символ 5 — к присваиванию current = 'В работе'. Первое переводится так: «нельзя присваивать переменной новое значение после завершения отрисовки; это может привести к несогласованному поведению при следующих отрисовках». В конце обоих сообщений подсказка: Consider using state instead — «используйте вместо этого состояние». Состояние разобрано в следующем разделе.

Нужна переменная, которая сохраняет значение между вызовами функции и при изменении вызывает отрисовку заново. В React это состояние.

§3Состояние: хук useState

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

В поликлинике над кабинетом висит табло с номером талона. Если врач запишет номер следующего пациента на листке у себя на столе, в коридоре номер не изменится. Чтобы номер поменялся для всех, врач нажимает кнопку пульта, и табло показывает новое значение. Обычная переменная из §2 похожа на листок на столе, функция изменения из useState — на кнопку пульта.

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

Состояние (state) — данные компонента, которые React хранит между отрисовками и при изменении которых отрисовывает компонент заново. Состояние создаётся функцией useState из библиотеки React. Функции React, имена которых начинаются с use, называются хуками (от англ. hook — крючок): через них компонент пользуется механизмами React, например хранением состояния.

Создание состояния
  const [current, setCurrent] = useState(status)

useState(status) получает начальное значение — здесь статус из props — и возвращает пару: текущее значение и функцию для его изменения. Квадратные скобки слева раскладывают пару по двум переменным, так же как фигурные скобки в главе 1.3 распаковывали объект props. Имена выбирает разработчик; по договорённости вторая переменная называется как первая с приставкой set: current и setCurrent. Слово const означает, что переменной нельзя присвоить другое значение: значение меняется только через setCurrent.

Вызов setCurrent('В работе') делает две вещи. Он сохраняет новое значение в React и сообщает React, что компонент нужно отрисовать заново. При следующем вызове функции RequestCard строка с useState вернёт уже 'В работе'; начальное значение используется только при первой отрисовке.

  1. Замените содержимое RequestCard.tsx
    zayavki/src/components/RequestCard.tsx
    import { useState } from 'react'
    
    type Status = 'Новая' | 'В работе' | 'Готово'
    
    type RequestCardProps = {
      title: string
      room: string
      details: string
      status?: Status
    }
    
    function RequestCard({ title, room, details, status = 'Новая' }: RequestCardProps) {
      const [current, setCurrent] = useState(status)
    
      function handleClick() {
        if (current === 'Новая') {
          setCurrent('В работе')
        } else {
          setCurrent('Готово')
        }
      }
    
      return (
        <article className="card">
          <div className="card__top">
            <span className="card__room">каб. {room}</span>
            <span className="card__status">{current}</span>
          </div>
          <h3>{title}</h3>
          <p>{details}</p>
          <button onClick={handleClick} disabled={current === 'Готово'}>
            {current === 'Новая' ? 'Взять в работу' : 'Завершить'}
          </button>
        </article>
      )
    }
    
    export default RequestCard
    VS Code: RequestCard.tsx с импортом useState в строке 1, const current setCurrent = useState(status) в строке 13 и обработчиком с if и else
    Так должен выглядеть RequestCard.tsx: импорт useState в первой строке, состояние в строке 13
  2. Проверьте кнопки

    Нажмите «Взять в работу» в первой карточке и «Взять в работу», затем «Завершить» во второй.

    Доска заявок: первая карточка В работе с кнопкой Завершить, вторая и третья Готово с серыми выключенными кнопками
    Так должна выглядеть страница: первая заявка «В работе», вторая и третья «Готово», их кнопки выключены

Первая строка import { useState } from 'react' подключает хук из библиотеки React. Фигурные скобки в импорте означают, что берётся функция с именем useState: библиотека React содержит много функций, и в файл подключают только нужные.

В обработчике стоит условие if … else. Знак === сравнивает два значения и даёт true, если они равны. Если текущий статус «Новая», он становится «В работе», иначе — «Готово». Так одна кнопка проводит заявку по двум шагам.

У кнопки появился второй атрибут: disabled={current === 'Готово'}. Атрибут disabled выключает кнопку, если его значение true: выключенная кнопка не нажимается, и правило .card button:disabled из первого шага красит её в серый цвет. Заявку в статусе «Готово» дальше переводить некуда, поэтому кнопка выключена.

Текст кнопки задан выражением {current === 'Новая' ? 'Взять в работу' : 'Завершить'}. Это тернарный оператор — условие в одну строку: условие ? значение_если_да : значение_если_нет. Пока заявка новая, на кнопке «Взять в работу», после этого — «Завершить». Условие с if внутри JSX записать нельзя, потому что в фигурных скобках JSX допускается только выражение, дающее значение; тернарный оператор — выражение.

Значение меняется к следующей отрисовке

Если сразу после setCurrent('В работе') в том же обработчике напечатать console.log(current), в консоли будет прежнее значение, «Новая». Вызов setCurrent не меняет переменную current в текущем вызове функции: новое значение она получит при следующей отрисовке. Поэтому в обработчике решение принимается по значению, которое было на экране в момент щелчка.

Вызов обработчика вместо передачи

В §1 говорилось, что после имени обработчика в onClick скобки не ставятся. Если в этой версии карточки написать onClick={handleClick()} и сохранить файл, страница станет пустой, а в консоли появится ошибка.

Пустая страница, в консоли ошибка Uncaught Error: Too many re-renders и предупреждение An error occurred in the RequestCard component
При onClick={handleClick()} страница пустая, в консоли — Too many re-renders

Ошибка Too many re-renders. React limits the number of renders to prevent an infinite loop переводится как «слишком много повторных отрисовок; React ограничивает их число, чтобы не допустить бесконечного цикла». Со скобками handleClick выполняется во время отрисовки и вызывает setCurrent. Вызов setCurrent требует новой отрисовки, в ней снова выполняется handleClick, и цикл не заканчивается. Строка ниже, An error occurred in the <RequestCard> component, указывает компонент, в котором произошла ошибка. Ту же ошибку до запуска показывает редактор: Type 'void' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined' — «результат функции нельзя передать туда, где ожидается обработчик щелчка». Уберите скобки и сохраните.

Кнопки работают. Следующий раздел показывает шаги между щелчком и новым статусом на экране.

§4Как React обновляет страницу

Между щелчком и новым статусом на экране проходит пять шагов. Схема ниже показывает их на первой карточке.

Шаги от щелчка до нового статуса на экране

1щелчокпо кнопке 2 handleClick()браузер вызывает 3 setCurrent('В работе') 4 RequestCard() зановоcurrent = 'В работе' 5 страницановый текст статуса React сравнивает новую разметку с прежней и меняет на странице только текст статуса и кнопки

Шаги 1–3 выполняет код обработчика. На шаге 4 React вызывает функцию компонента заново, и useState возвращает новое значение. На шаге 5 изменения попадают на страницу.

На шаге 5 React сравнивает разметку, которую функция вернула сейчас, с разметкой прошлой отрисовки и меняет в браузере только отличия — здесь текст статуса и текст кнопки. Остальная страница не перестраивается. Повторный вызов функции компонента React выполняет при каждом изменении состояния. Это та же формула из главы 0.1: интерфейс — функция от состояния, UI = f(state). Разработчик описывает, как выглядит карточка при данном статусе, а перенос изменений на страницу выполняет React.

У каждой карточки своё состояние

На снимке в §3 первая карточка перешла в «В работе», вторая — в «Готово», и друг на друга они не повлияли. Состояние привязано к каждому вызову RequestCard на странице. React хранит отдельную пару «значение и функция изменения» для каждой из трёх карточек и при отрисовке выдаёт каждой её собственное значение. Тот же компонент, вызванный в другом месте, начнёт со своего начального значения.

Каждая карточка хранит свой статус. Следующая задача — вывести в шапке число новых заявок, а для этого статусы нужны в одном месте.

§5Состояние в родителе

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

В ИТ-отделе три мастера, и каждый отмечает свои заявки в личном блокноте. Начальник отдела хочет знать, сколько заявок ещё никто не взял, и ему приходится обходить всех мастеров. Если журнал заявок лежит на столе у диспетчера, ответ виден сразу, а мастер, взяв заявку, звонит диспетчеру, и тот делает отметку в журнале.

Мастер звонит и сообщает, что взял заявку, — так карточка вызывает функцию, переданную ей от App.

Шапка доски должна показывать «Новых заявок: 1». Для этого числа нужны статусы всех трёх заявок, а сейчас каждый статус хранится внутри своей карточки, и ни App, ни Header к нему доступа не имеют: данные в React передаются только от родителя к ребёнку (глава 1.3, §6). Решение — хранить состояние в ближайшем общем родителе тех компонентов, которым оно нужно. Для карточек и шапки это App. Перенос состояния из дочерних компонентов в родителя называется подъёмом состояния (lifting state up).

После подъёма карточка снова получает статус через props, как в главе 1.3. Но менять его она не может: props только для чтения. Поэтому вместе со статусом родитель передаёт карточке функцию — prop onNext. Карточка вызывает её при щелчке, а функция в App меняет состояние. Prop с функцией, через который ребёнок сообщает родителю о событии, по договорённости называют с приставкой on: onNext, onDelete, onSave.

Передача данных и событий между App, шапкой и карточками

Appstatus1, status2, status3newCount = число «Новая» HeaderНовых: 1 newCount status, onNext RequestCard RequestCard RequestCard onNext()при щелчке сплошная линия — props от родителя; пунктир — вызов функции, переданной родителем

Статусы хранятся в App. Шапка получает готовое число, карточки — статус и функцию onNext. Щелчок в карточке вызывает onNext, App меняет состояние и отрисовывается заново вместе с шапкой и карточками.

  1. Добавьте стиль счётчика

    В конец src/index.css добавьте правило и сохраните.

    zayavki/src/index.css · в конец файла
    .header__count {
      display: inline-block;
      margin-top: 12px;
      padding: 3px 12px;
      border-radius: 12px;
      background: #2f6fde;
      font-size: 14px;
    }
  2. Шапка получает число

    Замените содержимое src/components/Header.tsx и сохраните.

    zayavki/src/components/Header.tsx
    type HeaderProps = {
      newCount: number
    }
    
    function Header({ newCount }: HeaderProps) {
      return (
        <header className="header">
          <h1>Доска заявок</h1>
          <p>ИТ-отдел колледжа · заявки студентов и преподавателей</p>
          <span className="header__count">Новых заявок: {newCount}</span>
        </header>
      )
    }
    
    export default Header

    После сохранения VS Code подчеркнёт <Header /> в App.tsx: шапке не передан обязательный prop newCount. Это исправит шаг 4.

  3. Карточка получает статус и функцию

    Замените содержимое src/components/RequestCard.tsx и сохраните.

    zayavki/src/components/RequestCard.tsx
    export type Status = 'Новая' | 'В работе' | 'Готово'
    
    type RequestCardProps = {
      title: string
      room: string
      details: string
      status: Status
      onNext: () => void
    }
    
    function RequestCard({ title, room, details, status, onNext }: 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>
          <button onClick={onNext} disabled={status === 'Готово'}>
            {status === 'Новая' ? 'Взять в работу' : 'Завершить'}
          </button>
        </article>
      )
    }
    
    export default RequestCard

    В App.tsx подчеркнутся три вызова карточки: им не передан onNext. Их тоже исправит шаг 4.

  4. Состояние в App

    Замените содержимое src/App.tsx и сохраните.

    zayavki/src/App.tsx
    import { useState } from 'react'
    import Header from './components/Header'
    import RequestForm from './components/RequestForm'
    import RequestCard, { type Status } from './components/RequestCard'
    
    function nextStatus(status: Status): Status {
      if (status === 'Новая') {
        return 'В работе'
      }
      return 'Готово'
    }
    
    function App() {
      const [status1, setStatus1] = useState<Status>('Новая')
      const [status2, setStatus2] = useState<Status>('В работе')
      const [status3, setStatus3] = useState<Status>('Готово')
    
      const newCount = [status1, status2, status3].filter((s) => s === 'Новая').length
    
      return (
        <>
          <Header newCount={newCount} />
          <main className="board">
            <RequestForm />
            <section className="list">
              <h2>Заявки</h2>
              <RequestCard
                title="Не работает проектор"
                room="214"
                details="Проектор включается, но изображения на экране нет."
                status={status1}
                onNext={() => setStatus1(nextStatus(status1))}
              />
              <RequestCard
                title="Нет интернета"
                room="305"
                details="На всех компьютерах в кабинете не открываются сайты."
                status={status2}
                onNext={() => setStatus2(nextStatus(status2))}
              />
              <RequestCard
                title="Замятие бумаги в принтере"
                room="112"
                details="Принтер пишет «замятие бумаги», лоток пустой."
                status={status3}
                onNext={() => setStatus3(nextStatus(status3))}
              />
            </section>
          </main>
        </>
      )
    }
    
    export default App
    VS Code: App.tsx с функцией nextStatus, тремя useState и newCount, счётчик ошибок в строке состояния 0
    Так должен выглядеть App.tsx: функция nextStatus, три состояния, подсчёт newCount; счётчик ошибок внизу — 0
  5. Проверьте счётчик

    Обновите страницу в браузере. В шапке — «Новых заявок: 1». Нажмите «Взять в работу» в первой карточке.

    Доска заявок: в шапке Новых заявок: 1, первая карточка Новая
    До щелчка: одна новая заявка
    Доска заявок: в шапке Новых заявок: 0, первая карточка В работе с кнопкой Завершить
    После щелчка: первая заявка «В работе», в шапке «Новых заявок: 0»

Разбор RequestCard.tsx

Из карточки удалены useState и handleClick: своего состояния у неё больше нет. В типе props два изменения. status: Status снова обязательный, без ? и без значения по умолчанию, потому что статус теперь всегда приходит из App. Новое поле onNext: () => void описывает функцию: пустые скобки — функция без параметров, void после стрелки — функция ничего не возвращает. Кнопка получает эту функцию напрямую: onClick={onNext}. Перед type Status появилось слово export: тип нужен в App.tsx, и его нужно сделать доступным из других файлов.

Разбор App.tsx

Четвёртая строка импортирует из одного файла два имени: RequestCard — экспорт по умолчанию, без скобок, и Status — именованный экспорт, в фигурных скобках. Слово type перед Status отмечает, что подключается только тип. Описания типов удаляются при переводе TypeScript в JavaScript, и настройки проекта Vite требуют отмечать такие импорты явно; без слова type редактор покажет ошибку TS1484.

Функция nextStatus записана вне App: состояние и props в ней не используются, она вычисляет следующий статус по текущему. Запись (status: Status): Status означает «принимает статус и возвращает статус». Логика та же, что была в handleClick карточки: из «Новая» — «В работе», из остальных — «Готово».

Три строки с useState создают три состояния — по одному на заявку. В угловых скобках useState<Status> указан тип значения. Без них TypeScript вывел бы из начального значения 'Новая' тип string, то есть любую строку, и передать такое значение в prop status типа Status было бы нельзя. В карточке в §3 угловые скобки не требовались: начальным значением был prop, у которого тип Status уже указан.

Строка с newCount считает новые заявки. [status1, status2, status3] собирает три статуса в массив — список значений в квадратных скобках. Метод filter оставляет в массиве только элементы, для которых условие s === 'Новая' истинно, а .length даёт их количество. Работа с массивами подробно разобрана в главе 1.5. Число newCount не хранится в состоянии: оно вычисляется из состояния при каждой отрисовке App. Данные, которые можно вычислить из уже имеющихся, в отдельное состояние не записывают, иначе два значения пришлось бы обновлять вместе и они могли бы разойтись.

Каждой карточке передаются status={status1} и onNext={() => setStatus1(nextStatus(status1))}. Запись () => … — стрелочная функция: короткая функция без имени, у которой после стрелки записано, что она делает. Здесь она нужна, потому что onNext должен получить функцию, которую карточка вызовет позже. Запись без стрелки, onNext={setStatus1(nextStatus(status1))}, вызвала бы setStatus1 сразу при отрисовке — та же ошибка, что с handleClick() в §3.

Цепочка при щелчке в первой карточке такая. Кнопка вызывает onNext, то есть стрелочную функцию из App. Та вычисляет nextStatus('Новая'), получает «В работе» и вызывает setStatus1. React отрисовывает App заново: status1 равен «В работе», newCount пересчитывается и становится 0, шапка и карточки получают новые props.

Одно место хранения статуса

Начальное значение useState(status) используется только при первой отрисовке. Если бы карточка хранила статус у себя, а App потом передал бы ей другой status, карточка продолжала бы показывать своё значение. У одного статуса было бы два места хранения, и они могли бы разойтись — та же ошибка, что в витрине с ручным обновлением из главы 0.1. Поэтому каждое значение хранится в одном компоненте, а остальные получают его через props.

В App.tsx остался повтор: три состояния и три почти одинаковых вызова карточки. Для доски с десятками заявок статусы хранят одним списком и выводят карточки из него циклом. Это тема главы 1.5.

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

Сообщения ниже получены в проекте zayavki: ошибки типов — командой tsc -b, предупреждения — командой npm run lint, ошибка «Too many re-renders» — в консоли браузера.

Что видноПочемуЧто сделать
Пустая страница, в консоли Too many re-renders. React limits the number of renders to prevent an infinite loop.Обработчик вызван при отрисовке: onClick={handleClick()}Передать функцию без скобок: onClick={handleClick} или onClick={() => …}
Type 'void' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'Та же ошибка, найденная редактором до запускаУбрать скобки после имени обработчика
Type 'void' is not assignable to type '() => void'В prop-функцию передан результат вызова: onNext={setStatus1(…)}Обернуть вызов в стрелочную функцию: onNext={() => setStatus1(…)}
Cannot find name 'useState'Нет импорта хукаДобавить первой строкой import { useState } from 'react'
Линтер: react(immutability): Cannot reassign variable after render completesОбработчик присваивает новое значение обычной переменной или propХранить значение в состоянии и менять через функцию из useState
Линтер: react-hooks(rules-of-hooks): React Hook "useState" is called conditionallyuseState вызван внутри if. React различает состояния компонента по порядку вызовов хуков, и этот порядок должен быть одинаковым при каждой отрисовкеВызывать хуки в начале функции компонента, вне условий и циклов
'Status' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabledТип импортирован без слова typeЗаписать import RequestCard, { type Status } from …
Property 'onNext' is missing in type '{ … }' but required in type 'RequestCardProps'Карточке не передана функция onNextДобавить атрибут onNext={() => …}
Кнопка нажимается, в консоли сообщение, на странице ничего не меняетсяЗначение хранится в обычной переменнойПеренести значение в состояние (§3)

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

Что такое обработчик события и как подключить его к кнопке?

Функция, которую браузер вызывает, когда произошло событие. К кнопке её подключают атрибутом onClick, передавая саму функцию без скобок: <button onClick={handleClick}>.

Почему присваивание обычной переменной в обработчике не меняет страницу?

Страница меняется, когда React вызывает функцию компонента заново, а присваивание переменной такого вызова не вызывает. Кроме того, обычная переменная создаётся заново при каждом вызове функции, и при следующей отрисовке значение вернулось бы к начальному.

Что возвращает useState('Новая') и что делает функция изменения?

Пару: текущее значение и функцию для его изменения. Функция изменения сохраняет новое значение в React и запускает повторную отрисовку компонента; при ней useState вернёт новое значение.

Чем отличаются onClick={handleClick} и onClick={handleClick()}?

Первая запись передаёт кнопке функцию, и браузер вызовет её при щелчке. Вторая вызывает функцию сразу при отрисовке и передаёт кнопке результат. Если функция меняет состояние, вторая запись даёт бесконечный цикл отрисовок и ошибку «Too many re-renders».

Шапке нужно число новых заявок, а статусы хранятся в карточках. Что сделать?

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

Почему newCount не хранится в отдельном состоянии?

Его можно вычислить из статусов, которые уже хранятся в состоянии. Вычисление выполняется при каждой отрисовке App, и число всегда соответствует статусам. Отдельное состояние пришлось бы обновлять вместе со статусами, и значения могли бы разойтись.

Добавьте в карточку кнопку «Вернуть», которая переводит заявку обратно в «Новая». Что нужно изменить?

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

  1. в тип props карточки добавить onReset: () => void и принять onReset в параметре;
  2. добавить в карточку <button onClick={onReset}>Вернуть</button>;
  3. в App передать каждой карточке функцию: onReset={() => setStatus1('Новая')}, для второй — setStatus2, для третьей — setStatus3;
  4. проверить, что счётчик в шапке увеличивается после нажатия.

Шпаргалка

Обработчик<button onClick={handleClick}>
Состояниеconst [value, setValue] = useState(0)
Импорт хукаimport { useState } from 'react'
Тип состоянияuseState<Status>('Новая')
Функция в propsonNext: () => void
Передача функцииonNext={() => setStatus1('Готово')}
Условие в JSX{ok ? 'Да' : 'Нет'}
Выключить кнопкуdisabled={status === 'Готово'}

Итог главы

Событие — действие пользователя, о котором сообщает браузер; обработчик — функция, подключённая к тегу атрибутом onClick, onChange или другим с приставкой on. Функция передаётся без скобок, иначе она выполнится при отрисовке. Изменение обычной переменной не меняет страницу: React обновляет разметку, только когда вызывает функцию компонента заново. Повторную отрисовку запускает изменение состояния — значения из хука useState, которое React хранит между отрисовками; меняется оно функцией вида setValue. После изменения React вызывает компонент, сравнивает новую разметку с прежней и меняет на странице только отличия. Если состояние нужно нескольким компонентам, его хранят в их общем родителе: родитель передаёт детям данные через props, а дети сообщают о событиях, вызывая переданные функции. Доска заявок переводит заявки по статусам кнопкой в карточке и показывает в шапке число новых.

Источники

  1. react.dev — Responding to Events
  2. react.dev — State: A Component's Memory
  3. react.dev — Render and Commit
  4. react.dev — State as a Snapshot
  5. react.dev — Sharing State Between Components
  6. react.dev — Rules of Hooks
  7. developer.chrome.com — Console overview
Справочник по Angular и React © Макаров Максим Николаевич