Проект на React Native проще всего начать командой npx create-expo-app@latest: она создаёт приложение на Expo с TypeScript и файловой навигацией, а npx expo start открывает его в Expo Go на телефоне, в эмуляторе или в браузере. Экран собирается из компонентов View, Text, Image, Pressable и ScrollView, а стили задаются объектами через StyleSheet — без CSS-файлов, с Flexbox, который по умолчанию выстраивает элементы в колонку.
После материала вы сможете
- создать проект командой
create-expo-app, сбросить примеры и объяснить, за что отвечаютsrc/app,app.jsonиpackage.json - выбрать способ запуска — Expo Go, эмулятор, браузер или development build — и объяснить их ограничения
- собрать экран из
View,Text,Image,PressableиScrollViewи стилизовать его черезStyleSheetс учётом Flexbox по умолчанию в колонку - проверить приложение без телефона:
tsc, тест на Jest иnpx expo exportдля Android
Что знать заранее
Проверено на
Expo SDK 57 (expo 57.0.27)React Native 0.86.3React 19.2.3TypeScript 6.0Node.js 24 LTS (24.18)
Сверено с документацией: 11 октября 2026 г.
Содержание
- Чем React Native отличается от React для веба
- Создаём проект: create-expo-app и его структура
- Запуск: Expo Go, эмулятор и development build
- Экран карточки услуги: View, Text, Image и Pressable
- Стили: StyleSheet и Flexbox по умолчанию в колонку
- Безопасные области и разные экраны
- Проверка без телефона и упражнения
Чем React Native отличается от React для веба
Если вы знаете React, компоненты, props, состояние и хуки останутся прежними. Меняется всё, что связано с браузером: в React Native нет DOM, HTML-тегов и CSS. Компоненты View и Text превращаются в настоящие элементы интерфейса Android и iOS, поэтому и правила вёрстки у них свои. Выбор между нативной разработкой, React Native и Flutter мы разобрали в сравнительном материале; здесь — практика.
Контейнер
- React в браузере
<div>- React Native
<View>
Текст
- React в браузере
<p>,<span>или просто строка- React Native
- Только внутри
<Text>: строка прямо вViewвызывает ошибку
Картинка
- React в браузере
<img src>- React Native
<Image source>
Нажатие
- React в браузере
onClickу любого элемента- React Native
onPressуPressable
Стили
- React в браузере
- CSS-файлы, классы, каскад
- React Native
- Объекты в
style,StyleSheet.create, наследование только внутри вложенногоText
Единицы
- React в браузере
px,rem,%- React Native
- Числа без единиц — независимые от плотности пиксели; проценты строкой
Раскладка
- React в браузере
display: blockпо умолчанию- React Native
- Flexbox везде,
flexDirection: 'column'по умолчанию
Навигация
- React в браузере
- Адреса страниц в браузере
- React Native
- Стек экранов; в Expo — файловые маршруты Expo Router
Создаём проект: create-expo-app и его структура
Команда React Native рекомендует начинать новые приложения с фреймворка, и Expo — основной вариант. Урок проверен на Expo SDK 57 — актуальной версии на момент написания: он включает React Native 0.86 и React 19.2, а шаблон по умолчанию ставит TypeScript 6.0. Нужен Node.js LTS; SDK 57 по таблице совместимости требует Node.js 22.13 или новее.
npx create-expo-app@latest service-card --yes
cd service-card
npm run reset-project # на вопрос о переносе примеров ответьте n
npx expo startШаблон по умолчанию — это уже работающее приложение с вкладками и примерами. Скрипт reset-project удаляет примеры и оставляет минимальный каркас: два файла в src/app. Дальше мы наполняем его своим кодом.
src/app/
- Зачем
- Экраны. Каждый файл здесь — маршрут Expo Router,
_layout.tsxзадаёт навигатор
src/components/, src/data/
- Зачем
- Наши компоненты и данные: всё, что не экран, держим вне
src/app
assets/images/
- Зачем
- Иконка приложения, заставка и картинки
app.json
- Зачем
- Конфигурация Expo: название, иконки, схема для ссылок, плагины, экспериментальные флаги
package.json
- Зачем
- Зависимости; поле
main: "expo-router/entry"подключает файловую навигацию
tsconfig.json
- Зачем
- Наследует
expo/tsconfig.base, псевдоним@/*указывает наsrc/*
Запуск: Expo Go, эмулятор и development build
npx expo start запускает Metro — сборщик JavaScript для React Native — и показывает QR-код. Дальше есть три пути, и они отличаются тем, какой нативный код окажется в приложении.
Expo Go на телефоне
- Как открыть
- Установить Expo Go и отсканировать QR-код
- Ограничения
- Только библиотеки из Expo SDK и пакеты без своего нативного кода; документация называет Expo Go песочницей для обучения
Эмулятор или симулятор
- Как открыть
- Клавиша
a(Android) илиi(iOS Simulator) в терминале - Ограничения
- Нужны Android Studio или Xcode; симулятор iOS работает только на Mac
Браузер
- Как открыть
- Клавиша
w - Ограничения
- Работает через React Native for Web: удобно для быстрой проверки, но это не мобильная платформа
Development build
- Как открыть
npx expo run:androidлокально или сборка в облаке EAS- Ограничения
- Ваша собственная «версия Expo Go» с любыми нативными библиотеками; рекомендуется для реальных проектов
Для этого урока хватит Expo Go: мы используем только компоненты React Native и пакет react-native-safe-area-context из шаблона. Как только понадобится библиотека со своим нативным кодом или изменение нативной конфигурации, переходите на development build: после этого приложение пересобирают при установке нативных библиотек, правке app.json и обновлении SDK.
Экран карточки услуги: View, Text, Image и Pressable
Практика урока — экран услуги салона с кнопкой записи. Начнём с данных: в реальном приложении они придут с сервера, здесь это объект с типом, чтобы урок был про интерфейс.
export type Service = {
id: string;
title: string;
description: string;
durationMin: number;
includes: string[];
};
export const service: Service = {
id: 'haircut',
title: 'Haircut and styling',
description: 'Consultation, wash, haircut and styling with a senior stylist.',
durationMin: 60,
includes: ['Consultation', 'Wash', 'Haircut', 'Styling'],
};Карточка — обычный React-компонент с props. View — контейнер, Text — любой текст, Image показывает локальную картинку из require, а Pressable отслеживает нажатие на своё содержимое. Стиль Pressable можно передать функцией: она получает { pressed }, и кнопка темнеет, пока палец на ней.
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
import type { Service } from '@/data/service';
type Props = {
service: Service;
booked: boolean;
onBook: () => void;
};
export function ServiceCard({ service, booked, onBook }: Props) {
return (
<View style={styles.card}>
<Image source={require('../../assets/images/icon.png')} style={styles.cover} resizeMode="cover" />
<View style={styles.body}>
<Text style={styles.title}>{service.title}</Text>
<Text style={styles.meta}>{service.durationMin} min</Text>
<Text style={styles.description}>{service.description}</Text>
<View style={styles.tags}>
{service.includes.map((item) => (
<Text key={item} style={styles.tag}>
{item}
</Text>
))}
</View>
<Pressable
onPress={onBook}
disabled={booked}
accessibilityRole="button"
accessibilityState={{ disabled: booked }}
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
booked && styles.buttonDone,
]}>
<Text style={styles.buttonText}>{booked ? 'Request sent' : 'Book a visit'}</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#ffffff',
borderRadius: 20,
borderWidth: 1,
borderColor: '#e4e4e7',
overflow: 'hidden',
},
cover: {
width: '100%',
height: 180,
},
body: {
padding: 20,
gap: 12,
},
title: {
fontSize: 24,
fontWeight: '700',
color: '#18181b',
},
meta: {
fontSize: 14,
color: '#71717a',
},
description: {
fontSize: 16,
lineHeight: 24,
color: '#3f3f46',
},
tags: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
tag: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: '#f4f4f5',
fontSize: 13,
color: '#3f3f46',
},
button: {
marginTop: 8,
minHeight: 48,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#18181b',
},
buttonPressed: {
opacity: 0.7,
},
buttonDone: {
backgroundColor: '#16a34a',
},
buttonText: {
fontSize: 16,
fontWeight: '600',
color: '#ffffff',
},
});Массив стилей
style={[styles.button, pressed && styles.buttonPressed]}— стили применяются по порядку, последний побеждает, аfalseпропускается. Так заменяют модификаторы классов из CSS.Кнопка для скринридера
accessibilityRole="button"сообщает VoiceOver и TalkBack, что это кнопка, аaccessibilityState— что она недоступна после записи.Текст только в Text
Даже длительность
60 min— отдельныйText. Строка прямо внутриViewприводит к ошибке.Картинка
Для локального файла
source={require(...)}, для удалённого —{ uri }с явными шириной и высотой. Expo также предлагает пакетexpo-imageс кэшированием и плавной подменой картинки.
Стили: StyleSheet и Flexbox по умолчанию в колонку
Стили в React Native — объекты JavaScript. Имена свойств в camelCase (backgroundColor вместо background-color), числа — независимые от плотности пиксели, поэтому fontSize: 16 выглядит сопоставимо на экранах с разной плотностью. StyleSheet.create по документации — функция-тождество: её главная польза в проверке типов и подсказках редактора, а ещё стили с именами проще читать.
Flexbox в CSS
display: flexнужно включитьflex-direction: rowпо умолчаниюflex-shrink: 1по умолчаниюalign-content: stretchпо умолчанию- Каскад и наследование стилей
Flexbox в React Native
- Flexbox работает у каждого
View flexDirection: 'column'по умолчаниюflexShrink: 0по умолчаниюalignContent: 'flex-start'по умолчанию- Наследование только во вложенных
Text
Поэтому в карточке элементы встают друг под другом без единой строчки раскладки, а для ряда тегов понадобились flexDirection: 'row' и flexWrap: 'wrap'. Отступы между элементами задаёт gap — React Native поддерживает его так же, как CSS.
Безопасные области и разные экраны
У телефонов есть вырезы, строка состояния и полоска жестов. Чтобы содержимое под них не уходило, используют безопасные области. Компонент SafeAreaView из самого React Native помечен устаревшим и работает только на iOS, поэтому берём одноимённый компонент из react-native-safe-area-context — он уже стоит в шаблоне как зависимость Expo Router.
import { useState } from 'react';
import { ScrollView, StyleSheet, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { ServiceCard } from '@/components/service-card';
import { service } from '@/data/service';
export default function ServiceScreen() {
const [booked, setBooked] = useState(false);
return (
<SafeAreaView style={styles.safeArea}>
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.heading}>Our services</Text>
<ServiceCard service={service} booked={booked} onBook={() => setBooked(true)} />
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#fafafa',
},
content: {
width: '100%',
maxWidth: 560,
alignSelf: 'center',
padding: 16,
gap: 16,
},
heading: {
fontSize: 32,
fontWeight: '800',
color: '#18181b',
},
});ScrollView прокручивает содержимое, если оно не помещается, а contentContainerStyle задаёт стиль внутреннего контейнера. Для планшета и браузера ширина ограничена maxWidth: 560 с выравниванием по центру — на телефоне карточка занимает всю ширину, на большом экране не растягивается в полосу.
Корневой макет прячет системный заголовок стека: в этом экране он не нужен, а навигацию разберём в следующем уроке про Expo Router.
import { Stack } from 'expo-router';
export default function RootLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}ScrollView отрисовывает всех потомков сразу. Для одной карточки это не важно, но для длинного списка документация React Native рекомендует FlatList: он создаёт элементы по мере прокрутки и освобождает память от ушедших за экран.
Проверка без телефона и упражнения
Запуск на устройстве — ваш шаг: откройте приложение в Expo Go и нажмите кнопку записи. Но часть проверок не требует телефона, и их стоит запускать перед каждым коммитом. Тесты в Expo пишут на Jest с пресетом jest-expo и React Native Testing Library.
npx expo install jest-expo jest @types/jest @testing-library/react-native --dev{
"scripts": {
"test": "jest"
},
"jest": {
"preset": "jest-expo"
}
}import { describe, expect, test } from '@jest/globals';
import { fireEvent, render, screen } from '@testing-library/react-native';
import ServiceScreen from '@/app/index';
describe('<ServiceScreen />', () => {
test('shows the service and books it once', async () => {
await render(<ServiceScreen />);
expect(screen.getByText('Haircut and styling')).toBeTruthy();
expect(screen.getByText('60 min')).toBeTruthy();
const button = screen.getByRole('button', { name: 'Book a visit' });
await fireEvent.press(button);
expect(screen.getByText('Request sent')).toBeTruthy();
expect(screen.getByRole('button', { name: 'Request sent' })).toBeDisabled();
});
});Тест ищет кнопку так же, как скринридер: по роли button и подписи. Если убрать accessibilityRole, тест упадёт — это заодно проверка доступности. Импорт describe, test и expect из @jest/globals нужен из-за TypeScript 6: без явного импорта или настройки types в tsconfig.json проверка типов не видит глобальные функции Jest.
$ npx tsc --noEmit
$ npm test
PASS __tests__/service-screen-test.tsx
<ServiceScreen />
√ shows the service and books it once (427 ms)
Tests: 1 passed, 1 total
$ npx expo export --platform android
Using src/app as the root directory for Expo Router.
React Compiler enabled
Starting Metro Bundler
› android bundles (1):
_expo/static/js/android/entry-943190dc34f449e6a0d00f14bf2ed486.hbc (2.7MB)
Exported: dist
$ npx expo-doctor
21/21 checks passed. No issues detected!npx expo export --platform android собирает JavaScript-бандл в байткод Hermes — так же, как при выпуске приложения, — и падает на ошибках импорта, которые tsc не ловит. Командой --platform ios бандл для iOS собирается и на Windows: Xcode нужен только для нативной сборки.
- Текст прямо внутри
View— оберните его вText - Элементы встали в колонку вместо ряда — добавьте
flexDirection: 'row' ScrollViewне прокручивается — у родителя нетflex: 1или ограниченной высоты- Пакет ставили через
npm installи получили несовместимую версию — переустановите черезnpx expo install - Библиотека с нативным кодом не работает в Expo Go — нужен development build
Следующий шаг — навигация: несколько экранов, вкладки, переход к деталям услуги с параметром в адресе. Это урок про Expo Router.
Как проверено4 · пройдено1 · не запускалосьсверено 11 октября 2026 г.
- запущено, работает
Проект service-card: src/data/service.ts, src/components/service-card.tsx, src/app/index.tsx, src/app/_layout.tsx
npx create-expo-app@latest service-card --yes (expo 57.0.27, react-native 0.86.3, react 19.2.3, typescript 6.0.3, expo-router 57.0.25) + reset-project; npx tsc --noEmit OK (после npx expo customize tsconfig.json), npx expo-doctor 21/21, npx expo export --platform web / android / ios — бандлы собраны (android и ios — Hermes .hbc); код статьи считан генератором из файлов проекта
- запущено, работает
__tests__/service-screen-test.tsx (Jest + jest-expo 57.0.5 + @testing-library/react-native 14.1.0)
npm test: 1 passed — карточка, длительность, нажатие кнопки по роли, состояние disabled; вывод в статье реальный (из двух прогонов, без строк списка ресурсов)
- запущено, работает
Веб-версия экрана (static export) в браузере
dist-web отдан локальным node-сервером, playwright-core + headless Edge: 390×844 и 1000×844 — карточка в пределах ширины, на 1000 — ограничена 560px по центру; нажатие «Book a visit» → кнопка «Request sent» с aria-disabled=true; ошибок страницы и консоли нет
- запущено, работает
Упражнения modify (ToggleServiceCard) и apply (список услуг)
решения добавлены во временные файлы проекта (src/app/ex/*, service-card-toggle.tsx, services.ts) + 2 теста Jest: переключение записи и независимые записи у двух карточек; tsc OK; после проверки файлы вынесены из src
- не запускалось
Запуск в Expo Go, эмуляторе Android, симуляторе iOS, npx expo start
на машине нет устройства/эмулятора; поведение Expo Go, клавиш a/i/w/r/m и development build — по официальной документации, в статье проверка на телефоне оставлена читателю без заявлений о выполнении
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.