В этом посте мы создадим реальное мобильное приложение на ReactNative. Мы также рассмотрим некоторые методы разработки и библиотеки, в том числе следующие:

  • структура каталогов
  • государственное управление (в Mobx)
  • инструменты для стилизации и линтинга кода (Prettier, ESLint и Arirbnb style guide)
  • экранная навигация с помощью react-navigation
  • пользовательский интерфейс с использованием React Native Elements
  • и важная, но часто игнорируемая часть: модульное тестирование вашего приложения (с помощью Jest и Enzyme).

Итак, приступим!

Управление состоянием в React

React и ReactNative сделали создание одностраничных и мобильных приложений увлекательным и простым занятием, но они охватывают только обзор приложений. Управление состоянием и дизайн пользовательского интерфейса все еще могут быть болезненной частью создания приложения.

Для React доступно несколько популярных библиотек State Management. Я использовал Redux, Mobx и RxJS. Хотя все три из них хороши по-своему, мне больше всего понравился MobX из-за его простоты, элегантности и мощного управления состоянием.

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

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

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

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

RxJS - библиотека реактивного программирования для JavaScript. Он отличается от MobX тем, что RxJS позволяет вам реагировать на события, находясь в MobX. Вы наблюдаете за значениями (или состоянием), и это помогает вам реагировать на изменения состояния.

Хотя и RxJS, и MobX предоставляют возможность выполнять реактивное программирование, их подходы сильно различаются.

О нашем приложении

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

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

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

Мы будем устанавливать все на Mac OS. Большинство команд будут такими же, когда у вас установлен Node, но если вы столкнетесь с какими-либо проблемами, дайте мне знать (или просто погуглите).

Темы охватывали

Мы рассмотрим разные темы и различные библиотеки, необходимые для создания и тестирования полномасштабного приложения React Native:

  1. Мы установим create-react-native-app и используем его для начальной загрузки нашего приложения "Книжный магазин".
  2. Настройте Prettier, ESLint и руководство по стилю Airbnb для нашего проекта
  3. Добавление навигации по ящику и вкладкам с помощью реакции-навигации
  4. Протестируйте наши компоненты React с помощью Jest и Enzyme
  5. Управляйте состоянием нашего приложения с помощью MobX (mobx-state-tree). Это также повлечет за собой некоторые изменения пользовательского интерфейса и дополнительную навигацию. Мы отсортируем и отфильтруем книги по жанрам и позволим пользователю видеть экран сведений о книге, когда пользователь нажимает на книгу.

Вот демонстрация приложения Bookstore, которое мы собираемся создать:

Что мы не будем рассказывать

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

  1. Инструменты для добавления системы статических типов в JavaScript, такие как поток и TypeScript
  2. Хотя мы добавим некоторые стили в наше приложение, мы не будем вдаваться в подробности о различных вариантах, доступных для добавления стилей в приложение ReactNative. Библиотека styled-components - одна из самых популярных как для React, так и для ReactNative приложений.
  3. Мы не будем создавать отдельную серверную часть для нашего приложения. Мы пройдем интеграцию с API Google Книг, но по большей части будем использовать фиктивные данные.

Создайте приложение React Native с помощью интерфейса командной строки create-react-native-app (CRNA)

Create React Native App - это инструмент, созданный Facebook и командой Expo, который позволяет легко начать работу с проектом React Native. Мы инициализируем наше приложение ReactNative с помощью интерфейса командной строки CRNA. Итак, приступим!

Предполагая, что у вас уже установлен Node, нам нужно установить create-react-native-app глобально, чтобы мы могли инициализировать новый проект React Native для нашего книжного магазина.

npm install -g create-react-native-app

Теперь мы можем использовать CLI create-react-native-app для создания нашего нового проекта React Native. Назовем его bookstore-app:

create-react-native-app bookstore-app

После завершения загрузки нашего приложения React Native CRNA покажет несколько полезных команд. Давайте сменим каталог на только что созданное приложение CRNA и запустим его.

cd bookstore-app npm start

Это запустит упаковщик, давая возможность запустить симулятор iOS или Android или открыть приложение на реальном устройстве.

Если у вас возникнут проблемы, обратитесь к Руководству по началу работы с React Native или Руководству по созданию приложения React Native (CRNA).

Открытие приложения CRNA на реальном устройстве через Expo

Когда приложение запускается через npm start, в вашем терминале отображается QR-код. Самый простой способ взглянуть на наше загруженное приложение - использовать приложение Expo. Для этого:

  1. Установите клиентское приложение Expo на свое устройство iOS или Android.
  2. Убедитесь, что вы подключены к той же беспроводной сети, что и ваш компьютер.
  3. Используя приложение Expo, отсканируйте QR-код со своего терминала, чтобы открыть свой проект.

Открытие приложения CRNA в симуляторе

Чтобы запустить приложение на iOS Simulator, вам необходимо установить Xcode. Чтобы запустить приложение на виртуальном устройстве Android, вам необходимо настроить среду разработки Android. Посмотрите руководство по началу работы с react-native для обеих настроек.

Настройте Prettier, ESLint и руководство по стилю Airbnb

В этом разделе мы настроим руководство по стилю Prettier, ESLint и Airbnb, чтобы убедиться, что наш код не только выглядит красиво, но и выполняет линтинг кода.

Зачем использовать инструмент для линтинга?

JavaScript - это динамический язык, и в нем нет системы статических типов, как в таких языках, как C ++ и Java. Из-за этой динамической природы в JavaScript отсутствуют инструменты для статического анализа, которые предлагают многие другие языки.

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

Поскольку это не компилируемый язык, ошибки обнаруживаются при выполнении кода JavaScript во время выполнения. Существуют такие инструменты, как TypeScript и flow, которые помогают отлавливать подобные ошибки, добавляя систему статических типов в JavaScript, но мы не будем рассматривать ни один из этих инструментов в этом руководстве.

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

Установить и настроить ESLint

Хороший инструмент для линтинга чрезвычайно важен для обеспечения качества с самого начала и раннего выявления ошибок. ESLint также помогает реализовать рекомендации по стилю.

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

ESLint полностью настраивается и настраивается. Вы можете установить свои правила в соответствии со своими предпочтениями. Однако сообщество предоставило различные конфигурации правил линтинга. Одним из самых популярных является руководство по стилю Airbnb, и мы будем его использовать. Это будет включать правила ESLint Airbnb, включая ECMAScript 6+ и React.

Сначала мы установим ESLint, выполнив эту команду в терминале:

Мы будем использовать eslint-config-airbnb Airbnb, который содержит правила ESLint Airbnb, включая ECMAScript 6+ и React. Для этого требуются определенные версии ESLint, eslint-plugin-import, eslint-plugin-react и eslint-plugin-jsx-a11y. Чтобы вывести список зависимостей и версий одноранговых узлов, выполните эту команду:

npm info "eslint-config-airbnb@latest" peerDependencies

На момент написания это версии, показанные в выходных данных вышеуказанной команды:

{ eslint: '^4.9.0',
  'eslint-plugin-import': '^2.7.0',
  'eslint-plugin-jsx-a11y': '^6.0.2',
  'eslint-plugin-react': '^7.4.0' }

Итак, давайте установим эти конкретные версии зависимостей, выполнив эту команду:

npm install -D eslint@^4.9.0 eslint-plugin-import@^2.7.0 eslint-plugin-jsx-a11y@^6.0.2 eslint-plugin-react@^7.4.0

Это установит необходимые зависимости и сгенерирует файл .eslintrc.js в корневом каталоге проекта. Файл .eslintrc.js должен иметь следующие конфигурации:

module.exports = {
  "extends": "airbnb"
};

Стиль кода

Хотя у нас есть линтинг, охватываемый ESLint и руководством по стилю Airbnb, большая часть качества кода - это согласованный стиль кода. Когда вы работаете в команде, вы хотите убедиться, что форматирование кода и отступы единообразны во всей команде. Prettier - как раз инструмент для этого. Это гарантирует, что весь код соответствует единому стилю.

Мы также добавим плагин ESLint для Prettier, который добавит Prettier в качестве правила ESLint и сообщит о различиях как об отдельных проблемах ESLint.

Теперь могут возникнуть конфликты между правилами ESLint и форматированием кода, выполненным Prettier. К счастью, есть плагин под названием eslint-config-prettier, который отключает все правила, которые не нужны или могут конфликтовать с Prettier.

Установите и настройте Prettier с ESLint

Установим все необходимые пакеты Prettier и eslint-plugin-prettier. Для этого нам также потребуется установить eslint-config-airbnb:

npm install -D prettier prettier-eslint eslint-plugin-prettier eslint-config-prettier eslint-config-airbnb

ПРИМЕЧАНИЕ. Если ESLint установлен глобально, убедитесь, что eslint-plugin-prettier также установлен глобально. Глобально установленный ESLint не может найти локально установленный плагин.

Чтобы включить плагин eslint-plugin-prettier, обновите файл .eslintrc.js, чтобы добавить плагин prettier. И чтобы отображать ошибку линтинга в правилах форматирования Prettier, добавьте «правило», чтобы отображать ошибку в правилах форматирования «prettier / prettier». Вот наш обновленный .eslintrc.js:

module.exports = {
  "extends": [
    "airbnb",
    "prettier"
  ],
  rules: {
    "prettier/prettier": "error",
  },
}

Eslint-config-prettier также поставляется с инструментом CLI, который поможет вам проверить, содержит ли ваша конфигурация какие-либо правила, которые не нужны или конфликтуют с Prettier. Давайте проявим инициативу и сделаем это.

Сначала добавьте для него скрипт в package.json:

{
  "scripts": {
    "eslint-check": "eslint --print-config .eslintrc.js | eslint-config-prettier-check"
  }
}

Теперь запустите команду eslint-check, чтобы увидеть конфликтующие правила ESLint и Prettier:

npm run eslint-check

В терминале будут перечислены конфликтующие правила. Давайте отключим конфликтующие правила, обновив файл .eslintrc.js. Я также предпочитаю singleQuote и trailingComma, поэтому я настрою и эти правила. Вот как теперь выглядит наш файл .eslintrc.js:

module.exports = {
  "parser": "babel-eslint",
  "extends": [
    "airbnb",
    "prettier"
  ],
  "plugins": [
    "prettier"
  ],
  "rules": {
    "prettier/prettier": "error",
    "react/jsx-closing-bracket-location": "off",
    "react/jsx-closing-tag-location": "off",
    "react/jsx-curly-spacing": "off",
    "react/jsx-equals-spacing": "off",
    "react/jsx-first-prop-new-line": "off",
    "react/jsx-indent": "off",
    "react/jsx-indent-props": "off",
    "react/jsx-max-props-per-line": "off",
    "react/jsx-tag-spacing": "off",
    "react/jsx-wrap-multilines": "off"
  }
}

Если вы сейчас запустите eslint с флагом --fix, код будет автоматически отформатирован в соответствии со стилями Prettier.

Настройте VS Code для запуска ESLint при сохранении

Мы можем настроить любую среду IDE для автоматического запуска ESLint при сохранении или по мере ввода. Поскольку мы также настроили Prettier вместе с ESLint, наш код будет автоматически претифицирован. VS Code - это IDE, популярная в сообществе JavaScript, поэтому я покажу, как настроить автоматическое исправление ESLint при сохранении с помощью VS Code, но шаги будут аналогичными в любой IDE.

Чтобы настроить VS Code на автоматический запуск ESLint при сохранении, нам сначала нужно установить расширение ESLint. Перейдите в Расширения, найдите расширение «ESLint» и установите его. После установки расширения ESLint перейдите к Preferences > User Settings и установите для параметра eslint.autoFixOnSave значение true. Также убедитесь, что для «files.autoSave» установлено значение «off», «onFocusChange» или «onWindowChange».

Теперь откройте файл App.js. Если ESLint настроен правильно, вы должны увидеть некоторую ошибку линтинга, например ошибки «react / preference-stateless-function», «react / jsx-filename-extension» и «no-use-before-define». Давайте отключим их в файле .eslintrc.js. Я также предпочитаю singleQuote и trailingComma, как упоминалось выше, поэтому я также настрою эти правила.

Вот обновленный файл .eslintrc.js.

module.exports = {
  "parser": "babel-eslint",
  "extends": [
    "airbnb",
    "prettier"
  ],
  "plugins": [
    "prettier"
  ],
  "rules": {
    "prettier/prettier": [
      "error",
      {
        "singleQuote": true,
        "trailingComma": "all",
      }
    ],
    "react/prefer-stateless-function": "off",
    "react/jsx-filename-extension": "off",
    "no-use-before-define": "off",
    "react/jsx-closing-bracket-location": "off",
    "react/jsx-curly-spacing": "off",
    "react/jsx-equals-spacing": "off",
    "react/jsx-first-prop-new-line": "off",
    "react/jsx-indent": "off",
    "react/jsx-indent-props": "off",
    "react/jsx-max-props-per-line": "off",
    "react/jsx-tag-spacing": "off",
    "react/jsx-wrap-multilines": "off"
  }
}

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

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

Навигация по ящику и вкладкам с помощью реакции-навигации

В этом разделе мы добавим навигацию по ящикам и вкладкам с помощью реакции-навигации.

В нашем приложении «Книжный магазин» будет панель навигации с двумя вариантами меню. Первый пункт меню AuthorsScreen, содержащий список авторов. Второй пункт меню для Книжного экрана, содержащий список книг.

При нажатии на книгу пользователь переходит на экран Сведения о книге. Для навигации между различными представлениями мы будем использовать React Navigation, чтобы добавить навигацию в наше приложение. Итак, давайте сначала установим его:

npm install --save react-navigation

createStackNavigator

Наше приложение ReactNative будет содержать два модуля:

  • модуль Author, позволяющий пользователям просматривать список авторов
  • модуль "Книги", содержащий список книг.

Модули Автор и Книга будут реализованы с помощью StackNavigator от React Navigation. Думайте о StackNavigator как о стеке истории в веб-браузере. Когда пользователь нажимает на ссылку, URL-адрес помещается в стек истории браузера и удаляется из верхней части стека истории, когда пользователь нажимает кнопку Назад.

export const BookStack = createStackNavigator({
  Books: {
    screen: BooksScreen,
  },
})
export const AuthorStack = createStackNavigator({
  Authors: {
    screen: AuthorsScreen,
  },
})

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

const BooksScreen = ({ navigation }) => (
  <View>
    <Button
      onPress={() => navigation.navigate('Authors')}
      title="Go to Authors"
    />
    <Button onPress={() => navigation.openDrawer()} title="Open Drawer" />
  </View>
)
const AuthorsScreen = ({ navigation }) => (
  <Button
    onPress={() => navigation.navigate('Books')}
    title="Go back to Books"
  />
)

navigation.openDrawer() приведет к открытию ящика. navigation.navigate() позволяет приложению переходить на разные экраны.

В нашем приложении мы добавим ящик, который будет поддерживать меню для наших модулей Автор и Книга. Мы реализуем ящик с помощью createDrawerNavigator React Navigation.

Первое меню в ящике предназначено для модуля «Автор», а второе - для модуля «Книга». Навигаторы Author и Book Stack будут находиться внутри основного DrawerStack.

Вот код для реализации ящика:

const App = createDrawerNavigator({
  Books: {
    screen: BookStack,
  },
  Authors: {
    screen: AuthorStack,
  },
})

Вот разница наших последних изменений.

В файле App.js мы внесли следующие изменения:

  1. Мы переименовали экспорт по умолчанию в App
  2. Мы добавили два компонента без сохранения состояния для наших экранов, BooksScreen и AuthorsScreen.
  3. Мы добавили StackNavigator из React Navigation, чтобы реализовать навигацию для нашего приложения.
  4. Мы использовали createDrawerNavigator () из react-navigation для реализации Drawer Navigation. Это отображает содержимое ящика вместе с параметрами меню для книг и авторов.

И после внесения вышеуказанных изменений вот как выглядит наш пользовательский интерфейс, когда мы нажимаем кнопку «Открыть ящик» и перемещаемся между экранами.

Структура каталогов

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

Давайте создадим каталог src, в котором мы будем хранить все наши исходные файлы. Внутри него создайте два каталога: один для представления книги с именем «книга», а другой для представления автора с именем «автор».

Создайте файлы index.js в каждом из двух только что добавленных каталогов. Эти файлы будут экспортировать компоненты для каждого из наших представлений. Переместите код из App.js для компонентов BookView и AuthorView в эти файлы и вместо этого импортируйте их.

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

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

У каждого экрана будет заголовок, что означает, что мы будем дублировать один и тот же код вместе со стилями. Чтобы наш код оставался СУХИМ, давайте переместим заголовок в отдельный файл src/components/Title.js и повторно используем его при необходимости. Мы также переместим основные представления в новый родительский каталог src/views, чтобы отделить их от других компонентов.

Вкладка навигации

Бизнес-требование для нашего приложения - иметь три вкладки в режиме просмотра книг, чтобы отображать все книги по умолчанию, и дополнительные вкладки для отображения отфильтрованных книг для художественной и научной литературы. Давайте воспользуемся createBottomTabNavigator из react-navigation для реализации навигации по вкладкам.

import { createBottomTabNavigator } from 'react-navigation'
import { AllBooksTab, FictionBooksTab, NonfictionBooksTab } from ' components/book-type-tabs'
export default createBottomTabNavigator({
  'All Books': AllBooksTab,
  Fiction: FictionBooksTab,
  Nonfiction: NonfictionBooksTab,
})

Мы также должны добавить заголовок на каждый экран, чтобы идентифицировать текущий выбранный экран. Давайте создадим отдельный каталог src/components для всех общих компонентов и создадим файл для нашего Title компонента внутри этого нового каталога.

// src/components/Title.js
import React from 'react'
import { StyleSheet, Text } from 'react-native'
const styles = StyleSheet.create({
  header: {
    textAlign: 'center',
    padding: 20,
    marginTop: 20,
    fontSize: 20,
    color: '#fff',
    backgroundColor: '#434343',
  },
})
export default ({ text }) => <Text style={styles.header}>{text}</Text>

Обратите внимание, что мы также добавили style в компонент <Text>, импортировав как StyleSheet, так и Text из react-native.

Мы добавим Title к каждому компоненту представления, указав заголовок text в реквизитах. Кроме того, поскольку представление авторов просто содержит список авторов, для него не нужен StackNavigator, поэтому мы заменим его на простой компонент React. Вот как теперь выглядит наш src/views/author/index.js файл:

// src/views/author/index.js
import Title from '../../components/Title'
export default ({ navigation }) => (
  <View>
    <Title text="Authors List" />
    <Button onPress={() => navigation.openDrawer()} title="Open Drawer" />
    <Button onPress={() => navigation.navigate('Books')} title="Go to Books" />
  </View>
)

Теперь, когда мы открываем меню "Книги" из ящика, мы можем переключать вкладки, нажимая на вкладки внизу.

С этими изменениями навигация в нашем приложении завершена. Вот разница наших последних изменений.

React Native Elements

Существует несколько библиотек компонентов пользовательского интерфейса для добавления компонентов React Native со стилем. Некоторые из наиболее популярных - React Native Elements, NativeBase и Ignite. Мы будем использовать React Native Elements для нашего приложения Bookstore. Итак, давайте сначала установим react-native-elements:

npm install --save react-native-elements

Создание нашего списка авторов с использованием response-native-elements

Давайте воспользуемся компонентом ListItem из React Native Elements, чтобы добавить список авторов на наш экран «Автор».

Для Списка авторов мы будем использовать данные и код из демонстрации ListItem. Мы вернемся к ListItem более подробно, когда реализуем экран со списком книг.

Вот разница наших последних изменений.

Тестирование компонентов ReactNative с помощью Jest и Enzyme

В этом разделе мы добавим несколько модульных тестов с использованием Jest и Enzyme.

Настройка Jest и Enzyme

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

Мы будем использовать Jest в качестве нашей среды тестирования вместе с утилитой Airbnb для тестирования JavaScript Enzyme. Enzyme имеет гибкий и интуитивно понятный интерфейс, который позволяет очень легко утверждать, манипулировать и перемещаться по компонентам React.

В комплект create-react-native-app уже включены все связанные библиотеки и конфигурации Jest. Для работы с Enzyme нам необходимо установить enzyme и некоторые связанные с ним зависимости. Поскольку мы используем React 16, мы будем добавлять react-dom@16 и enzyme-adapter-react-16.

npm install -D enzyme react-dom@16 enzyme-adapter-react-16

Нам нужно настроить enzyme-adapter-react-16. Мы сделаем это во время настройки Jest. Создайте корень jestSetup.js файлового проекта со следующим кодом:

import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
configure({ adapter: new Adapter() })

Теперь добавьте этот файл в конфигурацию Jest в package.json:

"jest": {
    "preset": "jest-expo",
    "setupTestFrameworkScriptFile": "<rootDir>/jestSetup.js"
  },

Тесты ферментов и снимков для нашего компонента Title

Теперь мы готовы добавить ферментные тесты. Я предпочитаю, чтобы тесты располагались вместе с моим кодом. Давайте создадим простой тест для нашего компонента Title, добавив тестовый файл рядом с нашим компонентом Title. В этом тесте мы просто неглубоко отрендерим компонент Title, создадим снимок и проверим стили компонентов. Создайте файл src/components/__tests__/Title.js со следующим содержимым:

import React from 'react'
import { shallow } from 'enzyme'
import Title from '../Title'
it('renders correctly', () => {
  const wrapper = shallow(<Title text="Sample Text" />)
  expect(wrapper).toMatchSnapshot()
expect(wrapper.prop('accessible')).toBe(true)
  expect(wrapper.prop('style')).toEqual({
    backgroundColor: '#434343',
    color: '#fff',
    fontSize: 20,
    marginTop: 20,
    padding: 20,
    textAlign: 'center',
  })
})

Давайте проведем наши тесты:

npm test

Тесты должны пройти и сгенерировать снимок, который даст следующий результат:

Если вы не знакомы с тестированием снимков Jest, это отличный способ протестировать компоненты React или различные виды выходных данных в целом.

По сути, вызов toMatchSnapshot() отображает ваш компонент и создает снимок в каталоге __snapshots__ (если снимок еще не существует). После этого, каждый раз, когда вы повторно запускаете свои тесты, Jest будет сравнивать вывод визуализированного компонента с выводом моментального снимка и завершится ошибкой в ​​случае несоответствия. Он покажет разницу между ожидаемым и фактическим результатом. Затем вы можете просмотреть различия, и, если это различие действительно из-за некоторых изменений, которые вы внедрили, вы можете повторно запустить тесты с флагом -u, который сигнализирует Jest о необходимости обновить снимок с новыми обновлениями.

Вот разница для наших изменений для тестов Jest и Enzyme, включая сгенерированный снимок.

сериализатор энзим-в-json

Если вы откроете файл снимка (src/components/__tests__/__snapshots__/Title.js.snap), вы заметите, что его содержимое не очень читается. Он запутывается кодом из оболочек Enzyme, поскольку мы используем Enzyme для рендеринга нашего компонента. К счастью, существует библиотека энзим-в-json, которая преобразует оболочки Enzyme в формат, совместимый с тестированием снимков Jest.

Давайте установим фермент в json:

npm install -D enzyme-to-json

И добавьте его в конфигурации Jest в качестве сериализатора снимков в pacakge.json:

"jest": {
    ...
    "snapshotSerializers": ["enzyme-to-json/serializer"]
  },

Поскольку теперь мы ожидаем, что снимок будет отличаться от предыдущего снимка, мы передадим флаг -u, чтобы обновить снимок:

npm test -- -u

Если вы снова откроете файл снимка, вы увидите, что снимок для визуализированного компонента Title правильный.

Подробнее о тестировании Jest мы поговорим в следующих разделах.

Управление состоянием с помощью React Navigation и Mobx Store

MobX или Redux для управления состоянием

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

Существует несколько библиотек управления состоянием, но самыми популярными являются Redux и MobX. Мы будем использовать магазин Mobx для нашего приложения Bookstore.

Я обычно предпочитаю MobX Redux для управления магазином, потому что считаю, что добавление новых данных магазина в Redux занимает гораздо больше времени по сравнению с MobX.

Некоторые недостатки Redux:

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

Некоторые преимущества MobX:

  • Он добавляет этот шаблон за вас и делает это хорошо. Мне очень легко работать, будь то первоначальная настройка или добавление дополнительных функций.
  • Он не заставляет вас реализовывать поток данных определенным образом, и у вас гораздо больше свободы. Но опять же, это может быть скорее проблематичным, чем полезным, если вы неправильно настроите свои магазины MobX.

Я знаю, что это деликатная тема, и я не хочу начинать здесь дебаты, поэтому оставлю эту тему на другой день. Но если вы хотите получить более широкое представление об этом, есть несколько точек зрения на эту дискуссию в Интернете. Redux и MobX - отличные инструменты для управления магазином.

Мы будем постепенно добавлять функции в наш магазин, а не сразу, просто чтобы показать вам, как легко добавить дополнительные функции в магазины MobX.

Дерево состояний MobX

Мы не будем использовать Mobx напрямую, а будем использовать оболочку MobX под названием mobx-state-tree. Они прекрасно описали себя, поэтому я просто процитирую их здесь:

Проще говоря, mobx-state-tree пытается объединить лучшие свойства как неизменяемости (транзакционность, отслеживаемость и композиция), так и изменчивости (обнаруживаемость, совместное расположение и инкапсуляция). - Страница MST на Github

Установим mobx вместе с mobx-react и mobx-state-tree

npm install --save mobx mobx-react mobx-state-tree

Мы будем использовать API Google Книг, чтобы получать книги для нашего приложения. Если вы хотите продолжить, вам нужно будет создать проект в Google Developers Console, включить в нем API Google Книг и создать в проекте ключ API. Получив ключ API, создайте файл keys.json в корне проекта со следующим содержимым (замените YOUR_GOOGLE_BOOKS_API_KEY на свой ключ API):

{
  "GBOOKS_KEY": "YOUR_GOOGLE_BOOKS_API_KEY"
}

ПРИМЕЧАНИЕ. Если вы не хотите получать ключ API, не волнуйтесь. Мы не будем использовать Google API напрямую, а вместо этого будем имитировать данные.

Конечная точка API Google Книг books/v1/volumes возвращает массив items, где каждый элемент содержит информацию об определенной книге. Вот сокращенная версия книги:

{
  kind: "books#volume",
  id: "r_YQVeefU28C",
  etag: "HeC4avg1XlM",
  selfLink: "https://www.googleapis.com/books/v1/volumes/r_YQVeefU28C",
  volumeInfo: {
    title: "Breaking Everyday Addictions",
    subtitle: "Finding Freedom from the Things That Trip Us Up",
    authors: [
      "David Hawkins"
    ],
    publisher: "Harvest House Publishers",
    publishedDate: "2008-07-01",
    description: "Addiction is a rapidly growing problem among Christians and non-Christians alike. Even socially acceptable behaviors, ...",
    pageCount: 256,
    printType: "BOOK",
    categories: [
      "Addicts"
    ],
    imageLinks: {
      smallThumbnail: "http://books.google.com/books/content?id=r_YQVeefU28C",
      thumbnail: "http://books.google.com/books/content?id=r_YQVeefU28C&printsec=frontcover"
    },
    language: "en",
    previewLink: "http://books.google.com.au/books?id=r_YQVeefU28C&printsec=frontcover",
    infoLink: "https://play.google.com/store/books/details?id=r_YQVeefU28C&source=gbs_api",
    canonicalVolumeLink: "https://market.android.com/details?id=book-r_YQVeefU28C"
  }
}

Мы не будем использовать все поля, возвращаемые в ответе API. Итак, мы создадим нашу модель MST только для тех данных, которые нам нужны в нашем приложении ReactNative. Давайте определим нашу модель книги в MST.

Создайте новую структуру каталогов stores/book внутри src и создайте в ней новый файл index.js:

// src/stores/book/index.js
import { types as t } from 'mobx-state-tree'
const Book = t.model('Book', {
  id: t.identifier(),
  title: t.string,
  pageCount: t.number,
  authors: t.array(t.string),
  image: t.string,
  genre: t.maybe(t.string),
  inStock: t.optional(t.boolean, true),
})

В приведенном выше определении узла MST наш тип модели Book определяет форму нашего узла - типа Book - в дереве состояний MobX. types.modeltype в MST используется для описания формы объекта. Имя модели не требуется, но рекомендуется для отладки.

Второй аргумент, аргумент свойств, представляет собой пару «ключ-значение», где ключ - это имя свойства, а значение - его тип. В нашей модели id - это идентификатор, title - это тип строка, pageCount - тип число, authors - массив строки, genre имеет тип строка, inStock тип логическое и image тип строка.

По умолчанию для создания действительного узла в дереве требуются все данные, поэтому, если мы попытаемся вставить узел без заголовка, MST не позволит этого и выдаст ошибку.

genre будет сопоставлен с полем categories (первое значение индекса массива категорий) данных API Google Книг. Это может быть, а может и не быть в ответе. Поэтому мы сделали его типа maybe. Если данные для жанра отсутствуют в ответе, genre будет установлено значение null в MST, но если оно есть, то для того, чтобы оно было, оно должно иметь тип строка. действительный.

Поскольку inStock является нашим собственным полем и не возвращается в ответе API Google Книг, мы сделали его необязательным и присвоили ему значение по умолчанию true. Мы могли бы просто присвоить ему значение true, поскольку для примитивных типов MST может выводить тип из значения по умолчанию. Итак, inStock: true это то же самое, что inStock: t.optional(t.boolean, true).

В разделе Создание моделей документации mobx-state-tree подробно рассказывается о создании моделей в MST.

// src/stores/book/index.js
const BookStore = t
  .model('BookStore', {
    books: t.array(Book),
  })
  .actions(self => {
    function updateBooks(books) {
      books.forEach(book => {
        self.books.push({
          id: book.id,
          title: book.volumeInfo.title,
          authors: book.volumeInfo.authors,
          publisher: book.volumeInfo.publisher,
          image: book.volumeInfo.imageLinks.smallThumbnail,
        })
      })
    }
const loadBooks = process(function* loadBooks() {
      try {
        const books = yield api.fetchBooks()
        updateBooks(books)
      } catch (err) {
        console.error('Failed to load books ', err)
      }
    })
return {
      loadBooks,
    }
  })

По умолчанию деревья MST защищены. Это означает, что только действия MST могут изменить состояние дерева.

Мы определили два действия: updateBooks - это функция, которая вызывается только функцией loadBooks, поэтому мы не показываем ее внешнему миру. loadBooks, с другой стороны, открыт (мы его возвращаем) и может быть вызван извне BookStore.

Асинхронные действия в MST записываются с использованием генераторов и всегда возвращают обещание. В нашем случае loadBooks должен быть асинхронным, поскольку мы выполняем Ajax-вызов API Google Книг.

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

// src/stores/book/index.js
let store = null
export default () => {
  if (store) return store
store = BookStore.create({ books: {} })
  return store
}

Использование магазина MST в нашем представлении

Начнем с просмотра "Все книги". Для этого мы создадим новый файл, содержащий наш BookListView компонент:

import React, { Component } from 'react'
import { observer } from 'mobx-react'
import BookStore from '../../../stores/book'
import BookList from './BookList'
@observer
class BookListView extends Component {
  async componentWillMount() {
    this.store = BookStore()
    await this.store.loadBooks()
  }
render() {
    return <BookList books={this.store.books} />
  }
}

Как видите, мы инициализируем BookStore в componentWillMount, а затем вызываем loadBooks() для асинхронного извлечения книг из API Google Книг. Компонент BookList выполняет итерацию по массиву books внутри BookStore и отображает компонент Book для каждой книги. Теперь нам просто нужно добавить этот BookListView компонент в AllBooksTab.

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

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

Если вы запустите npm start сейчас, вы должны увидеть список книг, полученных с помощью Google API.

Вот разница между нашими изменениями на данный момент.

Добавление тестов для нашего книжного магазина MST

Давайте добавим несколько модульных тестов для нашего книжного магазина. Однако наш магазин обращается к нашему API, который вызывает API Google. Мы можем добавить интеграционные тесты для нашего магазина, но чтобы добавить модульные тесты, нам нужно как-то имитировать API.

Простой способ имитировать API - использовать Jest Manual Mocks, создав каталог __mocks__ рядом с нашим существующим файлом api.js. Внутри него создайте еще один api.js, имитируемую версию наших вызовов выборки API. Затем мы просто вызываем jest.mock('../api') в нашем тесте, чтобы использовать эту фиктивную версию.

Внедрение зависимостей в дереве состояний MobX

Мы не будем использовать Jest Manual Mocks. Я хотел бы показать вам еще одну функцию MST и продемонстрировать, насколько легко имитировать наш API с помощью MST. Мы будем использовать внедрение зависимостей в дереве состояний MobX, чтобы предоставить простой способ имитировать вызовы API и упростить тестирование нашего магазина. Обратите внимание, что наше хранилище MST также можно протестировать без внедрения зависимостей с помощью Jest Mocks, но мы делаем это только для демонстрации.

Можно ввести данные, относящиеся к среде, в дерево состояний, передав объект в качестве второго аргумента вызова BookStore.create(). Этот объект будет доступен для любой модели в дереве, вызвав getEnv(). Мы будем внедрять фиктивный API в наш магазин BookStore, поэтому давайте сначала добавим необязательный параметр api к экспорту по умолчанию и установим для него фактическое значение bookApi по умолчанию.

// src/stores/book/index.js
let store = null
export default () => {
  if (store) return store
store = BookStore.create({ books: {} })
  return store
}

Теперь добавьте MST View для внедренного API, взяв его с помощью getEnv(). Затем используйте его в функции loadBooks как self.api.fetchBooks():

// src/stores/book/index.js
// ...
.views(self => ({
    get api() {
      return getEnv(self).api
    },
}))

Давайте теперь создадим фиктивный API с той же функцией выборки, что и настоящая функция выборки API:

// src/stores/book/mock-api/api.js
const books = require('./books')
const delayedPromise = (data, delaySecs = 2) =>
  new Promise(resolve => setTimeout(() => resolve(data), delaySecs * 1000))
const fetchBooks = () => delayedPromise(books)
export default {
  fetchBooks,
}

Я добавил задержку ответа, чтобы ответ не отправлялся сразу. Я также создал файл JSON с некоторыми данными, аналогичными данным ответа, отправленного API Google Книг src/stores/book/mock-api/books.json.

Теперь мы готовы внедрить фиктивный API в наши тесты. Создайте новый тестовый файл для нашего магазина со следующим содержанием:

// src/stores/book/__tests__/index.js
import { BookStore } from '../index'
import api from '../mock-api/api'
it('bookstore fetches data', async () => {
  const store = BookStore.create({ books: [] }, { api })
  await store.loadBooks()
  expect(store.books.length).toBe(10)
})

Запустите тест магазина:

npm test src/stores/book/__tests__/index.js

Вы должны увидеть пройденный тест.

Добавление фильтра книг и применение TDD

Я верю в гибридный подход к разработке через тестирование. По моему опыту, лучше всего работает, если вы сначала добавляете некоторые базовые функции при запуске проекта, или когда вы добавляете новый модуль или основные функции с нуля. После того, как базовая настройка и структура реализованы, TDD работает действительно хорошо.

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

Прежде чем мы начнем добавлять наши тесты и вносить изменения в наш магазин, я изменю задержку в нашем псевдониме API на 300 миллисекунд, чтобы наши тесты выполнялись быстрее.

const fetchBooks = () => delayedPromise(books, 0.3)

Нам нужно поле filter в нашей модели BookStore и действие setGenre() в нашем магазине для изменения значения этого filter.

it(`filter is set when setGenre() is called with a valid filter value`, async () => {
  store.setGenre('Nonfiction')
  expect(store.filter).toBe('Nonfiction')
})

Мы хотим запускать тесты только для нашего BookStore и держать тесты запущенными и следить за изменениями. Они будут повторно запущены после изменения кода. Итак, мы будем использовать команду watch и сопоставить путь к файлу с шаблоном:

npm test stores/book -- --watch

Вышеупомянутый тест должен завершиться неудачно, потому что мы еще не написали код для успешного прохождения теста. Принцип работы TDD заключается в том, что вы пишете атомарный тест для проверки наименьшей единицы бизнес-требования. Затем вы добавляете код, чтобы пройти этот тест. Вы повторяете один и тот же процесс, пока не добавите все бизнес-требования. Чтобы пройти тест, нам нужно добавить filterполе типа ENUM в нашу BookStore модель:

.model('BookStore', {
    books: t.array(Book),
    filter: t.optional(
        t.enumeration('FilterEnum', ['All', 'Fiction', 'Nonfiction']),
        'All'
    ),
})

И добавьте действие MST, которое позволит нам изменить значение фильтра:

const setGenre = genre => {
  self.filter = genre
}
return {
  //...
  setGenre,
}

С этими двумя изменениями мы должны оказаться в плюсе. Давайте также добавим отрицательный тест на недопустимое значение фильтра:

it(`filter is NOT set when setGenre() is called with an invalid filter value`, async () => {
  expect(() => store.setGenre('Adventure')).toThrow()
})

И этот тест тоже должен пройти. Это связано с тем, что мы используем тип ENUM в нашем магазине MST, и единственными допустимыми значениями являются All, Fiction и Nonfiction.

Вот разница наших последних изменений.

Сортировка и фильтрация книг

Первое значение индекса в поле categories фиктивных данных классифицирует книгу как Художественная или Документальная. Мы будем использовать его для фильтрации книг на вкладках Художественная литература и Художественная литература соответственно.

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

Давайте сначала добавим тест для сортировки книг:

it(`Books are sorted by title`, async () => {
  const books = store.sortedBooks
  expect(books[0].title).toBe('By The Book')
  expect(books[1].title).toBe('Jane Eyre')
})

Чтобы пройти тест, мы добавим представление с именем sortedBooks в нашу BookStoreмодель:

get sortedBooks() {
  return self.books.sort(sortFn)
},

И с этим изменением мы снова должны быть в зеленой зоне.

О представлениях MST

Мы только что добавили представление sortedBooks в нашу модель BookStore. Чтобы понять, как работают MST Views, нам нужно понять MobX. Ключевая концепция MobX: все, что может быть получено из состояния приложения, должно быть получено автоматически.

В этом видео на egghead.io создатель MobX Мишель Вестстрат объясняет ключевые концепции, лежащие в основе MobX. Я процитирую здесь ключевую концепцию:

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

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

Представления MST выводятся из текущего наблюдаемого состояния. Представления могут быть с аргументами или без них. Представления без аргументов - это в основном вычисленные значения из MobX, определенные с помощью функций получения. Когда наблюдаемое значение изменяется из действия MST, затронутое представление пересчитывается, вызывая изменение (реакцию) в @observer компонентах.

Добавление тестов по жанровому фильтру

Мы знаем, что в фиктивных данных есть семь документальных книг. Теперь добавим тест для фильтрации по genre:

it(`Books are sorted by title`, async () => {
  store.setGenre('Nonfiction')
  const books = store.sortedBooks
  expect(books.length).toBe(7)
})

Чтобы фильтрация по жанрам работала, мы добавим поле genre строкового типа в нашу Book модель и сопоставим его с volumeInfo.categories[0], полученным из ответа API. Мы также изменим метод получения sortedBooks представления в нашей BookStoreмодели, чтобы фильтровать книги перед их сортировкой:

get sortedBooks() {
  return self.filter === 'All'
    ? self.books.sort(sortFn)
    : self.books.filter(bk => bk.genre === self.filter).sort(sortFn)
},

И снова все испытания проходят.

Вот разница наших последних изменений.

Обновление пользовательского интерфейса при смене вкладки

ПРИМЕЧАНИЕ. С этого момента мы будем использовать фиктивные данные для наших реальных вызовов API вместо того, чтобы делать запросы Ajax к API Google Книг. Для этого я изменил bookApi в stores/book/index.js, чтобы он указывал на фиктивный API (./mock-api/api.js).

Также обратите внимание, что отображение всех трех вкладок («Все», «Художественная литература» и «Художественная литература») одинаково. Макет и формат элементов будут такими же, но разница только в данных, которые они будут отображать. А поскольку MobX позволяет нам полностью отделить наши данные от представления, мы можем избавиться от трех отдельных представлений и использовать один и тот же компонент для всех трех вкладок.

Это означает, что нам больше не нужны три отдельные вкладки. Поэтому мы удалим файл book-type-tabs.js и будем использовать компонент BookListView непосредственно в нашем TabNavigator для всех трех вкладок. Мы будем использовать обратный вызов tabBarOnPress, чтобы вызвать вызов setGenre() в нашем BookStore. routeName, доступный в объекте состояния навигации, передается в setGenre() для обновления фильтра, когда пользователь нажимает вкладку.

Вот обновленный TabNavigator:

// src/views/book/index.js
export default observer(
  createBottomTabNavigator(
    {
      All: BookListView,
      Fiction: BookListView,
      Nonfiction: BookListView,
    },
    {
      navigationOptions: ({ navigation }) => ({
        tabBarOnPress: () => {
          const { routeName } = navigation.state
          const store = BkStore()
          store.setGenre(routeName)
        },
      }),
    }
  )
)

Обратите внимание, что мы упаковываем createBottomTabNavigator в MobX observer. Это то, что преобразует класс компонента React или автономную функцию рендеринга в реактивный компонент. В нашем случае мы хотим, чтобы фильтр в нашем магазине книг менялся при вызове tabBarOnPress.

Мы также изменим представление, чтобы получать отсортированные книги вместо книг.

// src/views/book/components/BookListView.js
class BookListView extends Component {
  async componentWillMount() {
    this.store = BkStore()
    await this.store.loadBooks()
  }
render() {
    const { routeName } = this.props.navigation.state
    return (
      <View>
        <Title text={`${routeName} Books`} />
        <BookList books={this.store.sortedBooks} />
      </View>
    )
  }
}

Стилизация нашего списка книг

В нашем списке книг указаны только имя и автор каждой книги, но мы еще не добавили к нему никакого стиля. Давайте сделаем это с помощью компонента ListItem из react-native-elements. Это простое изменение:

// src/views/book/components/Book.js
import { ListItem } from 'react-native-elements'
export default observer(({ book }) => (
  <ListItem
    avatar={{ uri: book.image }}
    title={book.title}
    subtitle={`by ${book.authors.join(', ')}`}
  />
))

А вот как сейчас выглядит наша точка зрения:

! [Список книг с response-native-elements.png] (./ BookList с response-native-elements.png)

Вот разница наших последних изменений.

Добавить сведения о книге

Мы добавим поле selectedBook к нашему BookStore, которое будет указывать на выбранную модель книги.

selectedBook: t.maybe(t.reference(Book))

Мы используем ссылку MST для нашей selectedBook наблюдаемой. Ссылки в хранилищах MST позволяют легко ссылаться на данные и взаимодействовать с ними, сохраняя при этом данные нормализованными в фоновом режиме.

Мы также добавим действие для изменения этой ссылки:

const selectBook = book => {
  self.selectedBook = book
}

Когда пользователь нажимает на книгу в BookListView, мы хотим переместить пользователя на экран BookDetail. Итак, мы создадим для этого showBookDetail функцию и передадим ее в качестве опоры дочерним компонентам:

// src/views/book/components/BookListView.js
const showBookDetail = book => {
  this.store.selectBook(book)
  this.props.navigation.navigate('BookDetail')
}

В компоненте Book мы вызываем указанную выше функцию showBookDetail в onPress событии в Книге ListItem:

// src/views/book/components/Book.js
onPress={() => showBookDetail(book)}

Давайте теперь создадим BookDetailView, который будет отображаться, когда пользователь нажимает на книгу:

// src/views/book/components/BookDetailView.js
export default observer(() => {
  const store = BkStore()
  const book = store.selectedBook
return (
    <View>
      <View>
        <Card title={book.title}>
          <View>
            <Image
              resizeMode="cover"
              style={{ width: '60%', height: 300 }}
              source={{ uri: book.image }}
            />
            <Text>Title: {book.title}</Text>
            <Text>Genre: {book.genre}</Text>
            <Text>No of pages: {book.pageCount}</Text>
            <Text>Authors: {book.authors.join(', ')}</Text>
            <Text>Published by: {book.publisher}</Text>
          </View>
        </Card>
      </View>
    </View>
  )
})

Раньше у нас были только вкладки, но теперь мы хотим отображать детали, когда пользователь нажимает на книгу. Поэтому мы будем экспортировать createStackNavigator вместо прямого экспорта createBottomTabNavigator. createStackNavigator будет иметь два экрана в стеке, BookList и BookDetail:

// src/views/book/index.js
export default createStackNavigator({
  BookList: BookListTabs,
  BookDetail: BookDetailView,
})

Обратите внимание, что у нас есть представление "Список" и представление "Подробности" внутри createStackNavigator. Это потому, что мы хотим делиться одним и тем же BookDetailView только с другим контентом (отфильтрованные книги). Если бы мы хотели, чтобы на разных вкладках отображалось другое подробное представление, мы бы создали два отдельных StackNavigator и включили их в TabNavigator. Что-то вроде этого:

const TabStackA = createStackNavigator({
  Main: MainScreen,
  Detail: DetailScreen,
});
const TabStackB = createStackNavigator({
  Main: MainScreen,
  Detail: DetailScreen,
});
export default createBottomTabNavigator(
  {
    TabA: TabStackA,
    TabB: TabStackB,
  }
)

Вот разница наших последних изменений.

Стилизация вкладок

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

// src/views/book/index.js
const BookListTabs = observer(
  createBottomTabNavigator(
    {
      All: BookListView,
      Fiction: BookListView,
      Nonfiction: BookListView,
    },
    {
      navigationOptions: ({ navigation }) => ({
        // ...
      }),
      tabBarOptions: {
        labelStyle: {
          fontSize: 16,
          padding: 10,
        },
      },
    }
  )
)

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

Спасибо за чтение!

На этом мы завершаем наше руководство по созданию приложения ReactNative с магазином MobX. Надеюсь, вам понравился этот пост и он был вам полезен.

Первоначально опубликовано на qaiser.com.au.