Adding features

This is the last chapter of this book, the only one not requiring to add and setup other dependencies, but rather to use some of Relay and React Native features to create a minimum product.

The first thing we're going to do it to add pagination to the HomeScreen, in order to load more than 10 repositories. To achieve this, we'll use the PaginationContainer provided by Relay.

Here is the updated HomeScreen.js code:

src/components/HomeScreen.js
// @flow

import React, { Component } from 'react'
import { ScrollView, StyleSheet, View } from 'react-native'
import { Button, Icon, List, ListItem, Text } from 'react-native-elements'
import {
  createFragmentContainer,
  createPaginationContainer,
  graphql,
} from 'react-relay'

import ScreenRenderer from './ScreenRenderer'
import sharedStyles from './styles'

import type { HomeScreen_repository as Repository } from './__generated__/HomeScreen_repository.graphql'
import type { HomeScreen_viewer as Viewer } from './__generated__/HomeScreen_viewer.graphql'

type Variables = { [name: string]: any }
type Disposable = {
  dispose(): void,
}

const PAGE_SIZE = 20

type RepositoryItemProps = {
  navigation: Object,
  repository: Repository,
}

const RepositoryItem = ({ navigation, repository }: RepositoryItemProps) => (
  <ListItem
    containerStyle={styles.itemContainer}
    title={repository.name}
    subtitle={repository.owner.isViewer ? null : repository.owner.login}
    rightIcon={{ name: 'chevron-right', type: 'octicon' }}
    onPress={() => {
      navigation.navigate('Repository', {
        id: repository.id,
        name: repository.nameWithOwner,
      })
    }}
  />
)

const RepositoryItemContainer = createFragmentContainer(RepositoryItem, {
  repository: graphql`
    fragment HomeScreen_repository on Repository {
      id
      name
      nameWithOwner
      owner {
        login
        ... on User {
          isViewer
        }
      }
    }
  `,
})

type HomeProps = {
  navigation: Object,
  relay: {
    hasMore: () => boolean,
    isLoading: () => boolean,
    loadMore: (pageSize: number, cb?: Function) => ?Disposable,
  },
  viewer: Viewer,
}
type HomeState = {
  loading: boolean,
}

class Home extends Component<HomeProps, HomeState> {
  state = {
    loading: false,
  }

  onPressLoadMore = () => {
    if (this.props.relay.hasMore() && !this.state.loading) {
      this.setState({ loading: true })
      this.props.relay.loadMore(PAGE_SIZE, () => {
        this.setState({ loading: false })
      })
    }
  }

  render() {
    const { relay, viewer } = this.props

    if (
      !viewer.repositories ||
      !viewer.repositories.edges ||
      viewer.repositories.edges.length === 0
    ) {
      return (
        <View style={sharedStyles.scene}>
          <View style={sharedStyles.mainContents}>
            <Text h3>No repository!</Text>
          </View>
        </View>
      )
    }

    const rows = viewer.repositories.edges.map(edge => {
      return edge && edge.node ? (
        <RepositoryItemContainer
          key={edge.node.id}
          navigation={this.props.navigation}
          repository={edge.node}
        />
      ) : null
    })

    let footer = null
    if (this.state.loading) {
      footer = (
        <View style={styles.buttonContainer}>
          <Button disabled title="Loading..." />
        </View>
      )
    } else if (relay.hasMore()) {
      footer = (
        <View style={styles.buttonContainer}>
          <Button onPress={this.onPressLoadMore} title="Load more" />
        </View>
      )
    }

    return (
      <ScrollView>
        <List>
          {rows}
          {footer}
        </List>
      </ScrollView>
    )
  }
}

const HomeScreenQuery = graphql`
  query HomeScreenQuery($count: Int!, $cursor: String) {
    viewer {
      ...HomeScreen_viewer
    }
  }
`

const HomeContainer = createPaginationContainer(
  Home,
  {
    viewer: graphql`
      fragment HomeScreen_viewer on User {
        repositories(
          after: $cursor
          first: $count
          orderBy: { field: UPDATED_AT, direction: DESC }
        ) @connection(key: "HomeScreen_repositories") {
          edges {
            node {
              ...HomeScreen_repository
              id
            }
          }
          pageInfo {
            endCursor
            hasNextPage
          }
        }
      }
    `,
  },
  {
    getVariables: (props: HomeProps, paginationVariables: Variables) =>
      paginationVariables,
    query: HomeScreenQuery,
  },
)

export default class HomeScreen extends Component<{
  navigation: Object,
}> {
  static navigationOptions = {
    headerLeft: (
      <View style={sharedStyles.headerLeft}>
        <Icon
          name="repo"
          type="octicon"
          color="white"
          style={sharedStyles.headerIcon}
        />
      </View>
    ),
    title: 'Repositories',
  }

  render() {
    return (
      <ScreenRenderer
        container={HomeContainer}
        navigation={this.props.navigation}
        query={HomeScreenQuery}
        variables={{
          count: PAGE_SIZE,
        }}
      />
    )
  }
}

const styles = StyleSheet.create({
  buttonContainer: {
    marginVertical: 15,
  },
  itemContainer: {
    backgroundColor: 'white',
  },
})

The main change is the introduction of Relay's PaginationContainer, that will be used to paginate over the list of repositories. It provides the functions hasMore(), isLoading() an loadMore() in the relay prop, that are used in this updated Home component to implement the wanted behavior in the onPressLoadMore() method and the footer logic.

We are also going to update the RepositoryScreen.js file, with the following contents:

Beyond displaying more information, the main change in this module is the introduction of two Relay mutations: addStar used in the AddStarMutation fragment, and removeStar used in the RemoveStartMutation fragment.

Both of these mutations are used conditionally in the onPressStar() method in order to toggle starring the repository for the user, using Relay's commitMutation(). By implementing an optimistic response, we can ensure the UI is updated immediately by Relay using the payload provided.

That's it for this last chapter! This is still far from a full-feature app, but hopefully a good starting point to implement more of GitHub's APIs as you want!

Don't hesitate to check the additional resources page to discover complementary libraries to build this kind of project.

Last updated