Целевая аудитория: любой, кто хоть немного знаком с Javascript.

Итак, что мы здесь делаем?

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

Нажмите здесь, чтобы опубликовать эту статью в LinkedIn »

Давайте начнем!

Нет ничего хуже, чем чувство пустоты, когда вы удобно устраиваетесь перед телевизором, понимаете, что закончили свое любимое телешоу и не можете решить, что смотреть дальше, так что давайте создадим рулетку Netflix!

TL; DR вот ссылка на репо



Есть несколько способов создать проект React Native. Сегодня мы будем использовать приложение create-react-native-app (плюсы и минусы мы обсудим в будущих публикациях).

Введите на терминале следующие команды:

npm install -g create-react-native-app
create-react-native-app NetflixRoulette
cd NetflixRoulette
npm start

Если все прошло хорошо, вы должны увидеть на терминале что-то вроде этого:

Если вы хотите запустить приложение на своем реальном устройстве, загрузите клиент expo (Android или iOS) и просканируйте QR-код, это очень просто.

Вы также можете использовать симулятор, набрав «a» для Android (для этого может потребоваться дополнительная настройка) или «i» для iOS (если вы используете Mac и у вас установлен XCode, у вас все готово).

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

Точка входа - файл App.js. Если вы откроете его, вы увидите код, который генерирует экран выше. Простой компонент View, содержащий текст.

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

Давайте создадим папку «src» с папкой «components» внутри.

Создайте файл titleFinder.js внутри папки компонентов и добавьте следующий код:

import React, { Component } from 'react'
import { StyleSheet, Text, View } from 'react-native'
export default class TitleFinder extends Component {
   render() {
      return (
         <View style={styles.container}>
            <Text> Netflix Roulette! </Text>
         </View>
      )
   }
}
const styles = StyleSheet.create({
   container: {
      flex: 1,
      backgroundColor: '#fff',
      alignItems: 'center',
      justifyContent: 'center',
   },
})

А затем замените код в App.js следующим образом:

import React from 'react'
import TitleFinder from './src/components/titleFinder'
export default class App extends React.Component {
   render() {
      return (
         <TitleFinder />
      )
   }
}

Теперь точка входа просто создаст экземпляр нашего первого компонента с именем TitleFinder, но подождите,

Что такое компонент?

Все, что вы видите на экране, является своего рода компонентом. Это функция, которая возвращает некоторый JSX для рендеринга.

JS Что? JSX

JSX - это расширение Javascript, используемое для написания компонентов. Та же семантика, что и HTML. В этом случае мы не будем использовать компоненты HTML (div, p, h1 и т. Д.), Но будем использовать готовые собственные компоненты реагирования, которые в конечном итоге создадут собственные компоненты.

Вавилон

JSX → простой Javascript

Babel - это транспилятор JavaScript, который преобразует JSX в простой старый JavaScript ES5, который может работать в любом браузере (даже в старых).

Состояние

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

Разница с обычными переменными заключается в том, что вы не можете установить значение напрямую, вам нужно использовать функцию с именем setState.

Компоненты без состояния и с отслеживанием состояния

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

Давайте посмотрим на несколько примеров:

Как мы видели на скетче, на первом экране будут следующие элементы:

  • Заголовок с текстом «Предложи мне название».
  • Две вкладки, одна для телешоу, другая для фильмов.
  • Ползунок для выбора минимального рейтинга IMDB
  • Кнопка для вращения рулетки

Заголовок

Так же, как TitleFinder, давайте создадим еще один компонент под названием Header.

И снова в папке src / components создайте файл с именем header.js со следующим кодом:

import React from ‘react’
import { View, Text, Platform, StyleSheet } from ‘react-native’
const Header = ({ title }) => (
 <View style={styles.container}>
    <Text style={styles.title}>{title}</Text>
 </View>
)
const styles = StyleSheet.create({
 container: {
    height: Platform.OS === ‘ios’ ? 60 : 80,
    width: ‘100%’,
    borderBottomWidth: 1,
    borderBottomColor: ‘#dadadc’,
    justifyContent: ‘flex-end’,
    alignItems: Platform.OS === ‘ios’ ? ‘center’ : ‘flex-start’,
    backgroundColor: Platform.OS === ‘ios’ ? ‘#f8f8f8’ : ‘#3f51b5’
 },
 title: {
    fontFamily: Platform.OS === ‘ios’ ? “System” : “Roboto”,
    color: Platform.OS === ‘ios’ ? “#000” : “#fff”,
    fontSize: 17,
    fontWeight: ‘bold’,
    paddingBottom: Platform.OS === ‘ios’ ? 10 : 17,
    paddingLeft: Platform.OS === ‘ios’ ? 0 : 25
 }
})
export default Header

Обратите внимание, что в этом случае Header не является классом и не наследуется от React.Component. Вместо этого мы создадим функцию, которая получает некоторые параметры и возвращает некоторый JSX.

На всякий случай, если вы не знакомы с ES6:

({title}) - это то же самое, что (props) и доступ к заголовку с помощью props.title. Деструктуризация объекта здесь.

Вкладки

src / components / tab.js

import React from 'react'
import { TouchableOpacity, Text, StyleSheet } from 'react-native'
const Tab = ({ name, isActive, onPress }) => (
  <TouchableOpacity
    style={isActive ? [styles.container, styles.active] : styles.container}
    onPress={() => onPress(name)}>
    
    <Text style={isActive ? [styles.text, styles.textActive] : styles.text}>
      {name}
    </Text>
</TouchableOpacity>
)
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    height: 40
  },
  active: {
    borderBottomWidth: 3,
    borderBottomColor: '#0479fb'
  },
  text: {
    color: '#9d9d9d'
  },
  textActive: {
    color: '#0479fb',
    fontWeight: 'bold',
    paddingTop: 3
  }
})
export default Tab

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

Мы вернемся к onPress позже, но пока предположим, что это просто функция, отправленная извне (реализованная где-то еще) и срабатывающая, когда кто-то прикоснется к вкладке.

Слайдер

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

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

renderScorePicker () {
   const scoreToDisplay = this.state.minimumScore === 0 ? 'Any' : `More than ${this.state.minimumScore}`
   return (
      <React.Fragment>
         <Text
            key='scoreLabel'
            style={styles.scoreLabel}>
            IMDB score:
         </Text>
         <Slider
            key='scorePicker'
            step={1}
            minimumValue={0}
            maximumValue={9}
            onValueChange={(newValue) => this.setState({minimumScore: newValue})}
            value={this.state.minimumScore}
            style={styles.scoreSlider}
         />
         <Text
            key='scoreValue'
            style={styles.scoreValue}>
            {scoreToDisplay}
         </Text>
      </React.Fragment>
   )
}

В предыдущих версиях React (‹16) мы были вынуждены возвращать ровно один компонент, поэтому в этом случае (текст, слайдер и другой текст) нам нужно было что-то, чтобы обернуть весь контент, например ‹View›. Теперь с React 16 можно вернуть массив элементов ИЛИ использовать React.Fragment, который не добавляет никаких дополнительных узлов в дерево компонентов. Таким образом, он более плоский и производительный.

Помните, мы определили состояние как переменную, которая автоматически обновляется в пользовательском интерфейсе при изменении значения? Вот как это работает:

Сопоставьте значение с состоянием:

value={this.state.minimumScore}

В onChange нам нужно вызвать setState для обновления нового значения:

onValueChange={(newValue) => {
   this.setState({minimumScore: newValue})
}}

Таким образом, когда ввод (в данном случае ползунок) изменяется, выполняется функция onValueChange, которая обновляет локальное состояние новым значением. Поскольку каждый раз при обновлении переменной вызывается функция setState, рендеринг будет выполняться снова, и значение будет обновлено в пользовательском интерфейсе.

Кнопка

То же самое и для кнопки, функции внутри компонента TitleFinder.

renderSpinButton () {
   const buttonText = this.state.data ? 'Spin again!' : 'Spin!'
   return (
      <View style={styles.buttonContainer}>
         <Button
            onPress={this.fetchData.bind(this)}
            title={buttonText} />
      </View>
   )
}

Что это?

onPress={this.fetchData.bind(this)}

fetchData будет функцией, которая в конечном итоге вызовет конечную точку и получит данные. Но зачем нам использовать .bind? ну, если вы этого не сделаете, вы не сможете получить доступ к this (как TitleFinder) внутри реализации функции (в данном случае fetchData). В этом случае это будет самой кнопкой. Используя .bind, мы определяем контекст, в котором будет выполняться функция. Это будет иметь больше смысла, когда мы обсудим реализацию через минуту.

Текст изменится, если у нас уже есть данные для отображения, но подождите, я не объяснил местное состояние, не так ли?

Собираем все вместе

Не торопитесь на секунду, я объясню это позже. Давайте создадим в папке src файл с именем constants.js и вставим его содержимое внутрь:

export const BASE_RECOMMENDATION_URL = 'https://api.reelgood.com/v1/roulette/netflix?'
export const BASE_IMAGE_URL = 'https://img.reelgood.com/content/show'
export const IMAGE_POSTFIX = 'poster-780.jpg'
export const BASE_OPTIONS = {
   nocache: true,
   kind: 0,
   minimumScore: 0,
   sources: [
      'amazon_prime',
      'fx_tveverywhere',
      'hbo',
      'netflix',
      'showtime',
      'starz'
   ],
   free: true
}
export const MAX_SCORE = 9
export const TV_SHOWS = 'TV Shows'
export const MOVIES = 'Movies'

Мы будем использовать их позже.

Напишем метод конструктора TitleFinder:

Но сначала импортируйте только что созданные константы:

import * as Constants from ‘../constants’
constructor(props) {
    super(props)
    this.state = {
      loading: false,
      error: null,
      data: null,
      minimumScore: 0,
      activeTab: Constants.TV_SHOWS
    }
}

ТОЛЬКО в конструкторе мы присваиваем значение непосредственно состоянию. Мы называем это начальным состоянием.

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

ВАЖНАЯ ИНФОРМАЦИЯ О ГОСУДАРСТВЕ

this.setState({someValue: 'new value'})
console.log(this.state.someValue)

Угадай, что печатается на консоли

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

this.setState({someValue: 'new value'}, () => {
   console.log(this.state.someValue) // 'new value' 
})

Мы еще не добавили вкладки. Давайте создадим небольшой компонент, который будет обрабатывать обе вкладки за нас:

const TypeSelector = ({activeTab, onTabPress}) => {
  return (
    <View style={{ flexDirection: 'row' }}>
      <Tab 
        name={Constants.TV_SHOWS} 
        isActive={activeTab === Constants.TV_SHOWS}
        onPress={onTabPress}
      />
      <Tab 
        name={Constants.MOVIES} 
        isActive={activeTab === Constants.MOVIES}
        onPress={onTabPress}
      />
    </View>
  )
}

В компоненте TitleFinder напишите эти две функции:

renderType () {
  return (
    <TypeSelector
      activeTab={this.state.activeTab}
      onTabPress={this.onTabPress} />
  )
}
  
onTabPress = tab => {
  this.setState({ activeTab: tab })
}

Обратите внимание, что на этот раз мы не использовали. bind для onTabPress. Просто для того, чтобы показать вам еще один способ написания функций. Синтаксис жирной стрелки (= ›) сохраняет контекст, поэтому внутри этой функции this означает TitleFinder, а не компонент TypeSelector (тот, который запустил действие).

Наконец, функция рендеринга:

render() {
   return (
      <View style={styles.container}>
         <Header title='Suggest me a title' />
         {this.renderType()}
         {this.renderScorePicker()}
         {this.renderSpinButton()}
      </View>
   )
}

Еще две вещи, прежде чем мы взглянем на симулятор / устройство:

  • Удалите стили для контейнера (мы будем работать над этим позже)
  • Создайте функцию-заполнитель для fetchData:
fetchData () { console.log('Fetch data called') }

Если все прошло хорошо, вы увидите что-то вроде этого:

Если вы выберете «Телешоу» или «Фильмы», компонент вкладки будет обновляться, а также ползунок при изменении значения.

Давайте сначала создадим в папке src файл с именем utils.js со следующим кодом:

import * as Constants from './constants'
export function getImageURL (id) {
  return `${Constants.BASE_IMAGE_URL}/${id}/${Constants.IMAGE_POSTFIX}`
}
export function getRecommendationUrl ({activeTab, minimumScore}) {
  let params = ''
  let modifieldFilter = Object.assign({}, Constants.BASE_OPTIONS)
  modifieldFilter.kind = activeTab === Constants.TV_SHOWS ? 1 : 2
  modifieldFilter.minimumScore = minimumScore
const keys = Object.keys(modifieldFilter)
  keys.forEach((key, index) => {
    params += `${key}=${modifieldFilter[key]}`
    if (index < keys.length - 1) {
      params += '&'
    }
  })
  return Constants.BASE_RECOMMENDATION_URL + params
}

И импортируйте getRecommendationUrl в компонент TitleFinder, чтобы функция fetchData могла его использовать:

import { getRecommendationUrl } from '../utils'

Вот реальная реализация fetchData:

fetchData () {
    const { activeTab, minimumScore } = this.state
    const url = getRecommendationUrl({activeTab, minimumScore})
    this.setState({
      loading: true,
      data: null,
      error: false
    }, () => {
      fetch(url)
      .then((recommendationResponse) => recommendationResponse.json())
      .then((recommendationJSON) => {
        this.setState({
          data: recommendationJSON,
          error: false,
          loading: false
        })     
      })
      .catch((error) => {
        this.setState({
          loading: false,
          error: true,
          data: null
        })
      })
    })
  }

fetchData будет выполняться при нажатии кнопки вращения и будет делать следующее:

  • Выберите выбранные значения для типа рекомендации (телешоу или фильм) и минимальный балл IMDB в местном штате.
  • Вызовите функцию util для создания URL с этими параметрами.
  • Обновите локальное состояние, чтобы не было ошибок, данных и указать, что идет загрузка.
  • Выполните выборку и, когда она вернется, установите данные или, если произошла ошибка, установите флаг ошибки в локальном состоянии.

Хорошо, теперь у нас есть необходимые данные! иди и посмотри Netflix!

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

Но сначала давайте добавим еще один ресурс: изображение по умолчанию, когда оно недоступно в API.

На том же уровне src создайте папку с именем images и внутри нее поместите любое изображение с именем defaultImage.png.

Теперь создайте компонент внутри src / components с именем advice.js.

import React, { Component } from 'react'
import { ImageBackground, Text, StyleSheet, ActivityIndicator } from 'react-native'
import { getImageURL } from '../utils'
import defaultImage from '../../images/defaultImage.png'
class Recommendation extends Component {
   constructor (props) {
      super(props)
      this.state = {
         errorOnImageLoad: false,
         loading: false
      }
   }
   renderImage = () => {
      if (this.props.has_poster) {
         const uri = getImageURL(this.props.id)
         const { errorOnImageLoad, loading } = this.state
         let source
         if (errorOnImageLoad) {
            source = defaultImage
         } else {
            source = { uri }
         }
         return (
            <View style={styles.container}>
               <View style={styles.indicatorContainer}>
                  <ActivityIndicator
                     size='small'
                     animating={this.state.loading} />
               </View>
               <Image
                  onLoadStart={() => {
                     this.setState({loading: true})
                  }}
                  onLoadEnd={() => {
                     this.setState({loading: false})
                  }}
                  onError={() => {
                     this.setState({errorOnImageLoad: true})
                  }}
                 source={source}
                 style={styles.image}
                 resizeMode='contain'/>
            </View>
         )
       }
    }
   render () {
      const {id, has_poster, title, imdb_rating, released_on, overview} = this.props
      return (
         <React.Fragment>
            <Text
               key='title'
               style={styles.title}>
               {title}
            </Text>
            {this.renderImage()}
            <Text
               key='info'
               style={[styles.info, styles.text]}>
{`IMDB: ${imdb_rating} - ${new Date(released_on).getFullYear()}`}
            </Text>
            <Text
               key='overview'
               style={[styles.overview, styles.text]}>
               {overview}
             </Text>
         </React.Fragment>
      )
   }
}
const styles = StyleSheet.create({
   title: {
      marginTop: 40,
      marginBottom: 20,
      textAlign: 'center',
      fontSize: 17,
      fontWeight: 'bold'
   },
   info: {
      fontWeight: 'bold',
      marginHorizontal: 15,
      marginVertical: 15
   },
   text: {
      marginHorizontal: 15,
      marginVertical: 5
   },
   overview: {
      marginBottom: 5,
      fontWeight: '100'
   },
   container: {
      flex: 1,
      height: 400,
      position: 'relative',
      justifyContent: 'center',
      alignItems: 'center'
   },
   image: {
      flex: 1,
      width: '100%'
   },
   indicatorContainer: {
      position:'absolute'
   }
})
export default Recommendation

Теперь давайте добавим логику к функции рендеринга TitleFinder:

  • Переместите текущую реализацию рендеринга в функцию с именем renderForm.
  • Создайте функцию под названием renderRecommendation
  • Добавьте некоторую логику к рендерингу, чтобы отображать правильные компоненты в зависимости от состояния.
  • Также добавьте функцию отрисовки кнопки сброса renderResetButton плюс функцию reset, которая очистит данные и флаги в нашем локальном состоянии .
  • Не забудьте импортировать компонент рекомендаций!
import Recommendation from '../components/recommendation'
renderForm () {  
    return (
      <View style={styles.container}>
        <Header title='Suggest me a title' />
        {this.renderType()}
        {this.renderScorePicker()}
        {this.renderSpinButton()}
      </View>
    )
  }
renderRecommendation (recommendation) {
    const { id } = recommendation
    return (
      <ScrollView style={styles.recommendationContainer}>
        <Recommendation {...recommendation} />
        {this.renderSpinButton()}
        {this.renderResetButton()}
      </ScrollView>
    )
}
  
renderResetButton () {
   return (
      <View style={styles.buttonContainer}>
         <Button
            onPress={this.reset.bind(this)}
            title='Reset filters!' />
      </View>
   )
}
reset () {
   this.setState({
      loading: false,
      error: false,
      data: null,
      minimumScore: 0,
      activeTab: Constants.TV_SHOWS
   })
}
render() {
    const { loading, data, error, activeTab } = this.state
    let content
    if (data) {
      return this.renderRecommendation(data)
    } else if (loading) {
      content = <ActivityIndicator size='large'/>
    } else if (error) {
      content = <Text> Ops! </Text>
    } else {
      content = this.renderForm()
    }
    
    return (
      <View style={loading ? styles.loaderContainer : styles.container}>
        {content}
      </View>
    )
  }

Еще одна деталь стиля 💅

const styles = StyleSheet.create({
  scoreLabel: {
    paddingTop: 20,
    paddingLeft: 10
  },
  scoreValue: {
    textAlign: 'center',
    marginBottom: 10
  },
  scoreSlider: {
    margin: 10
  },
  loaderContainer: {
    flex: 1,
    flexDirection: 'column',
    justifyContent: 'center',
    alignItems: 'center'
  },
  recommendationContainer: {
    marginBottom: 10
  },
  buttonContainer: {
    paddingHorizontal: 20,
    paddingVertical: 8
  }
})

И мы закончили 🎉

Что делать дальше?

Вот список полезных ресурсов для продолжения этого путешествия:

  • Официальная документация: ознакомьтесь с API и готовыми компонентами.
  • Сообщество Reactiflux, присоединяйтесь к нему в Discord! Если вы только начинаете, вы можете почувствовать себя немного ошеломленным, особенно потому, что это чат с множеством дискуссий, происходящих одновременно, но определенно того стоит, так как основная команда, отвечающая за реагирование и реагирование, присутствует. Лучшее место, чтобы быть в курсе.
  • Реагирующий дайджест, одно письмо в неделю, 5 ссылок.
  • Информационный бюллетень ReactJS

Будущие статьи:

  • React Native, что происходит за кулисами?
  • CRNA vs react-native init, что мне использовать?
  • Навигация по React Native, тьфу

Я надеюсь, вы нашли этот пост полезным

Спасибо за прочтение! и счастливого кодирования 🤘😎