React Native — отличный выбор для веб-разработчиков, имеющих опыт работы с React, которые хотят создать первое мобильное приложение.


Хотя у React и React Native много общего, между веб-платформой и нативными приложениями существуют некоторые различия. В этой статье освещаются наиболее распространенные ошибки и вопросы, с которыми сталкиваются разработчики при переходе с React на React Native.


❯ Почему стоит выбрать React Native


Начнем с рассмотрения причин, по которым стоит выбрать React Native для разработки мобильного приложения.


Знакомые технологии


С React Native почти весь код приложения будет написан на JavaScript (или чаще TypeScript). Это означает, что разработчик сможет использовать многие шаблоны программирования и библиотеки, с которыми он привык работать в вебе.


Нативный код под капотом


Несмотря на то, что большая часть приложения пишется на JavaScript, приложения React Native по сути являются нативными. Код React сопоставляется с реальными нативными примитивами для каждой платформы. Это очень важно для производительности и внешнего вида приложения.


Кроссплатформенность кода


React Native позволяет создавать как приложения для iOS, так и приложения для Android на основе одной и той же кодовой базы. С помощью Expo Router и маршрутизаторов API можно настраивать перенаправление на веб-сайты и серверы.


❯ Почему стоит выбрать Expo


Expo — это фреймворк React Native, аналогично тому, как Next.js — это фреймворк React.

Особенность React Native заключается в том, что он позволяет запускать код React непосредственно на платформах iOS и Android. React Native также поставляется с фундаментальными компонентами, такими как Text, View и TextInput. Однако существует много функций, необходимых для большинства приложений, которые не включены в React Native изначально: навигация, отправка пуш-уведомлений, использование камеры устройства, сохранение данных между запусками приложения или даже публикация приложения в магазинах. Это намеренное решение, чтобы не перегружать базовый фреймворк и сделать его доступным для поддержки небольшой командой.


Поскольку ключевой функционал не включен в React Native, раньше разработчикам приходилось полагаться на разнообразные библиотеки сообщества. За почти 10 лет существования экосистема React Native обросла огромным количеством таких библиотек, среди которых важно делать правильный выбор. Особенно это сложно на старте нового проекта, когда еще нет опыта, чтобы обоснованно судить, какие именно библиотеки подойдут лучше всего.


Идеально было бы иметь в распоряжении набор часто используемых, хорошо поддерживаемых инструментов — и это как раз то, что предоставляет Expo.


Expo — это платформа, построенная вокруг React Native. Она предоставляет комплекс инструментов и сервисов, которые закрывают пробел между базовыми возможностями React Native и всем необходимым для создания готовых к использованию производственных приложений. Expo SDK — это расширенная стандартная библиотека, открывающая доступ к широкому спектру нативных API, не включенных в React Native (камера, видео, уведомления и др.). Expo также предоставляет интерфейс командной строки для создания новых проектов, среду для обучения и прототипирования, систему навигации на основе файловой структуры, инструменты для управления сборками исполняемых файлов, создание и доставкуприложений в магазины, настройку обновлений и даже возможность создания собственных нативных модулей. При этом остается возможность использовать большинство других сторонних библиотек React Native.


Неудивительно, что React Native официально рекомендует разработчикам использовать для новых проектов фреймворки вроде Expo. Аргументация проста: либо вы используете готовый фреймворк, либо создаете собственный. А создание с нуля собственного фреймворка — это далеко не лучшее решение для большинства команд React Native.


❯ Примитивы пользовательского интерфейса в React и React Native


Код React Native очень похож на код React. Но существуют некоторые различия, в том числе, в части компонентов UI.


Вот как вы могли бы рендерить текст в вебе:


export function MyTextComponent() {
  return (
    <div>Hello, web!</div>
  );
}

То же самое в React Native:


import { View, Text } from "react-native";

export function MyTextComponent() {
  return (
    <View>
      <Text>Hello, native!</Text>
    </View>
  );
}

Эти примеры показывают важное различие между React и React Native. В React Native нет стандартных HTML-элементов, вместо этого используются специальные нативные компоненты, экспортируемые из библиотеки react-native. В React Native все, что отображается на экране, всегда будет обернуто в компонент.


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


Рассмотрим несколько ключевых примитивов UI, предоставляемых React Native.


View


Ближайший аналог в вебе — div.


В React Native View является эквивалентом div и используется схожим образом: для создания макета (контейнера) и стилизации.


ScrollView


Ближайший веб-аналог — div.


В вебе страницы прокручиваются по умолчанию. В нативных приложениях — нет. Чтобы сделать страницу прокручиваемой, ее необходимо обернуть в ScrollView (или другой виртуализированный список (virtualized list), такой как FlatList или SectionList).


Text


Ближайший веб-аналог — p.


Любой текст, отображаемый в React Native, должен быть обернут в Text.


Image


Ближайший веб-аналог — img.


Компонент Image позволяет рендерить как удаленные (remote), так и локальные изображения. API немного отличается от веба. Например, загрузка изображения из URL выглядит так:


import { Image } from "react-native";

export function MyImage() {
  return <Image source={{ uri: "https://domain.com/static/my-image.png" }} />;
}

А из локального файла так:


import { Image } from "react-native";

const imageSource = require("../assets/my-image.png");

export function MyImage() {
  return <Image source={imageSource} />;
}

Однако в большинстве реальных приложений React Native вместо этого базового компонента используются компоненты сторонних библиотек, такие как FastImage или Expo Image. Эти компоненты предоставляют дополнительный функционал, связанный со стилизацией, кэшированием и поддержкой форматов изображений.


FlatList


Ближайший веб-аналог — array.map().


Для рендеринга больших списков элементов, как в ленте Instagram или списке писем в почтовом ящике, в вебе обычно используется перебор массива. Однако на нативных платформах этого следует избегать из-за проблем с производительностью. Вместо этого, списки нужно рендерить с помощью FlatList — виртуализированном списке со встроенными оптимизациями, такими как отложенный рендеринг данных, которые в данный момент не требуются, и повторным рендерингом списка при изменении данных.


import { Text, FlatList, View } from "react-native";

const posts = [
  { id: "1", name: "Post 1" },
  { id: "2", name: "Post 2" },
];

export function MyList() {
  return (
    <FlatList
      data={posts}
      renderItem={({ item }) => (
        <Text>{item.name}</Text>
      )}
    />
  );
}

TextInput


Ближайший веб-аналог — <input type="text" />.


Компонент TextInput может использоваться для любого текстового ввода. Основное отличие от <input /> состоит в том, что TextInput можно использовать только для ввода текста (и чисел). Хотя TextInput имеет обработчик onChange, обычно используется onChangeText, где колбек возвращает только обновленную строку текста, без дополнительных данных.


import { useState } from "react";
import { TextInput } from "react-native";

export function MyInput() {
  const [value, setValue] = useState("");

  return <TextInput value={value} onChangeText={setValue} />;
}

TouchableOpacity


Ближайший веб-аналог — button.


Когда требуется, чтобы определенные элементы UI реагировали на нажатия, стандартным решением является оборачивание этих элементов в TouchableOpacity. Данный компонент обеспечивает автоматическую подсветку области при нажатии, при этом степень этой подсветки настраивается с помощью пропа activeOpacity.


import { TouchableOpacity, Text } from "react-native";

export function MyButton() {
  const onPress = () => {
    console.log("Pressed!");
  }

  return (
      <TouchableOpacity onPress={onPress} activeOpacity={0.8}>
        <Text>Button Text</Text>
      </TouchableOpacity>
  );
}

Pressable


Ближайший веб-аналог — button.


Аналогично TouchableOpacity, компонент Pressable используется для создания кнопок. Он позволяет обрабатывать нажатия и предоставляет более гибкие возможности по сравнению с TouchableOpacity, например, отдельную обработку различных состояний нажатия.


Switch


Ближайший веб-аналог — <input type="radio" />


Switch — это компонент-переключатель, с помощью которого можно менять значение между состояниями true и false. Это отличный пример UI-элемента, внешний вид которого является уникальным для каждой мобильной платформы (iOS или Android) благодаря платформозависимой реализации.


import { Switch } from "react-native";

export function MySwitch() {
  const [value, setValue] = useState(false);

    return (
      <Switch
        value={value}
        onValueChange={(value) => setValue(value)}
        trackColor={{ true: "pink" }}
      />
  );
}

Здесь представлены далеко не все основные компоненты React Native. Полный перечень и более подробную информацию о каждом компоненте рекомендуюможно найти в официальной документации.


❯ Стилизация в React Native


Разработчикам, знакомым с CSS для веб-приложений, будет довольно просто освоить стилизацию в React Native. Поддерживается большинство CSS-свойств, однако существуют некоторые отличия.


Отсутствие глобальных стилей


В React Native все стили определяются непосредственно на уровне компонентов с помощью пропа style. Невозможно задать глобальные стили без использования специальной библиотеки для стилизации.


Чтобы распределить стили между компонентами, можно, например, создать отдельный файл theme.js и импортировать его в каждый файл, где необходимо применить эти стили.


import { View, Text, StyleSheet } from "react-native";

export function MyComponent() {
  return (
    <View style={styles.container}>
      <Text style={styles.greeting}>Set Reminder</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    padding: 24,
  },
  greeting: {
    fontSize: 24
  },
});

Flexbox с нюансами


В React Native позиционирование элементов осуществляется с помощью Flexbox. Это работает почти так же, как и в вебе, но есть несколько нюансов:


  • все элементы по умолчанию имеют display: flex
  • flexDirection по умолчанию устанавливается в column (а не в row)
  • alignContent по умолчанию устанавливается в flex-start (вместо stretch)
  • flexShrink по умолчанию устанавливается в 0 (вместо 1)
  • свойство flex поддерживает только одно числовое значение

Библиотеки для стилизации


Встроенные в React Native стили — неплохой вариант для оформления приложения. Многие приложения React Native обходятся без сторонних библиотек для стилизации.


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


  • NativeWindTailwindCSS для React Native
  • Styled Components — для написания стилей с помощью синтаксиса CSS
  • Tamagui — комплексный кросс-платформенный фреймворк для стилизации и создания UI

❯ Распространенные ошибки


Опыт приходит с практикой. Начинающие разработчики React Native часто сталкиваются со следующими проблемами и ошибками.


Использование array.map() для рендеринга списка элементов


Это очень распространенный подход в веб-разработке. Допустим, на странице необходимо отрендерить список постов. В веб-разработке это выглядело бы так:


export function Posts() {
  const posts = usePosts();

  return (
    <div>
      {posts.map(post => <div key={post.id}>{post.name}</div/>)
    </div>
  );
}

В нативной разработке такой подход использовать не рекомендуется, поскольку это может негативно влиять на производительность приложения. Вместо этого рекомендуется использовать компоненты виртуализированных списков, например, FlatList:


import { FlatList } from "react-native";

export function Posts() {
  const posts = usePosts();

  return (
    <FlatList
      data={posts}
      renderItem={({ item }) => (
        <View>
            <Text>{item.name}</Text>
        </View>
      )}
    />
  );
}

Виртуализированные списки автоматически выполняют оптимизации производительности, важные для нативных приложений. Например, они не рендерят элементы, которые находятся далеко за пределами видимой области просмотра.


Еще более оптимизированным вариантом FlatList является FlashList.


Использование компонента Button из React Native


React Native предоставляет компонент Button, который подходит только для 1% случаев, поскольку не позволяет настраивать стилизацию. Лучше вообще забыть о его существовании.


Правильный способ создания кнопок в React Native — оборачивание содержимого кнопки в TouchableOpacity или Pressable:


import { TouchableOpacity, Text } from "react-native";

export function MyButton() {
  const onPress = () => {
    console.log("Pressed!");
  }

  return (
    <TouchableOpacity onPress={onPress} activeOpacity={0.8}>
      <Text>Button Text</Text>
     </TouchableOpacity>
    );
}

Использование логического оператора И (&&) для условного рендеринга компонентов


Любые строки, которые нужно отрендерить, должны быть обернуты в компонент Text. Если вы случайно отрендерите NaN или 0, React Native попытается отрендерить их без оборачивания в Text, что приведет к сбою приложения. В отличие от веб-разработки, React Native очень строг к исключениям JavaScript и не прощает таких ошибок.





Примечание: раньше приложение также ломалось при рендеринге пустой строки '' вне компонента Text, но в более поздних версиях React Native это было исправлено.


Лучшим подходом является использование тернарного оператора для условного рендеринга компонентов: condition ? something : null.


Не храните секретные данные в коде приложения


В веб-разработке существует разделение на клиентскую и серверную части — фронтенд и бэкенд. Нельзя хранить конфиденциальную информацию в клиентском коде, поскольку он доступен злоумышленникам, которые могут изучить исходный код или перехватить сетевой трафик.


Приложение на React Native — это клиентское приложение! Хотя доступ к коду приложения и сетевому трафику не так прост, как в случае с веб-браузером, это все же возможно. Поэтому не рекомендуется размещать в коде приложения какую-либо конфиденциальную информацию. Такие данные, как конфигурация Firebase или другие идентификаторы клиента, которые используются для идентификации приложения в различных серверных службах, можно хранить в коде приложения. Однако любые секретные ключи API или другие критически важные данные не следует включать в клиентский код.


Если приложение должно взаимодействовать с API, требующими такие данные, которые нельзя включить в клиентский код, можно решить эту задачу так же, как при разработке веб-сайта: развернуть API-сервер и обращаться к нему из приложения.


При использовании Expo Router можно писать код API внутри приложения React Native и разворачивать его отдельно, используя API-маршруты.


❯ Новые специфические для нативных платформ концепции


При переходе от разработки веб-приложений к разработке нативных мобильных приложений необходимо ознакомиться с некоторыми новыми концепциями.


Подписание сборок


У iOS и Android есть встроенные механизмы безопасности, предотвращающие установку приложений из неизвестных источников. Чтобы установить приложение на устройство, его необходимо подписать (sign) с помощью учетных данных.


Процесс подписания сборок отличается на iOS и Android, но суть одна и та же. На Android сборки подписываются с помощью Keystore, который можно создать на локальном компьютере. Для iOS необходимо иметь Provisioning Profile и Signing Certificate, получить которые можно только при наличии платной учетной записи разработчика Apple. При использовании для сборки приложения CLI EAS, учетные данные создаются и синхронизируются автоматически.


Примечание: для разработки приложения можно использовать Expo Go. Это позволяет обойти ограничение на подписание сборок, так как Expo Go — публично выпущенное приложение. Такой подход позволяет быстро приступить к разработке приложения без необходимости настройки нативной конфигурации. Однако, в этом случае отсутствует возможность изменять нативный код. Поэтому для производственных приложений рекомендуется использовать сборки для разработки.


Глубокое связывание


В веб-разработке возможность ссылаться на конкретную страницу, как правило, тривиальна, поскольку каждая страница имеет уникальный URL. В мобильных приложениях сделать это может быть несколько сложнее.


В мобильном приложении регистрируется "схема" (scheme) — ключевое слово, на которое реагирует приложение, например, myapp://. Затем используется, например, myapp://my/page, который откроет приложение и передаст в него my/page (или любое другое значение) в качестве инструкции для глубокой ссылки (deep link). Далее необходимо обработать эту инструкцию и перенаправить пользователя по нужному пути. Способ реализации зависит от используемой системы навигации. При использовании Expo Router этот функционал является встроенным благодаря маршрутизации на основе файловой системы. Если применяется только React Navigation, это также реализуемо, но требует более серьезных усилий.


Рекомендую ознакомиться с дополнительной информацией о связывании и глубоком связывании.


Жесты и касания


Для создания нативных ощущений на мобильной платформе важно использовать жесто-ориентированные взаимодействия, такие как "свайп", долгое нажатие, масштабирование "пинчем" и т.д.


Встроенные в React Native элементы TouchableOpacity и Pressable имеют специальные пропы, например, onLongPress. Для построения более сложных жестовых взаимодействий в приложениях React Native часто применяется библиотека React Native Gesture Handler.


Тактильный отклик


Тактильный отклик (haptics) — это небольшая вибрация, которую выполняет телефон в ответ на определенные действия пользователя. Важно использовать такую обратную связь дозированно, но она действительно улучшает пользовательский опыт и делает приложение более нативным. Например, нажатие на кнопку "Нравится" в X, свайп поста в Gmail или нажатие и удержание на значке приложения на главном экране — все эти действия вызывают тактильный отклик.


Тактильный отклик рекомендуется добавлять к ключевым действиям в приложении, например, к добавлению товара в корзину или нажатию кнопки "Нравится".


Экранная клавиатура


Для обеспечения корректного взаимодействия с экранной клавиатурой в мобильном приложении необходимо учитывать следующее:


  • убедитесь, что текстовое поле остается видимым при открытии клавиатуры, а не перекрывается ею. Для этого можно использовать компонент KeyboardAvoidingView
  • клавиатура будет появляться автоматически при установке фокуса на текстовом вводе, но ее можно также показывать и скрывать программно
  • используйте проп keyboardShouldPersistTaps на ScrollView, чтобы определить, как должна вести себя клавиатура при касании за ее пределами (должна она закрываться или нет)
  • применяйте проп keyboardType на TextInput, чтобы указать нужный тип клавиатуры, например, цифровую. Также для настройки клавиатуры могут быть полезны такие пропы, как autoCapitalize, autoComplete, autoCorrect и returnKeyType

Анимация


В React Native нет CSS-анимаций. Вместо этого доступны анимация макета (экспериментальная функция на Android, следует использовать с осторожностью) и встроенный API Animated.


Однако, при необходимости создания анимированных эффектов предпочтительнее использовать библиотеку Reanimated. Это основанная на хуках React библиотека анимации для React Native, API которой, как правило, проще и понятнее, чем встроенный Animated API.


Плотность пикселей


Плотность пикселей (pixel density) — это характеристика, показывающая, сколько пикселей приходится на единицу отображаемой на экране площади. В React Native при определении стилей не используются традиционные единицы измерения, такие как px, em или rem. Вместо них применяется специальная единица — dp (display point), количество пикселей в которой может значительно различаться между устройствами. Узнать информацию о плотности пикселей конкретного устройства можно с помощью PixelRatio.


Это особенно важно при работе с изображениями. Необходимо обеспечить высокое качество изображений, но при этом рендерить только те, размер которых максимально соответствует области их размещения. Например, если изображение с шириной и высотой 100 dp отображается на современном iPhone с плотностью 3, его фактический размер должен быть 300x300 px. Однако на устройстве Android с плотностью 1.5 потребуется изображение 150x150 px. Рендеринг более крупного изображения в меньшей области работает, но является более ресурсоемким (может снизить производительность и вызвать сбои). В то же время отображение слишком маленького изображения в большой области приведет к пикселизации.


Поэтому при использовании удаленных изображений рекомендуется предварительно изменять их размер на сервере. Для статичных изображений, включенных в сборку приложения, можно применять суффиксы @2x и @3x для предоставления версий с разной плотностью пикселей.


❯ Публикация приложения в App Store и Google Play


Процесс публикации мобильных приложений существенно отличается от деплоя веб-приложений.


Важно понимать, что при выпуске новой версии приложения она не будет автоматически установлена всем пользователям. Ни Apple, ни Google не предоставляют механизма для принудительного обновления, поэтому необходимо либо реализовать такую функциональность самостоятельно, либо обеспечить обратную совместимость изменений.


Для публикации приложений требуются платные аккаунты Google Play Console (разовая плата $25) и Apple Developer (ежегодная плата $99), а также настройка страниц в магазинах, включая скриншоты приложения. Следует учитывать, что учетные данные для подписи приложения будут навсегда привязаны к конкретному аккаунту, поэтому публикация одного и того же приложения под другим аккаунтом потребует изменения этих данных, хотя возможен и перенос приложений.


Сборка приложения, загружаемая в магазины, отличается от той, что используется в процессе разработки. Она содержит другие учетные данные и является оптимизированной версией, не предназначенной для прямой установки на устройства. Более подробную информацию о различных типах сборок можно найти в видеокурсе по EAS или в текстовом руководстве.


Для каждого релиза необходимо загружать пакет приложения и отправлять его на рассмотрение. Процесс проверки включает различные руководства и автоматические/ручные проверки. Время на рассмотрение может варьироваться от нескольких часов до нескольких дней. Если приложение требует авторизации, необходимо предоставить рабочие учетные данные для тестирования рецензентом.


Приложение может быть отклонено по различным причинам (проверка в App Store, как правило, более строгая, чем в Google Play). Например, при первой попытке отправить приложение React Conf в App Store (iOS) оно было отклонено, так как содержало нативный код для изменения значка приложения, но рецензент не смог найти эту функцию. После объяснения того, как получить доступ к этой функции через быстрые действия, приложение было одобрено в течение пары часов. Приложение также могут отклонить, если оно сбоит при запуске, рецензент не может войти в систему, отсутствует описание или оно является недостаточно подробным и по многим другим причинам. Обычно исправление заключается в ответе рецензенту с объяснением того, что он пропустил, или в исправлении кода и отправке новой версии на рассмотрение.


После публикации приложения в магазинах можно использовать быстрые обновления, например с помощью EAS Update, для исправления ошибок без прохождения процесса рассмотрения до тех пор, пока вносимые изменения соответствуют правилам магазинов.


Начало работы


Эта статья охватывает лишь основные аспекты, на которые следует обратить внимание при создании первого приложения с помощью React Native. Если вы дошли до этого момента, значит вы настроены серьезно. В этом случае рекомендую ознакомиться с этими обучающими материалами.





? Читайте также:



Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале

Комментарии (7)


  1. isumix
    14.08.2024 15:56

    Гораздо лучше выбрать как раз таки вэб и PWA. Дабы избежать геморроя со сторами, аппрувами приложений, банами, 30% комиссиями, долгими обновлениями, разными костылями для разных платформ, огромными размерами приложений, и пятое и десятое...


    1. Xeosha
      14.08.2024 15:56

      Так expo поддерживает web и pwa из коробки. Разве не круто?


      1. isumix
        14.08.2024 15:56
        +1

        Так expo поддерживает web и pwa из коробки. Разве не круто?

        Как я понял это какой-то обрезанный вэб с обрезанным css, с различиями для разных платформ. Так что нет, не круто.


    1. egribanov
      14.08.2024 15:56

      а как же случаи, когда pwa не может взаимодействовать со всеми api устройства?


      1. isumix
        14.08.2024 15:56

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


  1. dsamsooon
    14.08.2024 15:56
    +1

    Хорошая статья!
    + вайб + rep
    Буду возвращаться перечитывать)


  1. Bolik
    14.08.2024 15:56

    А про платные функции expo почему не упамянули?

    И как быть если доступ из России к сервисам expo забанят?