Это вторая часть нашей серии руководств о React Native, Expo и новых функциях Expo SDK 33. Сегодня мы собираемся запачкать руки Redux, и пока мы это делаем, мы немного почистим и установите некоторые пресеты для всей будущей разработки, используя .editorconfig и linting.

[Часть 1] Настройка и навигация
[Часть 2] ← Вы здесь!

Если вы хотите пропустить часть 1, не стесняйтесь брать код из тега v0.1.1 на GitHub.

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

Файл .editorconfig

Если вы работаете в команде, это действительно помогает унифицировать способ написания нашего кода, потому что мы тратим гораздо больше времени на чтение кода, чем на его написание. Было бы здорово, если бы наша IDE автоматически вносила небольшие изменения во время работы, чтобы: а) мы лучше чувствовали требования на ходу и б) эти настройки можно было сохранять, изменять версии и делиться ими с командой?
Да, это было бы здорово. Я думаю, именно поэтому люди, работающие на editorconfig.org, начали этот замечательный проект, и многие IDE автоматически поддерживают файл .editorconfig с самого начала. Если выбранная вами IDE отсутствует в списке IDE с поддержкой этого файла, существуют совместимые плагины для всех основных и даже некоторых второстепенных альтернатив.

Вы можете найти копию моего .editorconfig на GitHub. Обычно я работаю с IntelliJ IDEA, который имеет полную поддержку из коробки, но, поскольку в настоящее время я использую VSCode для своих учебных проектов, мне потребовалось менее 5 минут, чтобы найти и установить подходящий плагин и включить мои пресеты во все мои новые проекты.

Линтинг

Анализ нашего кода — отличный способ убедиться, что весь проверенный код соответствует нашим согласованным стилям и правилам. Линтер берет набор правил и проверяет ваш код на соответствие этим рекомендациям по написанию кода. Мы собираемся использовать ESLint, чтобы сделать это за нас. Прежде всего, давайте возьмем все необходимые пакеты из npm и установим их в качестве зависимостей для разработки.

npm install --save-dev eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-react eslint-plugin-jsx-a11y babel-eslint

Помимо самого ESLint, мы собираемся использовать хорошо известный и часто адаптируемый набор правил, разработанный Airbnb. Чтобы все работало с React и Babel, нам также понадобится несколько плагинов. Обычный способ реализовать линтинг в проекте — установить пакеты, а затем выполнить следующую команду.

eslint --init // Don't run this line, if you use an existing config!

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

Чтобы запустить линтер, мы добавим 2 строки в скрипты в наш package.json между «web» и «eject».

"scripts": {
  "start": "expo start",
  "android": "expo start --android",
  "ios": "expo start --ios",
  "web": "expo start --web",
  "lint": "eslint src/ --ext .js,.jsx || true",
  "fix": "eslint --fix src/ --ext .js,.jsx || true",
  "eject": "expo eject"
},

В этом скрипте мы говорим eslint проверить папку src/ на наличие файлов с расширением .js и .jsx соответственно.

Я добавил «|| true», чтобы исправить (читай: взломать) уродливый вывод ошибки npm LIFECYCLE в консоли, который меня раздражал. Используйте это с осторожностью и не делайте этого, если вы планируете цепочку команд, где код выхода линтинга важен для следующих скриптов. Параметр «true» заставит скрипт выйти с кодом выхода 0 (успешно!), даже если при линтинге были ошибки.

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

npm run fix

Теперь, когда линтер исправил эти мелкие ошибки, у нас осталось 3 одинаковых предупреждения, информирующих нас о некорректном использовании кода JSX в файлах .js. Чтобы исправить это, мы должны сделать немного больше. Если мы изменим расширение этих файлов с .js на .jsx, сборщик Metro, который использует Expo, не будет знать, как с ними работать. Чтобы исправить это, мы добавим небольшой раздел конфигурации Metro в наш файл package.json, чтобы сообщить Metro, что js — не единственный тип файла, который разрешено разрешать.

"metro": {
  "resolver": {
    "sourceExts": [
      "js",
      "jsx",
      "json"
    ]
  }
},

Теперь мы можем безопасно изменить расширения наших файлов на .jsx и перезапустить DevTools.

Изменения в файлах конфигурации не вступят в силу, пока вы не перезапустите DevTools, поскольку эти файлы считываются только один раз при запуске. Просто отмените активный процесс выставки в командной строке, нажав «ctrl + c», и перезапустите их, набрав npm start.

Редукс

«Контейнер с предсказуемым состоянием для приложений JavaScript».

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

Чтобы Redux работал в нашем приложении, нам нужно включить дополнительную библиотеку для соединения React и Redux и подключить компоненты нашего приложения к нашему хранилищу. Мы включим оба в наши обычные зависимости через npm.

npm i -save redux react-redux

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

Базовая настройка

Позже я напишу и дам ссылку на более подробное руководство по Redux. А пока я советую вам взять my redux/folder from this GitHub Commit и продолжить оттуда. В следующем разделе я проведу вас через процесс создания нового действия и редуктора для нашего приложения.

После добавления папки и файлов redux в нашу папку src/ остается только обернуть наше приложение в поставщика, который позволит нам получить доступ к хранилищу. Давайте соответствующим образом настроим наш файл App.jsx.

// App.jsx
// add those imports
import { Provider } from 'react-redux'
import configureStore from './src/redux/store'
// create the store object
// adjust our export with the <Provider> tag
const store = configureStore()
export default function App() {
  return (
    <Provider store={store}> // our new wrapper
      <Navigation />
    </Provider>
  )
}

Вот и все! Отныне мы можем легко отправлять действия и считывать текущее состояние, подключая наши компоненты с помощью функции соединения react-redux.

Основной пример

Допустим, мы хотим отслеживать текущую версию нашего приложения с помощью Redux и отображать ее на главном экране. Принятие к сведению текущей версии приложения имеет большой смысл, если вы сохранили локальные данные и хотите проверить, выше ли текущая версия при загрузке, чем в последний раз, когда пользователь открывал приложение. Мы пока пропустим постоянное состояние, но давайте тем не менее добавим текущую версию на главный экран, и пока мы это делаем, мы добавим propTypes к нашему компоненту реагирования, чтобы убедиться, что наши типы свойств соответствуют нашему определению. Быстро запустите следующую строку, чтобы установить соответствующий пакет npm.

npm i -save prop-types

Изменения в нашей домашней сцене будут немного более обширными, но как только вы привыкнете к этому шаблону, он практически запишется сам. Начнем с импорта PropTypes и функции подключения в наш SceneHome.jsx.

// SceneHome.jsx
import PropTypes from 'prop-types'
import { connect } from 'react-redux'

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

  1. определите propTypes (альтернативой может быть TypeScript, но это совсем другое руководство)
  2. сопоставьте их с объектом состояния Redux
  3. подключите компонент к redux через connect() и передайте свои сопоставленные реквизиты
// SceneHome.jsx 
SceneHome.propTypes = {
    applicationState: PropTypes.object.isRequired,
}
 
const mapStateToProps = state => ({
    applicationState: state.application,
})
 
export default connect(
    mapStateToProps,
)(SceneHome)

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

// SceneHome.jsx
const SceneHome = (props) => {
  const { applicationState: { version } } = props
  return (
    <View style={styles.container}>
      <Text>Hello World!</Text>
      <Text>Home Page</Text>
      <Text>
        {`Version: ${version}`}
      </Text>
    </View>
  )
}

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

const initialState = { 
  status: false,
  version: -1,
}

Прямо сейчас приложение покажет нашу текущую версию как -1, поэтому давайте реализуем наше первое простое действие приведения. Создайте файл с именем application.actions.js в разделе redux/actions и вставьте в него следующий код.

// application.action.js
export const INIT = ‘[INIT]’
export const INIT_APPLICATION = `${INIT} Set Initial values for the application`
export const initialiseApplication = () => ({
 type: INIT_APPLICATION,
 payload: {},
})

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

Реализовав новое действие, нам нужно отреагировать на него в нашем редюсере, поэтому давайте соответствующим образом настроим наш application.reducer.js. Мы импортируем константу INIT_APPLICATION, которую мы определили ранее, а также импортируем манифест из нашего app.json для доступа к текущему номеру версии имени.

Затем мы говорим Redux следить за любым действием нашего типа инициализации и возвращать новый объект для нашего состояния с измененным статусом (теперь true), а также версию и имя из манифеста.

// application.reducer.js
import { INIT_APPLICATION } from '../actions/application.actions'
import manifest from '../../../app.json'
const { expo: { name, version } } = manifest
const initialState = {
  status: false,
  version: -1,
}
const applicationReducer = (state = initialState, action) => {
  switch (action.type) {
    case INIT_APPLICATION: {
      return {
        status: true,
        version,
        name,
      }
    }
    default: {
      return state
    }
  }
}
export default applicationReducer

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

Давайте кратко рассмотрим, как работает хук useEffect. Это сопоставимо с методами жизненного цикла типичных компонентов реакции на основе классов для componentDidMount, componentDidUpdate и componentWillUnmount. Использование хука довольно простое. Вы предоставляете хук useEffect со всеми реквизитами и функциями, которые хотите использовать, и код, который вы помещаете в тело хука, будет вызываться один раз, как только компонент будет смонтирован, и снова каждый раз, когда значение любого из параметров в предоставленном массиве изменения.

// example 1 // not part of the tutorial code
useEffect(() => {
  checkInit()
}, [checkInit])

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

// example 2 // not part of the tutorial code
useEffect(() => {
  console.log('init!')
  return () => console.log('unmounting...')
}, [checkInit])

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

// example 3 // not part of the tutorial code
useEffect(() => {
  const newCalculatedOutput = calculate(someState)
  console.log(newCalculatedOutput
}, [calculate, someState])

Допустим, «someState» изменяется в течение жизни нашего примерного компонента. Наш хук useEffect будет вычислять наши выходные данные один раз при монтировании, а затем снова каждый раз, когда изменяется «someState».

Итак, после этого небольшого отступления давайте вернемся к нашему учебному приложению и настроим наш файл SceneHome.jsx. Нам нужно импортировать новое действие, сопоставить его с нашими реквизитами и вызвать в хуке useEffect. Сделав это, приложение смонтирует наш компонент, хук отправит наше действие в redux, наш редюсер изменит состояние в нашем хранилище, а наш компонент будет обновляться, потому что наши реквизиты сопоставляются с состоянием в хранилище. В качестве бонуса я также добавляю имя приложения к номеру версии в выводе компонента.

// SceneHome.jsx
import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { StyleSheet, Text, View } from 'react-native'
import { initialiseApplication } from '../redux/actions/application.actions'

const SceneHome = (props) => {
    const { applicationState: { version, name }, checkInit } = props

    useEffect(() => {
        checkInit()
    }, [checkInit])

    return (
        <View style={styles.container}>
            <Text>
                {`${name} v${version}`}
            </Text>
            <Text>Home Page</Text>
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#fff',
        alignItems: 'center',
        justifyContent: 'center',
    },
})

SceneHome.propTypes = {
    applicationState: PropTypes.object.isRequired,
    checkInit: PropTypes.func.isRequired,
}

const mapStateToProps = state => ({
    applicationState: state.application,
})

const mapDispatchToProps = dispatch => ({
    checkInit: () => dispatch(initialiseApplication()),
})

export default connect(
    mapStateToProps,
    mapDispatchToProps,
)(SceneHome)

Если вы проверите сейчас, наша текущая версия настроена на стабильный основной выпуск «1.0.0», поскольку он обычно используется по умолчанию, если вы начинаете новый проект. Я бы сказал, что мы приберегаем первый основной выпуск на конец нашей серии руководств, когда мы закончим наш небольшой проект, поэтому давайте установим более реалистичный номер версии в нашем файле app.json. Я использую «0.1.2», так как сейчас мы находимся во второй части нашей серии руководств, и пока мы не изложим все основы, я не буду увеличивать младшую версию до «0.2.0».

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

Если вы хотите взглянуть на текущий код, перейдите по ссылке tag v0.1.2 — Base Redux Setup на GitHub. Если у вас возникли какие-либо проблемы, следуя инструкциям, не стесняйтесь задавать вопросы в комментариях. Я постараюсь вернуться к вам как можно скорее.

Как только эта серия будет закончена, я начну писать серию об основных принципах игрового дизайна и о том, как создавать простые игры для iOS, Android и Интернета с использованием Expo и React Native. Если вы хотите поддержать меня в написании других статей, подобных этой, или если есть конкретная тема, о которой вы хотели бы узнать больше, не стесняйтесь посетить мой сайт Patreon. Я буду публиковать там регулярные обновления, а также специальный контент и превью проектов, над которыми я работаю, в качестве бонуса для спонсоров.

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

Перспективы. Когда эта серия будет закончена, я начну писать серию статей об основных принципах дизайна игр и о том, как создавать простые игры для iOS, Android и Интернета с помощью Expo и React Native. Я также начал серию простых туториалов по Redux, и вскоре последует React. Если вы хотите поддержать меня в написании других статей, подобных этой, или если есть конкретная тема, о которой вы хотели бы узнать больше, не стесняйтесь посетить мой сайт Patreon. Я буду публиковать там регулярные обновления, а также специальный контент и превью проектов, над которыми я работаю, в качестве бонуса для спонсоров.