Программирование с ИИ
ENRU
10.3

Пропсы и состояние

откуда берутся данные и почему счётчик считает не так

Что это

Два источника данных в компоненте, и путать их нельзя:

  • пропсы — пришли снаружи, от родителя. Менять их нельзя.
  • состояние — живёт внутри компонента. Меняется, и при каждом изменении компонент рисуется заново.

Типы здесь дают больше всего пользы во всей главе: пропсы — это граница между компонентами, и TypeScript следит за ней строго.

Как выглядит в коде

const [count, setCount] = useState(0);

useState возвращает пару: текущее значение и функцию, которая его меняет. Квадратные скобки — распаковка массива из двух элементов.

Присваивать напрямую нельзя. count = 5 ничего не перерисует: React узнаёт об изменении только через setCount.

Что ловят типы

Вот настоящие сообщения компилятора на четыре типичные ошибки:

Забыт обязательный пропс:

<CityCard name="Oslo" temp={4} />
измерено
error TS2741: Property 'onRemove' is missing in type
'{ name: string; temp: number; }' but required in type 'CityCardProps'.

Не тот тип:

<CityCard name="Oslo" temp="4" onRemove={() => {}} />
измерено
error TS2322: Type 'string' is not assignable to type 'number'.

Не та сигнатура обработчика:

<CityCard name="Oslo" temp={4} onRemove={(id: number) => {}} />
измерено
error TS2322: Type '(id: number) => void' is not assignable
to type '(name: string) => void'.

Забыта проверка на пустоту:

function T({ temp }: { temp: number | null }) { return <p>{temp.toFixed(1)}</p>; }
измерено
error TS18047: 'temp' is possibly 'null'.

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

Ловушка, которую надо увидеть

Три кнопки, одна и та же задача — прибавить единицу три раза:

<button onClick={() => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }}>
  три раза подряд
</button>

<button onClick={() => { setCount(c => c + 1); setCount(c => c + 1); setCount(c => c + 1); }}>
  три раза через функцию
</button>

Измерено в браузере:

измерено
старт:                          счёт: 0 | отрисовок: 1
после одного +1:                счёт: 1 | отрисовок: 2
после «три раза подряд»:        счёт: 2 | отрисовок: 3
после «три раза через функцию»: счёт: 5 | отрисовок: 4

Три вызова подряд дали +1, а не +3. Три вызова через функцию — +3.

Причина: внутри обработчика count — это значение на момент отрисовки, и оно не меняется от вызовов setCount. Три раза подряд выполняется setCount(0 + 1). Запись setCount(c => c + 1) просит React взять актуальное значение сам.

Обрати внимание и на счётчик отрисовок: три вызова дали одну перерисовку. React собирает изменения и применяет разом — это называется пакетированием.

Где должно жить состояние

Правило: в ближайшем общем родителе тех компонентов, которым оно нужно.

Если состояние нужно двум соседним компонентам, его поднимают к родителю, а вниз передают пропсами — само значение и функцию изменения:

<CityCard name={c.name} temp={c.temp} onRemove={handleRemove} />

Это и есть ответ на вопрос «почему так много пропсов»: данные идут сверху вниз, события — снизу вверх.

Как сказать ИИ

Запрос
Состояние списка городов сейчас в CityCard. Подними его
в App, вниз передавай значение и обработчик. Типы пропсов
опиши интерфейсом.
Запрос
В этом обработчике несколько setState подряд, зависящих
от предыдущего значения. Перепиши через функциональную форму
setX(prev => ...).
Запрос
Опиши пропсы этого компонента интерфейсом: что обязательно,
что нет, какие типы у обработчиков. Реализацию не трогай.

На что смотреть

Пропс изменяют внутри компонента. props.items.push(...) — пропсы менять нельзя, это данные родителя.

setState подряд с зависимостью от старого значения. Разобрано выше.

Состояние дублируется. Одно и то же лежит и в родителе, и в ребёнке — рассинхронизируется гарантированно.

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

Пропсы без типов или с any. Теряется вся польза из статьи 9.5.

Десять пропсов у одного компонента. Признак, что компонент делает слишком много (статья 2.5 про число аргументов).

Шпаргалка

  • Пропсы приходят сверху и неизменяемы, состояние живёт внутри.

  • Присваивание напрямую ничего не перерисует — только через setX.

  • Новое значение зависит от старого — функциональная форма setX(prev => ...).

  • Три setCount(count + 1) подряд дают +1, а не +3.

  • Несколько изменений дают одну перерисовку: React пакетирует.

  • Общее состояние — в ближайшем общем родителе.