Как делать11 октября 2026 г.8 мин чтения
React Native + ExpoШаг 3 из 9

React Native и Expo: первое приложение

Коротко

Проект на React Native проще всего начать командой npx create-expo-app@latest: она создаёт приложение на Expo с TypeScript и файловой навигацией, а npx expo start открывает его в Expo Go на телефоне, в эмуляторе или в браузере. Экран собирается из компонентов View, Text, Image, Pressable и ScrollView, а стили задаются объектами через StyleSheet — без CSS-файлов, с Flexbox, который по умолчанию выстраивает элементы в колонку.

Пошаговый урокУровень: НачальныйReact Native и ExpoПрактика: ~1,5–3 ч

После материала вы сможете

  • создать проект командой 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 г.

Содержание
  1. 01Чем React Native отличается от React для веба
  2. 02Создаём проект: create-expo-app и его структура
  3. 03Запуск: Expo Go, эмулятор и development build
  4. 04Экран карточки услуги: View, Text, Image и Pressable
  5. 05Стили: StyleSheet и Flexbox по умолчанию в колонку
  6. 06Безопасные области и разные экраны
  7. 07Проверка без телефона и упражнения

Чем 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
Одна и та же задача в React для веба и в React Native

Создаём проект: 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 или новее.

Terminal
npx create-expo-app@latest service-card --yes
cd service-card
npm run reset-project   # на вопрос о переносе примеров ответьте n
npx expo start
Создание проекта из шаблона по умолчанию и сброс примеров; флаг --yes принимает настройки по умолчанию

Шаблон по умолчанию — это уже работающее приложение с вкладками и примерами. Скрипт 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

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

src/data/service.ts
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 }, и кнопка темнеет, пока палец на ней.

src/components/service-card.tsx
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',
  },
});
Карточка услуги: разметка из базовых компонентов и стили через StyleSheet
  1. Массив стилей

    style={[styles.button, pressed && styles.buttonPressed]} — стили применяются по порядку, последний побеждает, а false пропускается. Так заменяют модификаторы классов из CSS.

  2. Кнопка для скринридера

    accessibilityRole="button" сообщает VoiceOver и TalkBack, что это кнопка, а accessibilityState — что она недоступна после записи.

  3. Текст только в Text

    Даже длительность 60 min — отдельный Text. Строка прямо внутри View приводит к ошибке.

  4. Картинка

    Для локального файла 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.

src/app/index.tsx
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.

src/app/_layout.tsx
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.

Terminal
npx expo install jest-expo jest @types/jest @testing-library/react-native --dev
Установка тестовых пакетов; в PowerShell документация Expo пишет флаг как "--" --dev
package.json
{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "preset": "jest-expo"
  }
}
Фрагмент package.json: скрипт и пресет Jest
__tests__/service-screen-test.tsx
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.

Terminal
$ 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!
Реальный вывод проверки (Windows 11, Expo SDK 57); промежуточные строки со списком ресурсов убраны

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.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.