Ни div, ни класса, ни каскада
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
В прошлой статье элементы React Native поменялись на бумаге, таблицей: View вместо div, Text вместо p. Здесь переносим живую вёрстку. Карточку упражнения вы сверстали бы в части II как div с заголовком, абзацем, кнопкой и CSS-классами, а в части IV — тем же JSX с className. Перенесём её в React Native в лоб и посмотрим, кто что заметит. Сюрпризов три: div в предпросмотре работает, CSS почти целиком ловит компилятор, а самую частую ошибку переноса — строку прямо в контейнере — не ловит ни один тип, и сообщает о ней только консоль. Этаж статьи — React Native: примитивы и их рендерер; Expo здесь ни при чём.
Возьмём карточку «Жим лёжа» такой, какой она была бы в части IV: div.card, внутри заголовок h3, строка с числом подходов и кнопка, которая это число увеличивает. Классы — зацепки для CSS, как в доске задач из части II, где карточка была li.card с span.card-title и кнопкой button.card-del. Первый шаг переноса самый ленивый: вставить этот JSX в пример на React Native, ничего не меняя.
Прогноз: сколько ошибок покажет компилятор, сработает ли нажатие и какие теги окажутся под карточкой, под View и под кнопкой? Последнюю строку печатает hostTag — помощник из подложки: сам пример в DOM заглядывать не должен, почему — ниже.
Ни одной диагностики, нажатие сработало, а под всеми тремя — обычные теги HTML. Теги объявлены в самих типах React, в JSX.IntrinsicElements из пакета @types/react, и библиотека DOM для этого не нужна. Поэтому div с className компилятор считает законным JSX где угодно, в том числе рядом с View. А предпросмотр — это react-native-web поверх react-dom: View здесь сам становится div, и настоящий div рядом с ним ничем не хуже.
Помощник hostTag живёт в подложке, а не в примере, намеренно. Типы части V собраны без библиотеки DOM, потому что на телефоне DOM нет, и на строку с document компилятор ответит «Не удается найти имя "document". Вы хотите изменить целевую библиотеку? Попробуйте изменить параметр компилятора "lib", включив "dom"» — совет здесь как раз неуместный. Выполнится строка всё равно — предпросмотр стоит на настоящей странице, — а на телефоне упадёт с ReferenceError. Поэтому под предпросмотр мы заглядываем только через такие помощники и помним, что смотрим на веб, а не на телефон.
На телефоне DOM нет, и div там не значит ничего. Для каждого элемента со строковым типом рендерер React Native спрашивает конфигурацию нативного вида у реестра: в React Native 0.86 это getViewConfigForType в ReactFabric-dev.js, а сам реестр — Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js. RCTView, которым на телефоне рисуется View, в реестре есть, а div, h3 и button — нет, и реестр бросает исключение по шаблону «View config getter callback for component `%s` must be a function (received `%s`).%s». Для имени со строчной буквы он дописывает подсказку «Make sure to start component names with a capital letter.»
Это исключение посреди рендера, а не предупреждение: без границы ошибок React снимает всё дерево — так же, как в статье «Границы ошибок — единственное место, где ещё нужен класс». И назовёт сообщение не div. Нативные элементы создаются снизу вверх, от листьев к корню, поэтому первым рендерер споткнётся о h3. А ещё раньше, на самом листе, консоль пожалуется на строку «Жим лёжа»: над ней нет нативного Text, и для телефона она стоит вне текста — ровно та ошибка, которой посвящён следующий раздел.
Предпросмотр прощает HTML-теги потому, что сам сделан из HTML. Это первое место, где пример работает, а телефон — нет, но не последнее: следующая статья проведёт эту границу целиком.
Перенесём честно: div становится View, h3 и p — Text, button — Pressable. И почти каждый в первый раз делает одно и то же: надпись кнопки остаётся там, где стояла в <button>, — прямо внутри Pressable.
Прогноз: сколько диагностик у компилятора, сколько строк в консоли после двух нажатий и видна ли надпись «Записать подход» на экране примера?
Компилятор снова промолчал, а консоль после каждого рендера печатает красную строку «Unexpected text node: Записать подход. A text node cannot be a child of a <View>.». Типы здесь бессильны по устройству. children у View и Pressable принимают React.ReactNode, а строка — законный ReactNode. Правило «строка — только в Text» говорит не о типе пропса, а о том, какой нативный вид окажется над строкой в готовом дереве, и типом пропса его не выразить.
Печатает эту строку react-native-web. Pressable рисует себя через View, а View в отладочной сборке перебирает своих детей и ругается на каждого, кто оказался строкой. Проверка стоит в рендере и повторяется на каждой отрисовке: три рендера — три строки, а в списке из сотни таких кнопок было бы по сотне за рендер. И она закрыта условием process.env.NODE_ENV !== "production": в песочницу учебника нарочно положена отладочная сборка, в веб-сборке вашего приложения этой строки не будет.
Надпись при этом на экране примера видна — и здесь предпросмотр врёт. В HTML текст внутри div законен, браузер его и нарисовал. На телефоне так не будет. Рендерер React Native 0.86 создаёт для строки текстовый узел и, не найдя над ним Text, пишет в отладочной сборке «Text strings must be rendered within a <Text> component.» — LogBox показывает это красной плашкой внизу экрана. А сама строка не появится: нативный узел сырого текста — не вид и в раскладке не участвует, собрать такие узлы в рисуемую строку умеет только нативный Text. В релизной сборке проверки нет вовсе, и кнопка молча остаётся без надписи.
| Экран примера | Телефон, отладка | Телефон, релиз | |
|---|---|---|---|
| Сообщение | «Unexpected text node: … cannot be a child of a <View>.» | «Text strings must be rendered within a <Text> component.» | нет |
| Надпись на экране | видна | не видна | не видна |
| Кто сообщает | View из react-native-web | рендерер React Native | — |
Первая — в react-native-web, та, что пишет в консоль примера. Вторая — в рендерере React Native, та, что сработает на телефоне. Обе сокращены, строки приведены к двойным кавычкам.
Разница важнее, чем кажется. Предпросмотр проверяет прямых детей одного View, и только строки. Телефон проверяет каждый текстовый узел, откуда бы тот ни взялся, — и строку, и число. Поэтому почти всё, что поймал предпросмотр, потеряется и на телефоне, а обратное неверно. Почти — из-за пустой строки. {title && …} при title === "" кладёт в View строку "", и предпросмотр печатает «Unexpected text node: . …» с пустым местом вместо текста — мы проверили запуском. А на телефоне не случится ничего: для пустой строки React текстового узла не создаёт вовсе, в том же ReactFabric-dev.js условие так и записано — "" !== newChild. Для числа создаёт.
Проверим эту разницу на строках, которые попадают в View незаметно. Здесь пять таких мест. Прогноз: сколько красных строк напечатает пример и какой текст будет в каждой?
Три красные строки на пять ошибок. Первая — только « кг», без числа: JSX разрезал {weight} кг на двух детей, число 60 и строку " кг", а проверка смотрит лишь на строки. Поэтому одинокий {reps} прошёл молча. Вторая — одинокий пробел. JSX выбрасывает пробелы, в которых есть перевод строки, но пробел между двумя тегами на одной строке — полноправный ребёнок, и в ряду <Text>60 кг</Text> <Text>× 10</Text> он оказался прямо в View. Третья — шаблонная строка: она одна и целиком строка.
А <Label>отдых</Label> проверка не видит вовсе. Для View это элемент, и что он отрисует, станет известно позже, когда View уже проверил своих детей. Так же невидима строка во фрагменте <>отдых</> — мы проверили и это. На телефоне каждое из пяти мест даст ошибку, и ни одна из этих строк на экран не попадёт.
| Что в JSX | Компилятор | Экран примера | Телефон |
|---|---|---|---|
<Pressable>Записать подход</Pressable> | молчит | красная строка | ошибка, текста нет |
<View>{weight} кг</View> | молчит | только про « кг» | ошибка на каждую часть |
<View>{reps}</View> | молчит | молчит | ошибка, ноля нет |
<Text>…</Text> <Text>…</Text> на одной строке | молчит | про пробел | ошибка на пробел |
| строка через свой компонент или фрагмент | молчит | молчит | ошибка, текста нет |
{title && …} при title === "" | молчит | красная строка с пустым текстом | ничего: для "" текстового узла нет |
Поэтому правило проверяют глазами, а не консолью: у каждой строки и каждого числа ближайшим нативным предком должен быть Text. Смотрите на всё, что стоит между тегами View и Pressable: текст, пробел в одной строке с тегами, выражение в фигурных скобках, которое может дать строку или число. Всё это уходит в Text, а куски одной фразы — в один общий Text: внутри него JSX режет текст как угодно.
Ноль из условия sets && … — тот же одинокий номер. Что && отдаёт ноль и React его рисует, было в части IV, в статье «Выражения, условия и списки в разметке»; новое здесь другое: предпросмотр промолчит, а телефон — нет. Как с этим обходиться в стилях, разобрано в статье про StyleSheet.create.
Остался CSS. Класс переносить некуда: className у примитивов нет, стиль приезжает пропсом style — объектом, обычно из StyleSheet.create. Нет и каскада. Правило .card .card-title в CSS дотягивается до потомков, а селекторов в React Native нет вовсе: стиль действует ровно на тот элемент, которому передан, и имя title в листе стилей — просто ключ объекта. Что при этом наследуется, а что нет, разбирает вторая глава, статья «Каскада нет, наследования нет — кроме одного случая». Здесь перенесём сами правила так, как их переносит рука: имена в camelCase, значения как были.
Прогноз: сколько ошибок покажет компилятор и какое свойство, которого в React Native нет, он пропустит молча?
Четыре диагностики. display не бывает grid — только flex, none и contents. Единиц нет: "16px" не проходит, размеры здесь числа, и что это за единицы, разберёт вторая глава. margin принимает одно значение, а сокращения font нет совсем. Здесь настоящие типы работают на вас: почти каждая CSS-привычка упирается в компилятор. Три красные строки в консоли — проверка StyleSheet.create из react-native-web, о ней статья «StyleSheet.create: пишем свой, смотрим настоящий».
А про borderBottom компилятор промолчал, хотя такого свойства в React Native нет. Лишнее свойство объектного литерала он показывает, когда всё остальное в объекте сошлось с типом, а в card и так три ошибки. Исправьте их — и вместо четырёх диагностик останется одна, длинная, про весь аргумент StyleSheet.create, а borderBottom назван в самом её конце. Как это выглядит — во второй задаче.
Бывает и хуже — привычка, которую не заметит никто. В CSS line-height: 1.4 — множитель к размеру шрифта. В React Native lineHeight — абсолютная высота строки, и 1.4 — это 1,4 единицы, а не 1,4 размера шрифта. Число есть число: компилятор доволен, консоль молчит. Прогноз: какую line-height вычислит браузер для двух абзацев? Сравнивает их второй помощник подложки, hostStyle.
1.4px против 25px: первый абзац на экране примера слипся в одну полосу, строки легли друг на друга. На телефоне будет то же самое, только в единицах платформы. Лечится одним умножением: высота строки — это размер шрифта, умноженный на множитель из CSS, здесь 18 × 1.4 ≈ 25.Соберём карточку заново: текст — только в Text, надпись кнопки — тоже, стили — числами и длинными именами. Прогноз: сколько красных строк останется после нажатия?
Ни одной — и ни одной диагностики. border-bottom: 1px solid #ddd распался на borderBottomWidth и borderBottomColor, font — на fontSize и fontWeight, line-height: 1.4 стал lineHeight: 25, а display: grid просто исчез: каждый View и так flex-контейнер, причём раскладывает колонкой. Чем эти умолчания отличаются от CSS, разберёт статья «Flex с другими умолчаниями».
| Привычка из частей II и IV | В React Native | Кто заметит, если забыть |
|---|---|---|
div, h3, p, button | View, Text, Pressable | только телефон: исключение на первом же теге |
| текст прямо в контейнере или кнопке | только внутри Text | предпросмотр — строки среди прямых детей, телефон — всё |
className="card" | style={styles.card} | компилятор |
16px, display: grid, margin: 0 auto, font: … | 16, flex, alignSelf: "center", fontSize и fontWeight | компилятор |
border-bottom: 1px solid #ddd | borderBottomWidth и borderBottomColor | консоль предпросмотра; компилятор — когда остальное в объекте верно |
line-height: 1.4 | lineHeight: 25 — высота, а не множитель | никто, только глаза |
.card .card-title { … } | стиль у каждого элемента | ловить нечего: селекторов нет |
У нижней половины таблицы есть сторож — компилятор, а у верхней нет. Самые дорогие ошибки переноса, теги и текст вне Text, живут там, где типы не помогают, а предпросмотр видит их частично или вовсе прощает. Где ещё он расходится с телефоном и как это проверить запуском, — следующая статья, «Что в этой песочнице настоящее».
- HTML-теги в примере на React Native компилируются — их объявляют типы самого React — и работают в предпросмотре, где любой примитив и так
div. На телефоне реестр нативных видов не знаетdiv, и рендер обрывается исключением. - Строка и число живут только внутри
Text. Типы этого не выражают:childrenуViewиPressable—ReactNode, а правило касается готового дерева, а не пропса. - Предпросмотр ловит нарушение частично.
Viewиз react-native-web в отладочной сборке печатает «Unexpected text node: … A text node cannot be a child of a <View>.» на каждой отрисовке, но только для строк среди прямых детей; числа и текст через компонент или фрагмент проходят молча, а надпись всё равно рисуется. Пустую строку проверка, наоборот, ругает зря: из""текстового узла не выйдет и на телефоне. - На телефоне с React Native 0.86 рендерер проверяет каждый текстовый узел: в отладке LogBox показывает «Text strings must be rendered within a <Text> component.», в релизе проверки нет. В обоих случаях текст вне
Textна экран не попадает. - CSS переезжает в объект: имена в camelCase, размеры числами, длинные свойства вместо сокращений. Почти все привычки ловит компилятор, лишнее свойство — когда остальное в объекте верно.
lineHeight— высота, а не множитель, и её не ловит никто. - Классов и каскада нет: стиль действует только на тот элемент, которому передан. Что при этом наследуется — вторая глава.
1/4В примере на React Native стоит <View>{weight} кг</View>, weight равен 60. Что напечатает консоль предпросмотра?
2/4Почему <div className="card"> в примере на React Native и компилируется, и рисуется?
3/4Отладочная сборка на телефоне, в разметке <View>{reps}</View>, reps равен 0. Что вы увидите?
4/4Из CSS перенесли lineHeight: 1.4 при fontSize: 18. Что будет?
Четыре потерянные надписи
Строка подхода собрана так, как рука переносит её из HTML. В консоли три красные строки, но на телефоне текст потеряется в четырёх местах. Найдите четвёртое — то, о котором предпросмотр молчит, — и перепишите SetRow так, чтобы в консоли остался только «рендер списка», а на телефоне не пропало ни одного символа.
Три места показала консоль: точка после номера, пробел между двумя Text на одной строке и «Удалить» прямо в Pressable. Четвёртое — сам номер {set.n}. Это число, и проверка react-native-web его не видит, а рендерер телефона создаст для него текстовый узел вне Text. Надёжнее всего собрать строку подхода в один Text — внутри него JSX режет текст как угодно:
Консоль печатает только «рендер списка». Разнести номер, вес и повторы по разным Text тоже можно, но тогда пробел между ними должен жить внутри одного из них, а не между тегами.
Ошибка, которая ждала своей очереди
Строка списка свёрстана так, как её свёрстали бы в части II, и правило переехало в объект как было. Доведите стиль до нуля диагностик и нуля красных строк, а текст — до нормальной высоты строки. По дороге ответьте: какую ошибку компилятор показал не сразу и почему, а какую не показал никто?
Сначала компилятор видит две ошибки: display: "grid" и "12px". Уберите display (View и так flex) и замените "12px" на 12 — и вместо них появится одна новая, длинная, про весь аргумент StyleSheet.create. Нужное — в её конце: «Объектный литерал может использовать только известные свойства. "borderBottom" не существует в типе "ViewStyle | TextStyle | ImageStyle"». Лишнее свойство компилятор показывает, только когда остальное в объекте сошлось с типом. Консоль про borderBottom говорила с самого начала — это проверка StyleSheet из react-native-web.
Не показал никто lineHeight: 1.5: в CSS это множитель, а здесь — высота строки в полторы единицы. Нужно 16 × 1,5 = 24. После правок вывод — одна строка «рендер».