React Native

Базові компоненти React Native

View, Text, Image, Pressable, ScrollView, Safe Area та Platform — будівельні блоки екранів і mock-картка поїздки в Nomad

Базові компоненти React Native

Навіщо ця стаття

У попередніх матеріалах уже з’явилися Screen, AppText і Button. Під капотом вони зібрані з базових компонентів React Native. Тут ці «цеглини» розбираються окремо: що робить кожна, чим відрізняється від HTML, які типові помилки роблять люди з web-досвідом.

Після статті:

  1. Зрозуміло, навіщо View і чому текст лише в Text.
  2. Як показати зображення (Image).
  3. Як реагувати на натискання (Pressable).
  4. Коли потрібен ScrollView, а коли ще рано (списки — окрема стаття).
  5. Що таке safe area і навіщо Platform.
  6. У Nomad — mock-картки поїздок на домашньому екрані.
Головна думка. У браузері майже все — «коробка з вмістом» (div, span, img, button). У React Native різні ролі розділені на різні компоненти: контейнер (View), текст (Text), картинка (Image), зона натискання (Pressable). Змішати ролі «як у HTML» часто не виходить або дає попередження.
Живі прев’ю. У цій статті майже кожен примітив супроводжується ::react-native-preview — рендер через react-native-web у рамці телефону. Це не симулятор iOS/Android: layout і core-компоненти близькі до нативу, але тіні, шрифти й native modules можуть відрізнятися. У прев’ю доступні лише react і react-native (без @/…, expo-*, Router).

Місток з web React

У браузеріУ React NativeКоментар
div, section, mainViewКонтейнер для layout, без тексту всередині «просто рядком»
p, span, h1TextУсі видимі рядки для користувача
imgImageПотрібні розміри / стиль; джерело — локальний файл або URI
button, div onClickPressable (або старі Touchable*)onPress, не onClick
прокрутка сторінкиScrollView / спискиДовгий контент не «їде» сам, як document
CSS cursor, hoverмайже немаєТелефон — дотик, не миша
Найчастіша помилка після вебу. Написати {title} або 'Привіт'безпосередньо всередині View. У React Native рядок для UI має бути обгорнутий у Text (або в ваш AppText, який всередині використовує Text).

View — контейнер layout

Офіційна ідея з документації View: View — найфундаментальніший компонент UI. Це контейнер, який підтримує:

  • layout (у React Native — переважно Flexbox);
  • style (фон, відступи, межі, прозорість…);
  • частину обробки дотиків (responder-система; для кнопок у курсі беремо Pressable);
  • accessibility (підказки для скрінрідера).

На нативній стороні View відповідає «рідному» контейнеру платформи (UIView на iOS, android.view.View на Android тощо). Компонент розрахований на вкладеність: у нього може бути від 0 до багатьох вкладених елементів будь-якого типу (інші View, Text, Image…).

Документація радить стилізувати View через StyleSheet (ясність і дисципліна). Inline-стилі теж працюють, але в проєктах курсу — StyleSheet.create + tokens.

Мінімальний приклад

Зовнішній View — рядок; внутрішні — кольорові квадрати; текст — окремий Text, не «рядок у View».

TSXViewRow.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

Вкладеність: колонка, картка, «шапка»

View майже завжди гніздиться. Нижче — типова схема екрана: корінь → картка → шапка (рядок) + тіло.

TSXViewNesting.tsx
Android
9:41

Loading…

react-native-web · not a real device

Важливі props (не весь довідник)

Документація View містить довгий список props (accessibility, focus, responder…). Нижче — те, що реально потрібно на старті курсу.

style
ViewStyle | ViewStyle[]
Стилі контейнера. Масив стилів зливається зліва направо: пізніші перекривають попередні ([styles.base, condition && styles.active]).
pointerEvents
'auto' | 'none' | 'box-none' | 'box-only'
Чи може цей View бути ціллю дотику.
auto — так; none — ні (і вкладені елементи теж «прозорі» для дотику в класичному сенсі); box-none — сам View не ловить, вкладені елементи можуть; box-only — ловить лише «коробка», вкладені — ні. Корисно для оверлеїв і «дірявих» шарів.
onLayout
(event) => void
Викликається після монтування й при зміні розміру/позиції. У event.nativeEvent.layout{ x, y, width, height }. Зручно, коли треба виміряти блок (рідко на перших екранах).
testID
string
Ідентифікатор для e2e-тестів (Maestro/Detox пізніше). Увага: у docs зазначено, що деякі props на кшталт testID / nativeIDвимикають оптимізацію «layout-only view removal».
collapsable
boolean (Android-орієнтована оптимізація)
Порожні «лише для layout» view інколи прибирають з нативного дерева. collapsable={false} залишає вузол, якщо він потрібен для вимірювань/рефів. За замовчуванням оптимізація увімкнена.
hitSlop
Rect | number
Розширити зону старту дотику за межі видимого прямокутника (зручно для маленьких іконок). Зона не виходить за межі батька; при перекритті виграє z-index сусіда.
accessible / accessibilityLabel / accessibilityRole
a11y
Чи елемент «видимий» для assistive tech, який текст озвучити, яку роль повідомити (button, header, image…). Повний прохід a11y — окрема стаття; звичка ставити label на інтерактив — уже зараз.
removeClippedSubviews
boolean
Оптимізація для великих прокручуваних дерев: нативні дочірні view за межами екрана можуть відчіплятися. У docs є попередження про можливі баги з «зниклим» контентом — не вмикати навмання.

Playground: як style-пропси змінюють View

Більшість «зовнішності» View задається не окремими HTML-атрибутами, а полем style. Нижче перемикайте варіанти — той самий синій блок змінює flexDirection, borderRadius, opacity, padding.

TSXViewPropsPlayground.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

Аналогія.View — прозора коробка для зберігання: сама нічого не «говорить» користувачу текстом, але тримає форму layout. Усе, що треба прочитати, кладіть у Text. Props на кшталт pointerEvents / testIDне малюють пікселі — вони впливають на дотик і тести. Зовнішність майже завжди = style.
Глибока responder-модель (onStartShouldSetResponder, onResponderMove…) у docs View описана для низькорівневих жестів. У курсі для кнопок і карток використовуємо Pressable, а не ручний responder на кожному View.

Text — відображення тексту

За документацією Text: компонент для показу тексту. Підтримує вкладеність, стилі й навіть обробку натискань на самому тексті.

Головне правило (strict isolation)

У React Native не можна покласти текстовий вузол безпосередньо в View. Лише обгортка Text (або ваш AppText):

// ПОГАНО — runtime error / warning
<View>Привіт</View>

// ДОБРЕ
<View>
  <Text>Привіт</Text>
</View>
TSXTextRule.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

Це навмисно суворіше, ніж CSS inheritance у браузері. Офіційна мотивація (скорочено):

  1. Ізоляція компонентів — вигляд не повинен «витікати» з батьків через успадкований font, інакше компонент поводиться по-різному в різних місцях.
  2. Простіша реалізація — не треба тягнути fontFamily через усе дерево view.

Практичний висновок docs: зробіть свій MyAppText / AppText із базовими стилями й використовуйте його скрізь (саме так у Nomad).

Вкладений текст (nested text)

На iOS/Android форматований рядок (bold + color) — це нативні attributed/spannable strings. У RN обрали веб-парадигму: вкладені <Text>:

<Text style={{ fontWeight: 'bold' }}>
  Я жирний{' '}
  <Text style={{ color: 'red' }}>і червоний</Text>
</Text>

Під капотом це зводиться до одного нативного текстового блоку з діапазонами стилів.

TSXNestedText.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

Text усередині Text vs Text усередині View

КонтейнерПоведінка
<Text><Text>A</Text><Text>B</Text></Text>Текст inline: «A» і «B» течуть одним потоком, переносяться як один абзац
<View><Text>A</Text><Text>B</Text></View>Кожен Textокремий блок (як два абзаци / два flex-елементи)

Це критично для підписів «ціна · залишок» vs два рядки одне під одним.

TSXTextLayout.tsx
Android
9:41

Loading…

react-native-web · not a real device

Обрізання: numberOfLines + ellipsizeMode

Перемикайте скільки рядків і як обрізати — той самий довгий рядок виглядає інакше.

TSXTextEllipsisPlayground.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

Важливі props

numberOfLines
number (default 0)
Максимум рядків після layout. 0 — без обмеження. Часто разом з ellipsizeMode.
ellipsizeMode
'head' | 'middle' | 'tail' | 'clip'
Як обрізати, коли рядків забагато. Default — tail ("abcd...").
head"...wxyz"; middle"ab...yz"; clip — обрізати без «…».
Android: при numberOfLines > 1 коректно працює переважно tail.
allowFontScaling
boolean (default true)
Чи масштабувати шрифт згідно з розміром тексту в системі (accessibility). Зазвичай лишають true.
maxFontSizeMultiplier
number?
Верхня межа масштабу, коли scaling увімкнено (щоб величезний системний шрифт не розніс layout).
adjustsFontSizeToFit
boolean
Автозменшення шрифту, щоб вміститися в задані обмеження (рідше; обережно з читабельністю).
selectable
boolean (default false)
Дозволити виділення й системне copy/paste.
onPress / onLongPress
функції
Текст може бути клікабельним (лінки). Для кнопок з іконками краще Pressable.
style
TextStyle (+ частина View)
fontSize, fontWeight, color, lineHeight, textAlign, letterSpacing
fontFamilyодне ім’я шрифту (не CSS-список "Arial, sans-serif").

Playground: style-пропси Text

Типографіка теж живе в style. Порівняйте вирівнювання, вагу, розмір і колір на одному реченні.

TSXTextStylePlayground.tsx
Android
9:41

Loading…

react-native-web · not a real device

AppText у Nomad = рекомендація docs «зробіть MyAppText». Варіанти title / body / caption — це ваші семантичні стилі поверх одного Text (фактично фіксовані набори fontSize + fontWeight + color).

Image — зображення

За документацією Image: компонент для різних типів зображень — мережа, статичні ресурси проєкту, локальні файли, data URI.

Мережеві й data-зображення: розміри контейнера треба задати вручну (через style width/height або layout). Інакше часто побачите порожнє місце. Це прямо підкреслено в офіційних docs.

Джерела: source

// 1) Статичний файл у бандлі Metro
<Image source={require('./assets/logo.png')} style={{ width: 40, height: 40 }} />

// 2) Віддалений URL
<Image
  source={{ uri: 'https://reactnative.dev/img/tiny_logo.png' }}
  style={{ width: 40, height: 40 }}
/>

// 3) Кілька кандидатів (native обере найкращий uri під розмір контейнера)
<Image
  source={[
    { uri: 'https://example.com/img-1x.png', width: 100, height: 100 },
    { uri: 'https://example.com/img-2x.png', width: 200, height: 200 },
  ]}
  style={{ width: 100, height: 100 }}
/>

Об’єкт ImageSource (важливе):

ПолеНавіщо
urihttp(s), шлях, data URI
width / heightвідомі розміри (для вибору з масиву source)
scalescale factor (за замовчуванням 1)
headersHTTP-заголовки для remote
method / bodyрідкісні HTTP-сценарії
cache (iOS)стратегія кешу: default, reload, force-cache, only-if-cached

Підтримувані формати (орієнтир docs): png, jpg/jpeg, bmp, gif, webp, psd (iOS)… На iOS webp у docs обмежений сценарієм бандлу з JS; на Android GIF/WebP у custom native збірках інколи потребують додаткових залежностей Fresco — у Expo managed це зазвичай уже вирішено шаблоном, але знати варто.

resizeMode — як вписати картинку в рамку

Default у docs — cover.

ЗначенняПоведінка
coverЗберегти пропорції; заповнити view; можливе обрізання країв
containУмістити цілком; можливі «поля»
stretchРозтягнути width і height незалежно (пропорції можуть зламатись)
repeatПовторити зображення по view
centerЦентрувати; якщо більше view — зменшити, щоб умістилось

Порівняння режимів і інших props (потрібен інтернет для remote URI). Перемикайте значення — одна й та сама картинка змінюється в фіксованій рамці.

TSXImagePropsPlayground.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

Чому borderRadius на батьку? У Image скруглення інколи поводиться по-різному між платформами. Надійний патерн карток: View з borderRadius + overflow: 'hidden', а Image всередині на width/height: '100%'.
TSXImageCard.tsx
Android
9:41

Loading…

react-native-web · not a real device

Корисні props

defaultSource
локальний ImageSource
Статична «заглушка», поки вантажиться основне. На Android у debug docs кажуть, що prop може ігноруватися.
onLoadStart / onLoad / onLoadEnd / onError
колбеки
Життєвий цикл завантаження. onLoad дає nativeEvent.source з width/height/uri. onError — помилка мережі/формату.
onProgress
({ nativeEvent: { loaded, total } })
Прогрес завантаження remote (не завжди рівномірний).
blurRadius
number
Розмиття. На iOS docs радять значення більше 5, щоб ефект був помітний.
fadeDuration
number (Android, default 300)
Тривалість fade-in після появи.
alt / accessibilityLabel
string
Альтернативний опис для скрінрідера. alt також позначає елемент accessible.
tintColor
color
Перефарбовує непрозорі пікселі (іконки-монохром).
resizeMethod
Android: auto \| resize \| scale \| none
Як даунскейлити великий JPEG перед показом. resize — важкі фото в маленькій рамці; scale — швидше/якісніше коли різниця невелика; none — небезпечно по пам’яті.

Статичні методи (коли знадобляться)

МетодНавіщо
Image.getSize(uri)Дізнатися width/height remote до показу (Promise)
Image.prefetch(url)Завантажити в disk cache наперед
Image.queryCache(urls)Чи є url у memory/disk cache
Image.resolveAssetSource(require(...))uri, width, height, scale для локального asset
Image.abortPrefetch(id) (Android)Скасувати prefetch
Для стрічки карток Nomad достатньо source={{ uri }} + фіксована висота cover + resizeMode="cover". Prefetch/getSize — коли з’являться галереї й важкі фото.
Пізніше в курсі з’явиться expo-image (кращий кеш, blurhash). Базовий Image усе одно треба розуміти: його API — фундамент.

Pressable — етапи натискання

За документацією Pressable: обгортка, що відстежує стадії press на будь-якому вкладеному вмісті.

Як працює (офіційна модель)

На елементі всередині Pressable:

  1. onPressIn — палець/курсор активував press.
  2. Далі одне з двох:
    • палець швидко прибрали → onPressOut, потім onPress;
    • тримали довше за поріг (default 500 ms, див. delayLongPress) → onLongPress, а onPressOut усе одно буде, коли палець відпустять.

Додатково:

  • hitSlop — розширити зону, з якої press може початися (HitRect).
  • pressRetentionOffset — як далеко можна з’їхати пальцем, лишаючись у стані press (PressRect; default близько { top: 20, left: 20, right: 20, bottom: 30 }).
  • Зона дотику не виходить за bounds батька; при перекритті сусідів важливий z-order.
<Pressable
  onPressIn={() => console.log('in')}
  onPressOut={() => console.log('out')}
  onPress={() => console.log('press')}
  onLongPress={() => console.log('long')}
  delayLongPress={500}
  hitSlop={8}
  style={({ pressed }) => [{ opacity: pressed ? 0.7 : 1 }]}
>
  <Text>Натисни</Text>
</Pressable>

Етапи press уживу

Натисніть коротко, потім утримуйте кнопку ~0.5 с — у журналі з’являться in / press / long / out.

TSXPressableStages.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

TSXPressableCounter.tsx
Android
9:41

Loading…

react-native-web · not a real device

Playground: props Pressable і вигляд feedback

Один і той самий напис «Дія», але різні комбінації: disabled, стиль pressed, hitSlop (зона дотику ширша за кнопку — корисно для маленьких цілей).

TSXPressablePropsPlayground.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

Важливі props

onPress
(event) => void
Після успішного short press (після onPressOut у happy path).
onPressIn / onPressOut
(event) => void
Початок і кінець жесту — для анімації «втопленої» кнопки.
onLongPress
(event) => void
Довге утримання (меню, «обрати»).
disabled
boolean
Вимкнути press-поведінку.
hitSlop
number | Rect
Збільшити зону старту дотику (маленькі іконки).
pressRetentionOffset
number | Rect
Допуск «з’їзду» пальця до скасування press.
style
ViewStyle \| ({ pressed }) => ViewStyle
Стиль або функція від { pressed } — головний DX Pressable.
children
ReactNode \| ({ pressed }) => ReactNode
Діти теж можуть бути функцією від pressed (іконка/колір тексту).
android_ripple
RippleConfig (Android)
Системна «хвиля»: { color, borderless, radius, foreground }. foreground: true — ripple поверх вкладеного вмісту (корисно з Image).
android_disableSound
boolean (Android)
Не грати системний звук натискання.
delayLongPress
number (default 500)
Скільки ms від onPressIn до onLongPress.
Pressable побудований на внутрішньому Pressability API. Старі TouchableOpacity / TouchableHighlight у legacy-коді ще живуть; для нових екранів курсу — Pressable (як у Button і TripCard).
Подія — onPress, не DOM-onClick.

ScrollView — прокрутка

За документацією ScrollView: обгортка над platform ScrollView з інтеграцією в touch/responder-систему RN.

Критичне правило висоти

ScrollView повинен мати обмежену (bounded) висоту. Діти всередині можуть бути «вищими за екран», а сам ScrollView — ні: інакше немає куди скролити.

Типова помилка: забули flex: 1 на ланцюжку батьків (Screen → … → ScrollView). Docs радять не фіксувати height навмання, а передати bounded height зверху через flex. Інспектор елементів швидко показує, хто «з’їв» висоту.

ScrollView vs FlatList (офіційна рекомендація)

ScrollViewFlatList
Рендер вмістуУсі одразуЛіниво (майже видимі)
Пам’ять / CPUПогано на довгих спискахКраще для довгих списків
КолиМало контенту, різнорідні блоки (форма + картки + кнопка)Однотипні рядки, сотні елементів, separators, infinite scroll

Для mock-стрічки з 3 поїздок ScrollView + map — нормально. Для 200 — стаття про списки.

Прокрутка вживу

Проведіть пальцем (або коліщатком миші) по прев’ю: контент вищий за екран, ScrollView має flex: 1.

TSXScrollDemo.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

TSXScrollHorizontal.tsx
Android
9:41

Loading…

react-native-web · not a real device

Playground: props ScrollView

Критична плутанина новачків: style (рамка скролу) vs contentContainerStyle (внутрішній контейнер вмісту). Плюс horizontal, індикатор, scrollEnabled.

TSXScrollPropsPlayground.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

Важливі props

contentContainerStyle
ViewStyle
Стилі внутрішнього контейнера, що обгортає весь вміст (padding, alignItems, minHeight). Не плутати зі style самого ScrollView (рамка скролу).
horizontal
boolean (default false)
Горизонтальна прокрутка (галерея мініатюр).
showsVerticalScrollIndicator / showsHorizontalScrollIndicator
boolean
Чи показувати смугу прокрутки (default true).
scrollEnabled
boolean (default true)
Вимкнути жест скролу (програмний scrollTo усе одно можливий).
keyboardShouldPersistTaps
'never' \| 'always' \| 'handled'
Що з клавіатурою при тапі по scroll-області.
never (default) — тап поза інпутом ховає клавіатуру і вкладені елементи можуть не отримати тап.
handled — якщо дитина обробила тап, клавіатура не ховається автоматом.
always — клавіатура сама не ховається.
Критично на формах (наступні статті).
keyboardDismissMode
'none' \| 'on-drag' \| 'interactive' (iOS)
Чи ховати клавіатуру під час драгу скролу.
refreshControl
element
Pull-to-refresh через <RefreshControl /> (лише вертикальний ScrollView). Детальніше з мережею.
pagingEnabled
boolean
Зупинка на кратних розміру scroll view (простий пейджинг).
stickyHeaderIndices
number[]
Індекси елементів, які «прилипають» зверху при скролі (не з horizontal).
onScroll
(event) => void
Події скролу; у nativeEventcontentOffset, contentSize, layoutMeasurement… Часто з scrollEventThrottle (мс між подіями; ≤16 ≈ кожен кадр).
nestedScrollEnabled
boolean (Android)
Вкладений скрол на Android API 21+. Вкладені ScrollView усе одно важкі для UX — уникайте без потреби.

Методи (через ref)

const ref = useRef<ScrollView>(null);

ref.current?.scrollTo({ x: 0, y: 0, animated: true });
ref.current?.scrollToEnd({ animated: true });
ref.current?.flashScrollIndicators();
removeClippedSubviews у docs позначений як ризикований (можливий «зниклий» контент). Не вмикати «для швидкості» без вимірювань.

Safe Area — вирізи екрана

Офіційний SafeAreaView з react-native існує, але в Expo / сучасних проєктах надійніший пакет react-native-safe-area-context:

  • SafeAreaProvider (у дереві Router зазвичай уже є);
  • SafeAreaView або useSafeAreaInsets(){ top, bottom, left, right }.

Навіщо: notch, Dynamic Island, home indicator, вирізи Android. Контент без inset «заїжджає» під систему.

У Nomad примітив Screen використовує SafeAreaView з цього пакета. Глибше (клавіатура + insets) — у статті про layout.


Platform — код під iOS / Android

За Platform-Specific Code є два рівні.

1. Модуль Platform (дрібні відмінності)

import { Platform, StyleSheet } from 'react-native';

// Просто перевірка
const height = Platform.OS === 'ios' ? 200 : 100;

// Вибір значення
const styles = StyleSheet.create({
  card: {
    flex: 1,
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOpacity: 0.1,
        shadowRadius: 8,
        shadowOffset: { width: 0, height: 2 },
      },
      android: {
        elevation: 3,
      },
      default: {
        // web / інше
      },
    }),
  },
});
Platform.OS
'ios' | 'android' | …
Поточна платформа.
Platform.select
helper
Ключі: ios, android, native, default. На телефоні спочатку ios/android, інакше native, інакше default.
Platform.Version
number | string
Android — API level (число, напр. 34).
iOS — рядок system version (напр. "17.2"); major: parseInt(String(Platform.Version), 10).

Класичний кейс UI: тініshadow* на iOS, elevation на Android.

У прев’ю (web) спрацює гілка default / веб-тінь; на справжньому пристрої — ios або android:

TSXPlatformShadow.tsx
iPhone
9:41

Loading…

react-native-web · not a real device

2. Розширення файлів (великі відмінності)

BigButton.ios.tsx
BigButton.android.tsx
import BigButton from './BigButton'; // Metro обере .ios або .android

Також є .native.tsx — спільний native (iOS+Android), окремо від web-бандлера.

У TripCard тінь зроблена через Platform.select у одному файлі — цього достатньо. Окремі .ios/.android файли лишайте для реально різної логіки/розмітки.

Інші корисні примітиви (коротко)

ActivityIndicator

Індикатор завантаження (спінер). Props: size, color, animating.

StatusBar / expo-status-bar

Стиль системного статусного рядка. У layout Nomad — expo-status-bar.

Modal

Модальне вікно поверх дерева. Багато сценаріїв пізніше закриють модалки Router.

TextInput

Поле вводу — окрема стаття про форми (клавіатура, keyboardShouldPersistTaps).
TSXSpinnerProps.tsx
Preview

Loading…

react-native-web · not a real device

Офіційні довідники (глибше за курс, але канон):
View · Text · Image · Pressable · ScrollView · Platform


Міні-проєкт «від А до Я»: картка товару

Цей міні-проєкт не є Nomad. Мета — один раз руками зібрати екран із базових компонентів, перш ніж переносити ту саму ідею на картку поїздки.

Мета продукту

Один екран «товар у магазині»:

  • фото зверху;
  • назва й ціна;
  • короткий опис (обрізаний до двох рядків);
  • кнопка «У кошик», яка після натискання змінює підпис на «У кошику».

Крок 1. Створити окремий проєкт

cd /path/to  # поруч: git clone https://github.com/arakviel/nomad.git
npx create-expo-app@latest product-card -t blank-typescript@sdk-54
cd product-card

Перевірити запуск:

npx expo start

Відкрити в Expo Go (SDK 54). Поки на екрані шаблонний текст — це нормально.

Крок 2. Замінити App.tsx на повний екран

Нижче — цілісний файл. Вставте його замість вмісту App.tsx у blank-шаблоні (точка входу index.ts уже реєструє App).

У реальному проєкті краще обгорнути екран у SafeAreaView з react-native (або react-native-safe-area-context у Expo Router). У лістингу нижче — SafeAreaView, щоб одразу звикати до safe area. Повний лістинг згорнуто — розгорніть, коли будете копіювати в проєкт або звірятись.

Крок 3. Анатомія коду (що саме тут відбувається)

SafeAreaView

Обгортає весь екран, щоб заголовок і кнопка не залізли під системні вирізи. У blank-шаблоні ще немає вашого Screen — тому беремо SafeAreaView з react-native (для навчання достатньо; у Expo Router-проєктах частіше react-native-safe-area-context).

ScrollView + contentContainerStyle

Навіть один товар на маленькому телефоні з великим шрифтом може не вміститися. paddingBottom: 32 лишає повітря внизу.

View styles.card

«Картка» — білий прямокутник з рамкою. overflow: 'hidden' обрізає кути зображення під borderRadius батька (інакше фото лишається з гострими кутами зверху).

Image

source={{ uri: 'https://...' }} — картинка з мережі (потрібен інтернет).
style.cover задає width + height — без цього Image часто не видно.
resizeMode="cover" заповнює рамку 100% × 220.

Text для назви, ціни, опису

Жодного рядка поза Text.
numberOfLines={2} на описі: довгий текст обрізається, картка не роздувається.

useState + Pressable

inCart зберігає, чи товар «додано».
onPress виставляє true.
disabled={inCart} блокує повторні натискання.
Підпис кнопки залежить від стану: «У кошик» / «У кошику».
style як функція від { pressed } затемнює кнопку під час дотику.

Platform.select у тіні

На iOS — shadow*, на Android — elevation. Один об’єкт стилю card отримує потрібний набір полів через spread ...Platform.select(...).

Крок 4. Перевірка на пристрої

  1. Зберегти App.tsx, дочекатися Fast Refresh.
  2. Побачити фото, назву, ціну, опис, кнопку.
  3. Натиснути «У кошик» → текст стає «У кошику», кнопка сірішає.
  4. Прокрутити (якщо екран низький) — низ не обрізаний «намертво».

Критерій «готово»

  • Немає «голого» тексту в View.
  • У Image є ширина й висота.
  • onPress змінює UI (не лише console.log, хоча лог теж можна додати).
  • Safe area не «з’їдає» контент під вирізом.
Міні-проєкт навмисно без@/, tokens і feature-папок — щоб зосередитися на примітивах RN. У Nomad нижче ті самі ідеї з’являться вже в структурі курсу.

Живе прев’ю (після кроків)

Коли код уже зрозумілий — можна покрутити той самий екран у браузері (react-native-web, не Expo Go). Натисніть «У кошик» — підпис і колір кнопки зміняться. У прев’ю корінь — View замість SafeAreaView (у web safe area майже не відчутна).

TSXProductCard.tsx
iPhone
9:41

Loading…

react-native-web · not a real device


Nomad: mock-картки поїздок

Навіщо користувачу

Користувачу потрібна стрічка поїздок, а не порожній екран. Поки без мережі — mock-дані й картки, зібрані з базових компонентів React Native.

Нитка проєкту

Уже є (з попередніх статей — не викидаємо):

  • Структура app/ + src/shared (токени, Screen / AppText / Button).
  • Домашній екран-заглушка на примітивах.

Додаємо в цій статті:

  • Фіча src/features/trips: типи, mockTrips, TripCard.
  • Картка на core-компонентах: View, Text/AppText, Image, Pressable.
  • Стрічка з кількох mock-поїздок на home (map у контейнері).

Повний знімок проєкту після цієї статті

Готовий код 1:1 з Nomad. Найшвидший шлях: git pull у клоні репо. Нижче — весь проєкт на момент цієї статті (усі файли з кодом), не фрагмент «лише нові папки». Клік по файлу в дереві → повний вміст для копіпасту.
cd /path/to/nomad
git pull
npm install
npx expo start

Перевірка

  1. На home видно кілька карток з обкладинкою, назвою, датами.
  2. Натискання картки пише в консоль TODO: відкрити поїздку.
  3. Немає «голого» тексту поза Text / AppText.

Коміт

Якщо збирали вручну (не через git pull на вже запушений репо):

cd /path/to/nomad
git add -A
git commit -m "$(cat <<'EOF'
feat: mock trip cards with core RN components

Material: content/15.react-native/06.core-components.md
EOF
)"
git push

У публічному репо цей коміт уже є — після git pull повторно комітити не потрібно, якщо ви не змінювали код локально.

Результат (прев’ю)

TSXNomadHome.tsx
iPhone
9:41

Loading…

react-native-web · not a real device


Підсумок

У статті є інтерактивні playground (чіпи зі значеннями props): View style, Text (lines/ellipsis + style), Image (resizeMode/blur/height), Pressable (disabled/feedback/hitSlop), ScrollView (horizontal/content padding/style), ActivityIndicator. Усі — через ::react-native-preview (react-native-web). Для фінальної перевірки жестів, safe area й нативних тіней запустіть той самий код у Expo Go або симуляторі.

Далі — Flexbox і layout у React Native: flexDirection за замовчуванням column, вирівнювання, gap, клавіатура.


Практичні завдання

Базовий рівень

  1. Пояснити, чому рядок не кладуть прямо в View.
  2. Зібрати картку з Image + двох Text + Pressable.
  3. Увімкнути numberOfLines={2} на довгому описі.

Середній рівень

  1. Зібрати mock-стрічку з 3+ TripCard у Nomad.
  2. Додати тінь картки через Platform.select.
  3. Обгорнути довгий екран у ScrollView з contentContainerStyle padding знизу.

Професійний рівень

  1. Зробити TripCard з пропсами TypeScript (title, dateLabel, coverUri, onPress).
  2. Додати стан «обрана картка» (підсвітка бордера) через Pressable / стан у батьку.
  3. Порівняти (абзац) ScrollView і майбутній FlatList для 200 поїздок.

Часті запитання

Copyright © 2026