The Movie DB / React Native Part 1: Components

April 26, 2017

In this series of articles, we will create a simple app for discovering TV series, based on The Movie DB API. This series is made up of three articles:

  • Part 1: Creating the components
  • Part 2: Navigation
  • Part 3: Data

A preview of the result is available on YouTube.

In this part, you will not need an API key; the project will contain default data. Let's start by creating a new project:

While creating the components, we will need to use icons, so let's install the react-native-vector-icons library.

yarn add react-native-vector-icons
react-native link

Now let's move on to creating the components. Start by creating an src folder at the root of the app and an index.js file, which will be the app's entry point.

mkdir src
cd src && touch index.js

Paste the following code into the index.js file.

import React, { Component } from 'react'
import { AppRegistry, StyleSheet, Text, View } from 'react-native'
 
export default class App extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>Welcome to React Native!</Text>
        <Text style={styles.instructions}>To get started, edit index.ios.js</Text>
        <Text style={styles.instructions}>
          Press Cmd+R to reload,
          {'\n'}
          Cmd+D or shake for dev menu
        </Text>
      </View>
    )
  }
}
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
})

Replace the code in the index.ios.js and index.android.js files with the following:

import { AppRegistry } from 'react-native'
import App from './src/index.js'
AppRegistry.registerComponent('AwesomeSerie', () => App)

Create a components folder where we will add all the components

mkdir components

The first component we will create is the ImageWithOverlay component, which will be used to present the series on the main screen.

ImageWithOverlay.js

'use strict'
import React, { Component } from 'react'
import { StyleSheet, View, Image } from 'react-native'
 
class ImageWithOverlay extends Component {
  constructor(props) {
    super(props)
  }
 
  render() {
    return (
      <View>
        <Image
          resizeMode="stretch"
          style={{ height: this.props.height, width: this.props.width }}
          source={{ uri: this.props.src }}
        />
        <View style={styles.overlay} />
      </View>
    )
  }
}
 
ImageWithOverlay.defaultProps = {
  src: 'https://image.tmdb.org/t/p/w600_and_h900_bestv2/3iYNC7Iw6a65ed5GZz7KbInSHBd.jpg',
  width: 375, //iphone 6
  height: 667, // iphone 6
}
 
ImageWithOverlay.propTypes = {
  src: React.PropTypes.string.isRequired,
  width: React.PropTypes.number.isRequired,
  height: React.PropTypes.number.isRequired,
}
 
export default ImageWithOverlay
 
const styles = StyleSheet.create({
  overlay: {
    position: 'absolute',
    left: 0,
    top: 0,
    bottom: 0,
    right: 0,
    opacity: 0.7,
    backgroundColor: 'black',
  },
})

Next, in the app preview, you can see that on a series detail screen there is an image with a diagonal edge, so we will create the DiagonalImage component.

DiagonalImage.js

'use strict'
import React, { Component } from 'react'
import { StyleSheet, View, Image } from 'react-native'
 
class DiagonalImage extends Component {
  constructor(props) {
    super(props)
  }
 
  render() {
    return (
      <View>
        <Image
          resizeMode="stretch"
          style={{ height: this.props.height, width: this.props.width }}
          source={{ uri: this.props.src }}
        />
        <View
          style={[
            {
              borderRightWidth: this.props.width,
              borderTopWidth: this.props.height / 3.5,
            },
            styles.triangle,
          ]}
        />
      </View>
    )
  }
}
 
DiagonalImage.defaultProps = {
  src: 'https://image.tmdb.org/t/p/w600_and_h900_bestv2/3iYNC7Iw6a65ed5GZz7KbInSHBd.jpg',
  width: 375, //iphone 6
  height: 667, // iphone 6
}
 
DiagonalImage.propTypes = {
  src: React.PropTypes.string.isRequired,
  width: React.PropTypes.number.isRequired,
  height: React.PropTypes.number.isRequired,
}
 
export default DiagonalImage
 
const styles = StyleSheet.create({
  triangle: {
    width: 0,
    height: 0,
    backgroundColor: 'transparent',
    borderStyle: 'solid',
    borderRightColor: 'transparent',
    borderTopColor: 'white',
    transform: [{ rotate: '180deg' }],
    position: 'absolute',
    bottom: 0,
    right: 0,
  },
})

Let's briefly revisit how this component works. To create this diagonal effect, we need to create a view shaped like a triangle, with the screen background color, positioned at the bottom right of the image.

Here is the style that creates a triangle from a view:

triangle:{
  width: 0,
  height: 0,
  backgroundColor: 'transparent',
  borderStyle: 'solid',
  borderTopWidth: 90,
  borderRightWidth: 90,
  borderRightColor: 'transparent',
  borderTopColor: 'white',
  transform: [
    {rotate: '180deg'}
  ],
  position:'absolute',
  bottom:0,
  right:0,
}

The code above will create a 90*90 triangle. In our case, we want the width to be equal to the screen width and the height to be a ratio of the image height, so we will define the two borderTopWidth and borderRightWidth properties dynamically.

  • borderTopWidth: image height / 3.5
  • borderRightWidth: screen width

If you want to learn more about creating different shapes with simple views and styles, you can read this article.

Before the detailed series screen, we will create a component that displays additional information about the series.

IconInfos.js

'use strict'
import React, { Component } from 'react'
import { StyleSheet, Text, View, Image } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome'
 
class IconInfos extends Component {
  constructor(props) {
    super(props)
  }
 
  render() {
    return (
      <View style={styles.container}>
        <Icon name={this.props.iconName} size={16} color={this.props.color} />
        <Text
          style={{
            marginLeft: 13,
            fontWeight: 'bold',
            color: this.props.color,
          }}
        >
          {this.props.text}
        </Text>
      </View>
    )
  }
}
 
IconInfos.propTypes = {
  iconName: React.PropTypes.string.isRequired,
  color: React.PropTypes.string.isRequired,
  text: React.PropTypes.string.isRequired,
}
IconInfos.defaultProps = {
  iconName: 'flag',
  color: '#575050',
  text: 'FR',
}
 
export default IconInfos
 
const styles = StyleSheet.create({
  container: {
    justifyContent: 'flex-start',
    flexDirection: 'row',
  },
})

Now let's create the detailed screen for a series. Here are all the properties this component will receive.

  • serieItem: object containing information about the series
  • goBack: navigation function used to return to the previous screen

SerieDetail.js

'use strict'
import React, { Component } from 'react'
import { StyleSheet, Text, View, Image, ScrollView, TouchableOpacity } from 'react-native'
import DiagonalImage from './DiagonalImage'
import IconInfos from './IconInfos'
import Icon from 'react-native-vector-icons/FontAwesome'
 
class SerieDetail extends Component {
  constructor(props) {
    super(props)
    this.state = {
      width: 0,
      height: 0,
    }
  }
 
  _onLayout(event) {
    var { x, y, width, height } = event.nativeEvent.layout
    this.setState({
      width: width,
      height: height,
    })
  }
 
  render() {
    const diagonalImageHeight = this.state.height / 2.1
    return (
      <ScrollView
        style={{ backgroundColor: 'white' }}
        onLayout={event => {
          this._onLayout(event)
        }}
      >
        <View style={styles.container}>
          <DiagonalImage
            src={'https://image.tmdb.org/t/p/w500/' + this.props.serieItem.poster_path}
            height={diagonalImageHeight}
            width={this.state.width}
          />
          <TouchableOpacity style={styles.back} onPress={() => this.props.goBack()}>
            <Icon name="chevron-left" color="white" size={26} />
          </TouchableOpacity>
          <View style={[styles.subInfos, { width: this.state.width }]}>
            <IconInfos iconName="flag" text={this.props.serieItem.origin_country[0]} />
            <IconInfos iconName="star" text={this.props.serieItem.vote_average.toString()} />
            <IconInfos iconName="calendar" text={this.props.serieItem.first_air_date.split('-')[0]} />
          </View>
          <View>
            <View style={styles.infos}>
              <Text style={styles.title}>{this.props.serieItem.original_name}</Text>
              <Text style={styles.description}>{this.props.serieItem.overview}</Text>
            </View>
          </View>
        </View>
      </ScrollView>
    )
  }
}
 
SerieDetail.propTypes = {
  serieItem: React.PropTypes.object.isRequired,
  goBack: React.PropTypes.func.isRequired,
}
SerieDetail.defaultProps = {
  serieItem: {
    poster_path: '/mBDlsOhNOV1MkNii81aT14EYQ4S.jpg',
    popularity: 54.910076,
    id: 44217,
    backdrop_path: '/A30ZqEoDbchvE7mCZcSp6TEwB1Q.jpg',
    vote_average: 6.88,
    overview:
      "Vikings follows the adventures of Ragnar Lothbrok, the greatest hero of his age. The series tells the sagas of Ragnar's band of Viking brothers and his family, as he rises to become King of the Viking tribes. As well as being a fearless warrior, Ragnar embodies the Norse traditions of devotion to the gods. Legend has it that he was a direct descendant of Odin, the god of war and warriors.",
    first_air_date: '2013-03-03',
    origin_country: ['IE', 'CA'],
    genre_ids: [18, 10759],
    original_language: 'en',
    vote_count: 399,
    name: 'Vikings',
    original_name: 'Vikings',
  },
  goBack: () => console.log('go back'),
}
 
export default SerieDetail
 
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-start',
    alignItems: 'center',
    backgroundColor: 'white',
  },
  back: {
    position: 'absolute',
    top: 24,
    left: 16,
    backgroundColor: 'transparent',
  },
  infos: {
    justifyContent: 'flex-start',
    alignItems: 'center',
    padding: 16,
    marginTop: 12,
  },
  subInfos: {
    justifyContent: 'space-between',
    flexDirection: 'row',
    paddingHorizontal: 16,
    marginTop: 12,
  },
  title: {
    color: '#575050',
    fontWeight: 'bold',
    fontSize: 18,
    marginBottom: 20,
  },
  description: {
    color: '#575050',
    fontWeight: '500',
    fontSize: 14,
    textAlign: 'center',
  },
})

To finish, we will create the listView that displays the list of series retrieved through the API, as well as the listViewItem component. Let's start by creating the listViewItem component. Here are its properties:

onItemPress: Displays the image detail screen

  • image: URL of the poster image
  • height: Height
  • width: Width
  • title: Series title
  • description: Series description ListViewItem.js
'use strict'
import React, { Component } from 'react'
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native'
import ImageWithOverlay from './ImageWithOverlay'
 
class ListViewItem extends Component {
  constructor(props) {
    super(props)
  }
 
  render() {
    return (
      <TouchableOpacity activeOpacity={0.5} onPress={() => this.props.onItemPress()}>
        <ImageWithOverlay src={this.props.image} height={this.props.height} width={this.props.width} />
        <View style={styles.infosContainer}>
          <Text elispsisMode="tail" numberOfLines={1} style={styles.title}>
            {this.props.title}
          </Text>
          <View style={styles.separator} />
          <Text elispsisMode="tail" numberOfLines={4} style={styles.description}>
            {this.props.description}
          </Text>
        </View>
      </TouchableOpacity>
    )
  }
}
 
export default ListViewItem
 
const styles = StyleSheet.create({
  infosContainer: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    height: 150,
    padding: 16,
    backgroundColor: 'transparent',
  },
  title: {
    color: 'white',
    fontWeight: 'bold',
    fontSize: 18,
  },
  description: {
    color: 'white',
    textAlign: 'justify',
  },
  separator: {
    backgroundColor: 'white',
    height: 1,
    marginTop: 8,
    marginBottom: 8,
  },
})

Finally, let's move on to the listView. Here are its properties:

  • data: list of data returned by the API
  • showDetail: navigation function used to display the detail screen for a series,
  • nextPage: next page of results,
  • hasMoreResult: defines whether there are pages left to load,

SerieListView.js

'use strict'
import React, { Component } from 'react'
import { StyleSheet, Text, View, ListView, TouchableOpacity } from 'react-native'
import ListViewItem from './ListViewItem'
import Icon from 'react-native-vector-icons/FontAwesome'
 
class SerieListView extends Component {
  constructor(props) {
    super(props)
    const ds = new ListView.DataSource({
      rowHasChanged: (r1, r2) => r1 !== r2,
    })
    this.state = {
      ds: ds,
      dataSource: ds.cloneWithRows(this.props.data),
    }
  }
 
  _onLayout(event) {
    var { x, y, width, height } = event.nativeEvent.layout
    this.setState({
      width: width,
      height: height,
    })
  }
 
  _onEndReached() {
    if (this.props.hasMoreResult) {
      //Fetch data
    }
  }
 
  render() {
    return (
      <View style={{ flex: 1 }}>
        <ListView
          style={{ backgroundColor: '#706666' }}
          onEndReached={() => this._onEndReached()}
          onEndReachedThreshold={10}
          enableEmptySections={true}
          onLayout={event => {
            this._onLayout(event)
          }}
          dataSource={this.state.dataSource}
          renderRow={rowData => (
            <ListViewItem
              onItemPress={() => this.props.showDetail(rowData)}
              title={rowData.original_name}
              description={rowData.overview}
              image={'https://image.tmdb.org/t/p/w500/' + rowData.poster_path}
              height={this.state.height}
              width={this.state.width}
            />
          )}
        />
      </View>
    )
  }
}
SerieListView.propTypes = {
  data: React.PropTypes.array.isRequired,
  showDetail: React.PropTypes.func.isRequired,
  nextPage: React.PropTypes.number.isRequired,
  hasMoreResult: React.PropTypes.bool.isRequired,
}
 
SerieListView.defaultProps = {
  data: [
    {
      poster_path: '/mBDlsOhNOV1MkNii81aT14EYQ4S.jpg',
      popularity: 54.910076,
      id: 44217,
      backdrop_path: '/A30ZqEoDbchvE7mCZcSp6TEwB1Q.jpg',
      vote_average: 6.88,
      overview:
        "Vikings follows the adventures of Ragnar Lothbrok, the greatest hero of his age. The series tells the sagas of Ragnar's band of Viking brothers and his family, as he rises to become King of the Viking tribes. As well as being a fearless warrior, Ragnar embodies the Norse traditions of devotion to the gods. Legend has it that he was a direct descendant of Odin, the god of war and warriors.",
      first_air_date: '2013-03-03',
      origin_country: ['IE', 'CA'],
      genre_ids: [18, 10759],
      original_language: 'en',
      vote_count: 399,
      name: 'Vikings',
      original_name: 'Vikings',
    },
    {
      poster_path: '/vHXZGe5tz4fcrqki9ZANkJISVKg.jpg',
      popularity: 35.357012,
      id: 19885,
      backdrop_path: '/bvS50jBZXtglmLu72EAt5KgJBrL.jpg',
      vote_average: 7.79,
      overview: 'A modern update finds the famous sleuth and his doctor partner solving crime in 21st century London.',
      first_air_date: '2010-07-25',
      origin_country: ['GB'],
      genre_ids: [80, 18, 9648],
      original_language: 'en',
      vote_count: 381,
      name: 'Sherlock',
      original_name: 'Sherlock',
    },
    {
      poster_path: '/igDhbYQTvact1SbNDbzoeiFBGda.jpg',
      popularity: 30.675316,
      id: 57243,
      backdrop_path: '/cVWsigSx97cTw1QfYFFsCMcR4bp.jpg',
      vote_average: 6.83,
      overview:
        "The Doctor looks and seems human. He's handsome, witty, and could be mistaken for just another man in the street. But he is a Time Lord: a 900 year old alien with 2 hearts, part of a gifted civilization who mastered time travel. The Doctor saves planets for a living – more of a hobby actually, and he's very, very good at it. He's saved us from alien menaces and evil from before time began – but just who is he?",
      first_air_date: '2005-03-26',
      origin_country: ['GB'],
      genre_ids: [10759, 18, 10765],
      original_language: 'en',
      vote_count: 339,
      name: 'Doctor Who',
      original_name: 'Doctor Who',
    },
  ],
  showDetail: serie => console.log('show detail ' + JSON.stringify(serie)),
  nextPage: 1,
  hasMoreResult: true,
}
export default SerieListView

The listView's onEndReached event will later let us automatically load additional results. We will come back to this part in the third article of this series.

That's it: we have created all the components needed for the app.

Series navigation

  1. The Movie DB / React Native Part 1: Components
  2. The Movie DB / React Native Part 2: Navigation
  3. The Movie DB / React Native Part 3: Data