Introduction

Working with GraphQL in React Native is similar in many ways to other backend technologies. We just need to connect our mobile app’s GraphQL client to our GraphQL back-end written in Elixir, JavaScript or any other language.

For this app, we will be using TypeScript rather than pure JavaScript. We already have our React Native app bootstrapped with TypeScript. It's a pure react-native app with some conventions for directory structure.

Our code lives in the src directory. In our TypeScript configuration, the builds are going to the build directory.

Our components live in the Components directory. Each component will have its own directory containing the styles, the test file for this component, and the component itself.

The screens and containers will be in the Screens directory. This is where we connect with GraphQL and send the properties to our components.

GraphQL configuration and navigation will have their own directories as well.

./
├── android
├── app.json
├── babel.config.js
├── build
├── index.js
├── ios
├── jest.config.js
├── node_modules
├── package.json
├── rn-cli.config.js
├── src
│   ├── App.tsx
│   ├── Components
│   ├── Config
│   ├── GraphQL
│   ├── Navigation
│   └── Screens
├── tsconfig.jest.json
├── tsconfig.json
├── yarn-error.log
└── yarn.lock

Here you can see a quick overview of our application’s structure on disk.

We have two screens already already implemented with no logic in them: the Login screen and the categories screen. If we press the login button, we navigate to the categories screen.

Navigation

We are using React Navigation and we built a NavigatorService module. This helps us initialize the navigation, and then we can use the Navigator Service to navigate between the screens. This is basically a wrapper for the navigation functions that react-navigation provides.

import { NavigationActions, StackActions, NavigationNavigateAction, NavigationComponent } from 'react-navigation'
import { ReactNode } from 'react'

type ConfigType = {
  navigator: React.Ref<ReactNode> | NavigationComponent
  dispatch: () => void
}

let config: ConfigType = { navigator: null, dispatch: () => {} }

export function setNavigator (nav: React.Ref<ReactNode>): void {
  if (nav) {
    config.navigator = nav
  }
}

export function dispatch (action: NavigationNavigateAction) {
  if (config.navigator) {
    config.navigator.dispatch(action)
  }
}

export function navigate (routeName: string, params?: object) {
  if (config.navigator && routeName) {
    let action = NavigationActions.navigate({ routeName, params })
    config.navigator.dispatch(action)
  }
}

export function reset (routeName: string) {
  if (config.navigator && routeName) {
    const action = StackActions.reset({
      index: 0,
      actions: [ NavigationActions.navigate({ routeName }) ]
    })

    config.navigator.dispatch(action)
  }
}

export function goBack (params = {}) {
  if (config.navigator) {
    let action = NavigationActions.back(params)
    config.navigator.dispatch(action)
  }
}

We are setting the navigator in the RootContainer.

import React, { PureComponent } from 'react'
import AppNavigation from './Navigation/AppNavigation'
import * as NavigatorService from './Navigation/NavigatorService'
import { NavigationComponent } from 'react-navigation'
import './Config/ReactotronConfig'

export default class RootContainer extends PureComponent {
  render () {
    return (<AppNavigation
      ref={(nav: NavigationComponent) => {
        NavigatorService.setNavigator(nav)
      }}
    />)
  }
}

So far, we have just two screens: the Login screen and the Categories screen. Let's look at them.

In the Login Screen, we are just calling the LoginForm component.

In the CategoriesScreen, we have a FlatList that will render the categories.

import React from 'react'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import * as NavigatorService from '../Navigation/NavigatorService'
import { Icon, ListItem, Text } from 'react-native-elements'
import { AsyncStorage, FlatList, View } from 'react-native'

type Props = { navigation: any }

const categories = [
  { id: '1', title: 'My Category 1' },
  { id: '2', title: 'My Category 2' },
  { id: '3', title: 'My Category 3' },
  { id: '4', title: 'My Category 4' }
]

export default class CategoriesScreen extends React.Component<Props> {
  state = {
    refreshing: false
  }

  onPress = (categoryId, categoryName) => {
    return NavigatorService.navigate('Threads', { categoryId, categoryName })
  }

  renderItem = ({ item }) => {
    return <ListItem titleStyle={{ fontSize: 18 }} onPress={() => this.onPress(item.id, item.title)} key={item.id} title={item.title} />
  }

  render () {
    return (
      <Screen>
        <Header
          text='Categories'
          rightComponent={
            <Icon
              onPress={async () => {
                await AsyncStorage.clear()
                NavigatorService.reset('Login')
              }}
              name='exit-to-app'
              color='#fff'
            />
          }
        />
        <FlatList
          data={categories}
          keyExtractor={item => item.id}
          renderItem={this.renderItem}
        />
      </Screen>
    )
  }
}

Here, we can see how to navigate between the two screens and what our project looks like in general.

Our Screen component

We also added a Screen component to make our lives easier. It wraps each of our screens.

We created this Screen component, because it implements the back button behaviour for Android. This is a common problem in lots of React Native apps where people forget to implement this, and when people use the native back button on Android phones in that situation it closes the app.

import React from 'react'
import { BackHandler } from 'react-native'
import * as NavigatorService from '../Navigation/NavigatorService'
import { withNavigation } from 'react-navigation'

class Screen extends React.Component {
  constructor (props) {
    super(props)
  }

  onBackButtonPressAndroid = () => {
    this.props.navigation.goBack()
    return true
  }

  componentDidMount () {
    BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
  }

  componentWillUnmount () {
    BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
  }

  render () {
    return this.props.children
  }
}

export default Screen

Our categories screen presently has a hardcoded list of categories. Let’s connect to the API to fetch a list of categories instead.

Creating our GraphQL client

Generating our types

One of the major benefits of GraphQL is its strong typing. We can create TypeScript types to help work with the GraphQL results. We can use GraphQL code generator to generate these TypeScript types for us.

We already have a script in our package.json that does exactly that:

"generate-types": "./node_modules/.bin/gql-gen --schema=http://localhost:4000 --template graphql-codegen-typescript-template --out=./src/GraphQL/",

It creates the types file inside our GraphQL directory and we can just use it. If we ever change types, we can regenerate these files and TypeScript will help us identify places in our code that need to change.

Adding necessary libraries

Let's create our client. We will start by adding some libraries we will use.

yarn add apollo-client
yarn add apollo-link
yarn add apollo-link-state
yarn add apollo-link-error
yarn add apollo-cache-inmemory
yarn add apollo-link-http

Set the token

We will create a file to hold our client. We will need to update the client with the token, which we get back after authorizing with the backend. We will have a variable called CONTEXT, and a method called setToken. Once we get the token, we will call this method and update the token. This token will be used in the authorization header.

let CONTEXT: ContextType = {}

export const setToken = (token: String) => {
  CONTEXT.token = token
}

HTTP Links

Our Apollo Client will have a link for each of the HTTP connection, the web socket connection, and the cache. We will use ApolloLink.split to make sure we are using the links for the subscriptions, http link and web socket link.

We will have two websocket connections that are used for our absinthe connection and our javascript Apollo Server. They each behave slightly differently, so this is a situation where we can’t be backend-agnostic.

Let's create our AbsintheSocketLink. We are going to use @absinthe/socket, @absinthe/socket-apollo-link and phoenix.

import * as AbsintheSocket from '@absinthe/socket'
import { createAbsintheSocketLink } from '@absinthe/socket-apollo-link'
import { Socket as PhoenixSocket } from 'phoenix'

export default createAbsintheSocketLink(AbsintheSocket.create(
    new PhoenixSocket('ws://localhost:4000/socket')
))

We can reuse it in our client, we will have an environment variable called BACKEND, that we will use to check if we want to use the Elixir back-end or the JavaScript back-end.

const javaScriptWebsocketLink = new WebSocketLink({
  uri: Config.WEBSOCKET_API_URL,
  options: {
    reconnect: true
  }
})

const httpLink = new HttpLink({
  uri: Config.GRAPHQL_API_URL,
  credentials: 'same-origin'
})

const subscriptionsLink = Config.BACKEND === 'js' ? javaScriptWebsocketLink : absintheSocketLink

const link = ApolloLink.split(
  operation => hasSubscription(operation.query),
  subscriptionsLink,
  middlewareLink.concat(httpLink)
)

Once we have the link, we are going to use the ApolloLink.from and pass a list of Apollo Links (the http link and the web socket link), and the cache. We are going to use state link (apollo link state), error link to help us handling the errors, and the link itself that we saw above.

const client = new ApolloClient({
  link: ApolloLink.from([ errorLink, link ]),
  cache
})

Let's see how to set up each of these.

Error link

Apollo Link error is executed when some Apollo error happens. We could use this to report errors back to an error monitoring service, for instance.

import { onError } from 'apollo-link-error'
...

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors)
    graphQLErrors.map(({ message, locations, path }) =>
      console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`)
    )
  if (networkError) console.log(`[Network error]: ${networkError}`)
})

Now that we have our client, we can just import it at the root of our app. In our RootContainer:

import React, { PureComponent, Ref, ReactElement, ReactNode } from 'react'
import AppNavigation from './Navigation/AppNavigation'
import { ApolloProvider } from 'react-apollo'
import client from './GraphQL/client'
import { Text } from 'react-native-elements'
import * as NavigatorService from './Navigation/NavigatorService'
import { NavigationComponent } from 'react-navigation'
import './Config/ReactotronConfig'

export default class RootContainer extends PureComponent {
  render () {
    if (client) {
      return (
        <ApolloProvider client={client}>
          <AppNavigation
            ref={(nav: NavigationComponent) => {
              NavigatorService.setNavigator(nav)
            }}
          />
        </ApolloProvider>
      )
    } else {
      return <Text>Loading...</Text>
    }
  }
}

Our Query component

Before going further in our implementation, I'd like to mention something we also did to help the loading screens. Apollo has a loading property and we can normally use it to handle loading screens. People normally create a higher order component to render the loading state too.

What we did was: we created a Query component that handles the loading state for us, which uses the Query component from Apollo. In our screens we will be using this Query component, so we can handle the loading state in just one place.

import React, { ReactNode } from 'react'
import { Text } from 'react-native'
import { Query, QueryResult } from 'react-apollo'

type PropsLoading = {
  query: any
  variables?: any
  children: (x: QueryResult) => ReactNode
}

export default class MyQuery extends React.PureComponent<PropsLoading> {
  render () {
    const { children, query, ...queryProps } = this.props
    return (
      <Query query={query} {...queryProps}>
        {response => {
          if (response.loading) {
            return <Text>Loading...</Text>
          } else {
            return children(response)
          }
        }}
      </Query>
    )
  }
}

Let's start implementing our screens, starting with the Login screen.

Login

On the LoginScreen, we need to use the authenticate mutation to authenticate the user. Let's wrap the Mutation component in our LoginForm.

Let's create our mutation to authenticate. It receives email and password as required fields.

export const authenticate = gql`
  mutation authenticate($email: String!, $password: String!) {
    authenticate(email: $email, password: $password)
  }
`

Now we can use it on the Login Screen.

import React, { Fragment } from 'react'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import { FormLabel, FormInput, FormValidationMessage, Button, Text } from 'react-native-elements'
import { ScrollView, View, Alert, AsyncStorage } from 'react-native'
import { Mutation } from 'react-apollo'
import * as Mutations from '../GraphQL/mutations'
import * as Queries from '../GraphQL/queries'
import * as NavigatorService from '../Navigation/NavigatorService'
import LoginForm from '../Components/LoginForm/LoginForm'
import { setToken } from '../GraphQL/client'

type Props = {}

export default class LoginScreen extends React.Component<Props> {
  state = {
    email: '',
    password: ''
  }

  async componentWillMount () {
    const token = await AsyncStorage.getItem('token')
    if (token) {
      setToken(token)
      NavigatorService.navigate('Categories')
    }
  }

  render () {
    const { email, password } = this.state
    return (
      <Screen>
        <Mutation
          mutation={Mutations.authenticate}
          variables={{ email, password }}
          onCompleted={response => {
            const token = response.authenticate
            AsyncStorage.setItem('token', token)
            setToken(token)
            NavigatorService.navigate('Categories')
          }}
          refetchQueries={[ { query: Queries.getCategories } ]}
          onError={error => {
            Alert.alert(error.message)
          }}
        >
          {authenticate => {
            return (
              <LoginForm
                email={email}
                password={password}
                setEmail={email => this.setState({ email })}
                setPassword={password => this.setState({ password })}
                authenticate={authenticate}
              />
            )
          }}
        </Mutation>
      </Screen>
    )
  }
}

Once the mutation completes, we set the token we just get back to AsyncStorage. After doing this, we go to the Categories Screen. We also do a refetch query for getting the categories. If we have received any errors, we are for now showing an Alert with the error message.

Paginating categories

We will paginate categories, so we are going to send page and perPage with the query.

export const getCategories = gql`
  query allCategories($pagination: Pagination) {
    categories(pagination: $pagination) {
      page
      perPage
      entries {
        id
        title
        slug
        insertedAt
      }
    }
  }
`

We use this query in our component. Remember, we are not using the Query component from Apollo; we are using our own Query component that handles the loading state.

In our Header component, on the right side, we will have a logout button. Once the user presses the button, it should reset the stack navigation, going back to the Login and clear the AsyncStorage.

We need to first remove the hardcoded data and wrap the current FlatList into a Query component. We’ll use the results of our query in our FlatList.

Using FlatList it’s really simple to manage the pagination.

On the onPress function for each row, we navigate to the threads screen passing the categoryId on the navigate state params.

We will create a loadMore function that receives the fetchMore from Apollo. We pass the current page to this function. We are also going to implement PullToRefresh on the FlatList by implementing the onRefresh method that will use our refetch attribute from the query. refreshing is just a bit of state.

              <FlatList
                onEndReached={this.loadMore(fetchMore, categories.page)}
                onEndReachedThreshold={0.8}
                refreshing={this.state.refreshing}
                onRefresh={refetch}
                data={categories.entries}
                keyExtractor={item => item.id}
                renderItem={this.renderItem}
              />

In the loadMore function we call fetchMore and it has the variables we need to pass to the query--in this case, the page number. In the updateQuery function we can merge the previous result and the current one. Let's take a look at our merge function.

  loadMore = (fetchMore, page) => () => {
    fetchMore({
      variables: { pagination: { page: page + 1, perPage: 5 } },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev;

        if (prev && prev.categories && prev.categories.entries) {
          return this.merge(fetchMoreResult, prev.categories.entries)
        }
      }
    })
  }

In the merge function we are merging the API result with the existing entries. To avoid duplicate entries, we perform some validation in our prepare function. This function basically sorts by insertedAt and ensures we don’t have any duplicates by id.

  merge = (result, oldEntries) => ({
    ...result,
    categories: {
      ...result.categories,
      entries: prepare([...oldEntries, ...result.categories.entries])
    }
  })

The prepare function uses some lodash functions.

import sortBy from 'lodash/sortBy'
import uniqBy from 'lodash/uniqBy'
import reverse from 'lodash/reverse'
import PrettierLog from 'reactotron-prettier-log'

export const prepare = (records, sortByList = [ 'insertedAt', 'id' ], reverseRecords = true) => {
  PrettierLog.log('records', { records })
  const result = sortBy(uniqBy(records, 'id'), sortByList)
  if (!reverseRecords) return result
  return reverse(result)
}

This should fetch new pages of categories as we scroll the FlatList. We can also pull to refresh to make it refetch the query.

import React from 'react'
import * as Queries from '../GraphQL/queries'
import Query from '../GraphQL/Query'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import * as NavigatorService from '../Navigation/NavigatorService'
import { Icon, ListItem, Text } from 'react-native-elements'
import { AsyncStorage, FlatList, View } from 'react-native'
import { prepare } from '../Helpers/RecordsHelper'

type Props = { navigation: any }

export default class CategoriesScreen extends React.Component<Props> {
  state = {
    refreshing: false
  }

  merge = (result, oldEntries) => ({
    ...result,
    categories: {
      ...result.categories,
      entries: prepare([...oldEntries, ...result.categories.entries])
    }
  })

  loadMore = (fetchMore, page) => () => {
    fetchMore({
      variables: { pagination: { page: page + 1, perPage: 5 } },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev

        if (prev && prev.categories && prev.categories.entries) {
          return this.merge(fetchMoreResult, prev.categories.entries)
        }
      }
    })
  }

  onPress = (categoryId, categoryName) => {
    return NavigatorService.navigate('Threads', { categoryId, categoryName })
  }

  renderItem = ({ item }) => {
    return <ListItem titleStyle={{ fontSize: 18 }} onPress={() => this.onPress(item.id, item.title)} key={item.id} title={item.title} />
  }

  render () {
    return (
      <Screen>
        <Header
          text='Categories'
          rightComponent={
            <Icon
              onPress={async () => {
                await AsyncStorage.clear()
                NavigatorService.reset('Login')
              }}
              name='exit-to-app'
              color='#fff'
            />
          }
        />
        <Query query={Queries.getCategories} variables={{ pagination: { page: 0, perPage: 5 } }}>
          {({ data, fetchMore, refetch }) => {
            let categories = get(data, 'categories', { entries: [], page: 0 })
            return (
              <FlatList
                onEndReached={this.loadMore(fetchMore, categories.page)}
                onEndReachedThreshold={0.8}
                refreshing={this.state.refreshing}
                onRefresh={refetch}
                data={categories.entries}
                keyExtractor={item => item.id}
                renderItem={this.renderItem}
              />
            )
          }}
        </Query>
      </Screen>
    )
  }
}

Listing Threads

We will start by copying the CategoriesScreen we have just created to get the general structure of it. We also need to add the Threads entry into our Navigation routes. Let's remove the loadMore function and other functions, because we are not going to paginate the threads.

We are going to start by creating our snapshot test for our component.

import React from 'react'
import ListThreads from './ListThreads'
import renderer from 'react-test-renderer'
import { Thread } from '../../GraphQL/types'

describe('ListThread component test', () => {
  it('should render', () => {
    const tree = renderer.create(<ListThreads refetch={() => null} threads={new Array<Thread>()} / >).toJSON()
    expect(tree).toMatchSnapshot()
  })
})

We need to have the types from our backend. Let's run the command to generate the types.

yarn generate-types

It generates the types in the file /GraphQL/types. With this, we can use the type Thread.

Let's now create our component.

We will have a component that will handle the FlatList for the threads, this component will be called ListThreads. It will receive the refetch function and a list of threads. We are implementing PullToRefresh on the FlatList by using the two attributes: onRefresh and refreshing. The onRefresh function should call a function that makes the refresh, in this case the refetch function we have from our Query. refreshing is just some state that tracks whether we are refreshing or not.

import React from 'react'
import { Text, View, FlatList } from 'react-native'
import styles from './ListThreadsStyle'
import { ListItem } from 'react-native-elements'
import { Thread } from '../../GraphQL/types'
import * as NavigatorService from '../../Navigation/NavigatorService'

type Props = {
  refetch: () => void
  threads: Array<Thread>
}

export default class ListThreads extends React.PureComponent<Props> {
  state = {
    refreshing: false
  }

  renderThread = ({ item }) => {
    const thread = item
    return <ListItem
      titleStyle={{ fontSize: 18 }}
      onPress={() => NavigatorService.navigate('Posts', { threadId: thread.id, threadTitle: thread.title })}
      title={thread.title}
    />
  }
  render () {
    const { threads, refetch } = this.props
    return (
      <View style={{ flex: 1 }}>
        <FlatList
          data={threads}
          onRefresh={refetch}
          refreshing={this.state.refreshing}
          renderItem={this.renderThread}
          keyExtractor={item => item.id} />
      </View>
    )
  }
}

Coming back to our ThreadsScreen, on the header in the right button, we will go to a screen to create Threads. We will pass the category id and the refetch function. We will need the refetch function on the next screen because when we get back from there, we will need to refetch everything here.

In the screen itself, it will call the component ListThreads we created, by passing the refetch function and the threads.

We also need to create our query that will get the threads by category.

export const getThreadsByCategory = gql`
  query threadsByCategory($categoryId: ID!){
    category(id: $categoryId){
      threads {
        id
        title
        slug
        insertedAt
      }
    }
  }
`
import React from 'react'
import ListThreads from '../Components/ListThreads/ListThreads'
import * as Queries from '../GraphQL/queries'
import Query from '../GraphQL/Query'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import * as NavigatorService from '../Navigation/NavigatorService'
import { Icon } from 'react-native-elements'

type Props = {}

export default class App extends React.Component<Props> {
  render () {
    const categoryId = get(this.props, 'navigation.state.params.categoryId')
    const categoryName = get(this.props, 'navigation.state.params.categoryName')

    return (
      <Query query={Queries.getThreadsByCategory} variables={{ categoryId }}>
        {({ data, refetch }) => {
          const threads = get(data, 'threadsByCategory', [])
          return (
            <Screen>
              <Header
                text={`Threads - ${categoryName}`}
                allowGoBack
                rightComponent={
                  <Icon
                    onPress={() =>
                      NavigatorService.navigate('CreateThread', {
                        categoryId,
                        refetch
                      })}
                    name='add'
                    color='#fff'
                  />
                }
              />
            <ListThreads
                    refetch={refetch}
                    threads={threads}
                  />
            </Screen>
          )
        }}
      </Query>
    )
  }
}

This will be the implementation of our screen to list the threads. We list the threads, we can go back to the previous screen and we have a button to create a thread. Let's implement the create thread screen.

Creating Threads

Our create thread screen will be simple. We will have our mutation to create Thread. When we create a thread, we are going ahead and creating the first post of this thread.

Our mutation that creates threads receives the title, categoryId, and the body of the first post in our thread.

Mutation to create a thread

export const createThread = gql`
  mutation createThread($title: String!, $categoryId: ID!, $body: String!) {
    createThread(title: $title, categoryId: $categoryId, body: $body) {
      id
      title
    }
  }
`

And now we can pass this into a Mutation component to create a thread based on our form inputs.

On the createThread screen we are going to have a preview for the markdown of the post.

Implementing the tabs for creating Threads

We are going to implement the screen by using react-native-tab-view.

We will have two scenes. One scene to create the thread that will be the function renderCreateFragment, the other one just to preview that will be the component PostPreview.

       renderScene={({ route, focused }) => {
                switch (route.key) {
                  case 'create':
                    return this.renderCreateFragment({
                      createThread, title, body,
                      setBody: (body) => this.setState({ body }),
                      setTitle: (title) => this.setState({ title })
                    })
                  case 'preview':
                    return <PostPreview body={body} />
                  default:
                    return null
                }
              }}

In our state, we need to track the two screens we have and the indexes for them.

We will have a function called renderCreateFragment that receives everything it needs for creating the thread. And we will have an AddButton component that we created that already has the right color and a + icon on the button.

 renderCreateFragment = ({ createThread, title, body, setBody, setTitle }) => {
    return (
      <View>
        <FormLabel>Thread Name</FormLabel>
        <FormInput onChangeText={title => setTitle(title)} value={title} />
        <FormLabel>Your Post</FormLabel>
        <FormInput
          multiline
          containerStyle={{ minHeight: 100 }}
          value={body}
          onChangeText={body => setBody(body)} />
        <AddButton
          text='Create Thread'
          style={{ marginTop: 20 }}
          onPress={() => createThread()}
          large
        />
      </View>
    )
  }

Add Button

Let's implement our AddButton. It will receive text as the button text, style as the styling for the component, large to specify if the component is large or not, and an onPress function to be executed when the button is pressed.

import React from 'react'
import { ViewStyle, StyleProp } from 'react-native'
import { Button } from 'react-native-elements'
import Colors from '../../Config/Colors'

type Props = { text: string; large?: boolean, onPress: () => void, style: StyleProp<ViewStyle> }

export default class AddButton extends React.PureComponent<Props> {
  render () {
    const { text, style, large, onPress } = this.props
    return (
      <Button
        style={style}
        large={large}
        onPress={onPress}
        backgroundColor={Colors.action}
        icon={{ name: 'plus', type: 'font-awesome' }}
        title={text} />
    )
  }
}

Post Preview component

Our PostPreview component will just render the markdown that we pass to it wrapped by a ScrollView. Pretty simple. We are using react-native-simple-markdown to render the markdown.

import React from 'react'
import { ScrollView } from 'react-native'
import Markdown from 'react-native-simple-markdown'

type Props = {body: string}

export default class Home extends React.PureComponent<Props> {
  render () {
    const { body } = this.props
    return (<ScrollView style={{ flex: 1 }}>
      <Markdown>
        {body}
      </Markdown>
    </ScrollView>)
  }
}
import React, { Fragment } from 'react'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import { FormLabel, FormInput } from 'react-native-elements'
import { Mutation } from 'react-apollo'
import * as Mutations from '../GraphQL/mutations'
import * as NavigatorService from '../Navigation/NavigatorService'
import Colors from '../Config/Colors'
import AddButton from '../Components/AddButton/AddButton'
import { TabView, TabBar } from 'react-native-tab-view'
import { View } from 'react-native'
import PrettierLog from 'reactotron-prettier-log'
import PostPreview from '../Components/PostPreview/PostPreview'

type Props = {}

export default class CreatePostScreen extends React.Component<Props> {
  state = {
    title: '',
    body: '',
    index: 0,
    routes: [
      { key: 'create', title: 'Create' },
      { key: 'preview', title: 'Preview' }
    ]
  }

  renderCreateFragment = ({ createThread, title, body, setBody, setTitle }) => {
    return (
      <View>
        <FormLabel>Thread Name</FormLabel>
        <FormInput onChangeText={title => setTitle(title)} value={title} />
        <FormLabel>Your Post</FormLabel>
        <FormInput
          multiline
          containerStyle={{ minHeight: 100 }}
          value={body}
          onChangeText={body => setBody(body)} />
        <AddButton
          text='Create Thread'
          style={{ marginTop: 20 }}
          onPress={() => createThread()}
          large
        />
      </View>
    )
  }

  render () {
    const categoryId = get(this.props, 'navigation.state.params.categoryId')
    const refetch = get(this.props, 'navigation.state.params.refetch')
    const { title, body } = this.state
    return (
      <Screen>
        <Header text='Create a Thread' allowGoBack />
        <Mutation
          mutation={Mutations.createThread}
          variables={{ categoryId, title: this.state.title, body: this.state.body }}
          onCompleted={(data) => {
            PrettierLog.log('data for on completed', { data })
            const thread = get(data, 'createThread', {})
            refetch().then(() => NavigatorService.navigate('Posts', { threadId: thread.id, threadName: thread.title }))
          }}
        >
          {createThread => {
            return (<TabView
              style={{ marginTop: -5 }}
              navigationState={this.state}
              renderTabBar={props =>
                <TabBar
                  {...props}
                  style={{ backgroundColor: Colors.secondary }}
                  pressColor='blue'
                  indicatorStyle={{ height: 2, backgroundColor: 'white' }}
                />
              }
              renderScene={({ route, focused }) => {
                switch (route.key) {
                  case 'create':
                    return this.renderCreateFragment({
                      createThread, title, body,
                      setBody: (body) => this.setState({ body }),
                      setTitle: (title) => this.setState({ title })
                    })
                  case 'preview':
                    return <PostPreview body={body} />
                  default:
                    return null
                }
              }}
              onIndexChange={index => this.setState({ index })}
            />)
          }}
        </Mutation>
      </Screen>
    )
  }
}

This is how we create a thread and the first post of this thread. Once we create a thread, we will redirect to the list of posts. Let's create the list of posts.

Listing Posts

Let's create a general structure for our screen.

Now, let's create a snapshot test for our ListPosts component.

import React from 'react'
import ListPosts from './ListPosts'
import renderer from 'react-test-renderer'
import { Post } from '../../GraphQL/types'

describe('ListPost component test', () => {
  it('should render', () => {
    const tree = renderer.create(<ListPosts refetch={() => null} posts={new Array<Post>()} / >).toJSON()
    expect(tree).toMatchSnapshot()
  })
})

To list the posts, we will have this component we created called ListPosts that receives the posts and renders them.

 <FlatList
          data={posts}
          onRefresh={refetch}
          refreshing={this.state.refreshing}
          renderItem={this.renderPost}
          keyExtractor={item => item.id} />

This will be FlatList where we will also implement the PullToRefresh bits by having the refetch property for the onRefetch and the refreshing in our local state.

<Card containerStyle={{ backgroundColor: Colors.cardBackground }}>
            <View style={styles.generalContainer}>
              <View style={styles.containerBody}>
                <View style={styles.width100Percent}>
                  <Text style={styles.username}>{post.user.name}</Text>
                  <Text style={styles.dateText}>{new Date(post.insertedAt).toLocaleString()}</Text>
                </View>
                <View style={styles.markdownContainer}>
                  <Markdown>{post.body}</Markdown>
                </View>
              </View>
            </View>
          </Card>

The item itself will be wrapped by a Card where we will show the user avatar and name, the post creation time, and the rendered markdown.

import React from 'react'
import { Text, View, ScrollView, FlatList } from 'react-native'
import styles from './ListPostsStyle'
import { Card, Avatar, Divider } from 'react-native-elements'
import { Post } from '../../GraphQL/types'
import * as AvatarHelper from '../../Helpers/AvatarHelper'
import Markdown from 'react-native-simple-markdown'
import Colors from '../../Config/Colors'

type Props = {
  posts: Array<Post>
  refetch: () => void
}

export default class ListPosts extends React.PureComponent<Props> {
  state = {
    refreshing: false
  }

  renderPost = ({ item }) => {
    const post = item

    return (
      <View style={{ flexDirection: 'row', flex: 1, paddingHorizontal: 5 }}>
        <View style={styles.containerAvatar}>
          <Avatar rounded source={{ uri: AvatarHelper.getUrl(post.user) }} activeOpacity={0.7} />
        </View>
        <View style={{ flex: 1 }}>
          <Card containerStyle={{ backgroundColor: Colors.cardBackground }}>
            <View style={styles.generalContainer}>
              <View style={styles.containerBody}>
                <View style={styles.width100Percent}>
                  <Text style={styles.username}>{post.user.name}</Text>
                  <Text style={styles.dateText}>{new Date(post.insertedAt).toLocaleString()}</Text>
                </View>
                <View style={styles.markdownContainer}>
                  <Markdown>{post.body}</Markdown>
                </View>
              </View>
            </View>
          </Card>
        </View>
      </View>
    )
  }
  render () {
    const { posts, refetch } = this.props
    return (
      <View style={{ flex: 1 }}>
        <FlatList
          data={posts}
          onRefresh={refetch}
          refreshing={this.state.refreshing}
          renderItem={this.renderPost}
          keyExtractor={item => item.id} />
      </View>
    )
  }
}

We can use this component in our screen to List Posts. The screen to list posts will just list all the posts by getting them using the getPostsByThread query, which takes the thread id.

export const getPostsByThread = gql`
  query getPostsByThread($threadId: ID!) {
    thread(id: $threadId) {
      posts {
        id
        body
        insertedAt
        user {
          id
          name
          username
        }
      }
    }
  }
`

On the right side of the header, we will have a button that navigates to the CreatePost screen. As we did on the CreateThread screen, we are passing the refetch function to help us refetch the posts once we leave this screen.

import React from 'react'
import * as Queries from '../GraphQL/queries'
import Query from '../GraphQL/Query'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import * as NavigatorService from '../Navigation/NavigatorService'
import { Icon, ListItem } from 'react-native-elements'
import { AsyncStorage } from 'react-native'
import ListThreads from '../Components/ListThreads/ListThreads'

type Props = { navigation: any }

export default class CategoriesScreen extends React.Component<Props> {
  state = {
    refreshing: false
  }

  onPress = (categoryId, categoryName) => {
    return NavigatorService.navigate('Threads', { categoryId, categoryName })
  }

  renderItem = ({ item }) => {
    return <ListItem titleStyle={{ fontSize: 18 }} onPress={() => this.onPress(item.id, item.title)} key={item.id} title={item.title} />
  }

  render () {
    const categoryId = get(this.props, 'navigation.state.params.categoryId')
    const categoryName = get(this.props, 'navigation.state.params.categoryName')
    return (
      <Query query={Queries.getThreadsByCategory} variables={{ categoryId }} >
        {({ data, refetch }) => {
          let threads = get(data, 'category.threads', [])
          return (
            <Screen>
              <Header
                allowGoBack
                text={`Threads - ${categoryName}`}
                rightComponent={
                  <Icon
                    onPress={async () => {
                      await AsyncStorage.clear()
                      NavigatorService.navigate('CreateThread', { categoryId, refetch })
                    }}
                    name='add'
                    color='#fff'
                  />
                }
              />
              <ListThreads
                refetch={refetch}
                threads={threads}
              />
            </Screen>
          )
        }}
      </Query >
    )
  }
}

This is how we are listing the posts.

Creating Posts

Let's copy what we have for creating the thread. The screen will be really similar, except now we just need to handle the body of the post. We can start by adding this screen into the navigation.

import CategoriesScreen from '../Screens/CategoriesScreen'
import { createStackNavigator } from 'react-navigation'
import LoginScreen from '../Screens/LoginScreen'
import ThreadsScreen from '../Screens/ThreadsScreen'
import CreateThreadScreen from '../Screens/CreateThreadScreen'
import PostsScreen from '../Screens/PostsScreen'
import CreatePostScreen from '../Screens/CreatePostScreen'

const AppNavigation = createStackNavigator(
  {
    Login: LoginScreen,
    Categories: CategoriesScreen,
    Threads: ThreadsScreen,
    Posts: PostsScreen,
    CreateThread: CreateThreadScreen,
    CreatePost: CreatePostScreen
  },
  {
    initialRouteKey: 'Login',
    headerMode: 'none'
  }
)

export default AppNavigation

Coming back to the screen, we can set up the new query. Right now, we want to create a Post. To create a post we just need to have body and threadId fields.

export const createPost = gql `
  mutation createPost($body: String!, $threadId: ID!) {
    createPost(body: $body, threadId: $threadId){
      id
      body
    }
  }
`

Let's now remove the references for the title that we were using when we were creating a thread.

import React, { Fragment } from 'react'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import { FormLabel, FormInput, FormValidationMessage, Button } from 'react-native-elements'
import { ScrollView } from 'react-native'
import { Mutation } from 'react-apollo'
import * as Mutations from '../GraphQL/mutations'
import { TabView, TabBar } from 'react-native-tab-view'
import * as NavigatorService from '../Navigation/NavigatorService'

type Props = {}

export default class CreatePostScreen extends React.Component<Props> {
  state = {
    body: ''
  }
  render () {
    const threadId = get(this.props, 'navigation.state.params.threadId')
    const refetch = get(this.props, 'navigation.state.params.refetch')
    return (
      <Screen>
        <Header text='Create a Post' allowGoBack />
        <Mutation
          mutation={Mutations.createPost}
          variables={{ threadId, body: this.state.body }}
          onCompleted={() => {
            refetch().then(() => NavigatorService.goBack())
          }}
        >
          {createPost => {
            return (<TabView
              style={{ marginTop: -5 }}
              navigationState={this.state}
              renderTabBar={props =>
                <TabBar
                  {...props}
                  style={{ backgroundColor: Colors.secondary }}
                  pressColor='blue'
                  indicatorStyle={{ height: 2, backgroundColor: 'white' }}
                />
              }
              renderScene={({ route, focused }) => {
                switch (route.key) {
                  case 'create':
                    return this.renderCreateFragment({
                      createPost, body,
                      setBody: (body) => this.setState({ body })
                    })
                  case 'preview':
                    return <PostPreview body={body} />
                  default:
                    return null
                }
              }}
              onIndexChange={index => this.setState({ index })}
            />)
          }}
        </Mutation>
      </Screen>
    )
  }
}

With this, we can create posts. Let's create some posts.

Let's have a quick overview of what we've built. We can list all the categories, we can list all the threads, create a thread. We can also list the posts for a given thread and create posts for it. Sweet!

Adding Subscriptions

Now that our app is working, let’s make it real-time. Subscriptions are a nice way to have a good experience when something on the screen is updated. For instance, when someone creates a new category, thread or post, it will appear automatically on the screen.

Setting up Subscriptions

Our client is already set up to work with subscriptions.

Adding Category Subscription

Let's start by adding the subscription to handle when a new category is added. We cannot add categories in the app, but we can add them through GraphQL Playground. Once a new category is added, we're going to show them in our list.

Let's add our first subscription, categoryAdded. We will get back the category itself.

import gql from 'graphql-tag'

export const categoryAdded = gql`
subscription categoryAddedSub {
    categoryAdded{
      id
      title
      slug
    }
  }
`

We can use apollo’s Subscription component to subscribe to categoryAdded. We will have the Subscription component inside of our Query component. We will have initially the data coming from our Query, then once the subscription is fired, we will update the categories entries, adding the new category in the array.

So, we will use the Subscription component where we will enable the resubscriptions by using the shouldResubscribe property, and on the subscription property, we are going to pass the subscription we've just created.

<Screen>
        <Header
          text='Categories'
          rightComponent={
            <Icon
              onPress={() => {
                AsyncStorage.clear()
                NavigatorService.reset('Login')
              }}
              name='exit-to-app'
              color='#fff'
            />
          }
        />
        <Query query={Queries.getCategories} variables={{ pagination: { page: 0, perPage: 5 } }}>
          {({ data, fetchMore }) => {
            let categories = get(data, 'categories', { entries: [], page: 0 })
            return (
              <Subscription
                shouldResubscribe
                subscription={Subscriptions.categoryAdded}
              >
                {({ data }) => {
                  if (data) {
                    categories = set(categories, 'entries', [data.categoryAdded, ...categories.entries])
                    uniqBy(categories, 'id')
                  }
                  return (<FlatList
                    onEndReached={this.loadMore(fetchMore, categories.page)}
                    onEndReachedThreshold={0.8}
                    data={categories.entries}
                    keyExtractor={item => item.id}
                    renderItem={this.renderItem}
                  />)
                }}
              </Subscription>
            )
          }}
        </Query>
      </Screen>

To add the new category coming from the subscription data, we are going to use lodash get, to just add the category inside of the array entries. So, every time a new category arrives from the subscription, we are going to just add it. Then the screen will be updated with this new category.

import React from 'react'
import * as Queries from '../GraphQL/queries'
import * as Subscriptions from '../GraphQL/subscriptions'
import Query from '../GraphQL/Query'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import * as NavigatorService from '../Navigation/NavigatorService'
import { Icon, ListItem, Text } from 'react-native-elements'
import { AsyncStorage, FlatList, View } from 'react-native'
import { prepare } from '../Helpers/RecordsHelper'
import { Subscription } from 'react-apollo'
import set from 'lodash/set'
import uniqBy from 'lodash/uniqBy'

type Props = { navigation: any }

export default class CategoriesScreen extends React.Component<Props> {
  state = {
    refreshing: false
  }

  merge = (result, oldEntries) => ({
    ...result,
    categories: {
      ...result.categories,
      entries: prepare([...oldEntries, ...result.categories.entries])
    }
  })

  loadMore = (fetchMore, page) => () => {
    fetchMore({
      variables: { pagination: { page: page + 1, perPage: 5 } },
      updateQuery: (prev, { fetchMoreResult }) => {
        if (!fetchMoreResult) return prev

        if (prev && prev.categories && prev.categories.entries) {
          return this.merge(fetchMoreResult, prev.categories.entries)
        }
      }
    })
  }

  onPress = (categoryId, categoryName) => {
    return NavigatorService.navigate('Threads', { categoryId, categoryName })
  }

  renderItem = ({ item }) => {
    return <ListItem titleStyle={{ fontSize: 18 }} onPress={() => this.onPress(item.id, item.title)} key={item.id} title={item.title} />
  }

  render () {
    return (
      <Screen>
        <Header
          text='Categories'
          rightComponent={
            <Icon
              onPress={async () => {
                await AsyncStorage.clear()
                NavigatorService.reset('Login')
              }}
              name='exit-to-app'
              color='#fff'
            />
          }
        />
        <Query query={Queries.getCategories} variables={{ pagination: { page: 0, perPage: 5 } }}>
          {({ data, fetchMore, refetch }) => {
            let categories = get(data, 'categories', { entries: [], page: 0 })
            return (
              <Subscription
                shouldResubscribe
                subscription={Subscriptions.categoryAdded}
              >
                {({ data }) => {
                  if (data) {
                    categories = set(categories, 'entries', [data.categoryAdded, ...categories.entries])
                    uniqBy(categories, 'id')
                  }
                  return (
                    <FlatList
                      onEndReached={this.loadMore(fetchMore, categories.page)}
                      onEndReachedThreshold={0.8}
                      refreshing={this.state.refreshing}
                      onRefresh={refetch}
                      data={categories.entries}
                      keyExtractor={item => item.id}
                      renderItem={this.renderItem}
                    />
                  )
                }}
              </Subscription>
            )
          }}
        </Query>
      </Screen>
    )
  }
}

This should be enough to get what we want. Let's test it by adding a new category from GraphQL Playground.

When we create a new category in GraphQL Playground, the category we created shows up on the screen! Awesome!

Adding Thread Subscription

Let's add the subscription to the threads.

export const threadAdded = gql`
subscription threadAdded($categoryId: ID!) {
    threadAdded(categoryId: $categoryId) {
      id
      title
      slug
    }
  }
`

This subscription needs a categoryId, because we will be listening just for the threads that were added on the category we’re looking at.

Now, like we did on the Categories screen, we will place the Subscription component inside of our Query component. We will need to pass the categoryId into the subscription.

Once we have new data from the subscription, we are going to add it to the array of threads.

<Query query={Queries.getThreadsByCategory} variables={{ categoryId }}>
        {({ data, refetch }) => {
          let threads = get(data, 'category.threads', [])
          return (< Subscription
            shouldResubscribe
            subscription={Subscriptions.threadAdded}
            variables={{ categoryId }}
          >
            {({ data }) => {
              if (data) {
                threads = [data.threadAdded, ...threads]
              }
              return (
                <Screen>
                  <Header
                    text={`Threads - ${categoryName}`}
                    allowGoBack
                    rightComponent={
                      <Icon
                        onPress={() =>
                          NavigatorService.navigate('CreateThread', {
                            categoryId,
                            refetch
                          })}
                        name='add'
                        color='#fff'
                      />
                    }
                  />
                  <ListThreads
                    refetch={refetch}
                    threads={threads}
                  />
                </Screen>
              )
            }}
          </Subscription>)
        }
        }
      </Query >)

Our Screen will look like this:

import React from 'react'
import ListThreads from '../Components/ListThreads/ListThreads'
import * as Queries from '../GraphQL/queries'
import * as Subscriptions from '../GraphQL/subscriptions'
import Query from '../GraphQL/Query'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import * as NavigatorService from '../Navigation/NavigatorService'
import { Icon } from 'react-native-elements'
import { Subscription } from 'react-apollo'
import { prepare } from '../Helpers/RecordsHelper'

type Props = {}

export default class App extends React.Component<Props> {
  render () {
    const categoryId = get(this.props, 'navigation.state.params.categoryId')
    const categoryName = get(this.props, 'navigation.state.params.categoryName')
    return (
      <Query query={Queries.getThreadsByCategory} variables={{ categoryId }}>
        {({ data, refetch }) => {
          let threads = get(data, 'category.threads', [])
          return (< Subscription
            shouldResubscribe
            subscription={Subscriptions.threadAdded}
            variables={{ categoryId }}
          >
            {({ data }) => {
              if (data) {
                threads = prepare([data.threadAdded, ...threads])
              }
              return (
                <Screen>
                  <Header
                    text={`Threads - ${categoryName}`}
                    allowGoBack
                    rightComponent={
                      <Icon
                        onPress={() =>
                          NavigatorService.navigate('CreateThread', {
                            categoryId,
                            refetch
                          })}
                        name='add'
                        color='#fff'
                      />
                    }
                  />
                  <ListThreads
                    refetch={refetch}
                    threads={threads}
                  />
                </Screen>
              )
            }}
          </Subscription>)
        }
        }
      </Query >)
  }
}

This is how we are going to update our screen with the new thread. Let's test it! We’ll add a new thread, and voilà, we instantly see the thread we've just added.

Adding the Post Subscription

Next, we are going to add the subscription for posts.

export const postAdded = gql `
  subscription postAdded($threadId: ID!){
    postAdded(threadId: $threadId){
      id
      body
      insertedAt
      user {
        id
        name
        username
        avatarUrl
        email
      }
    }
  }
`

The subscription for posts needs a threadId, because we will just subscribe to posts added to the thread we’re viewing.

Now, we can add the Subscription component like we did on the other screens. We are going to append the post that will be added from the subscription to our list of posts. We are using the prepare method and we are adding the new posts at the end of the list.

Our screen will look like:

import React from 'react'
import ListPosts from '../Components/ListPosts/ListPosts'
import * as Queries from '../GraphQL/queries'
import Query from '../GraphQL/Query'
import get from 'lodash.get'
import Header from '../Components/Header/Header'
import Screen from '../Components/Screen/Screen'
import { Icon } from 'react-native-elements'
import * as NavigatorService from '../Navigation/NavigatorService'
import * as Subscriptions from '../GraphQL/subscriptions'
import { Subscription } from 'react-apollo'
import { prepare } from '../Helpers/RecordsHelper'

type Props = {}

export default class PostsScreen extends React.Component<Props> {
  render () {
    const threadId = get(this.props, 'navigation.state.params.threadId')
    const threadTitle = get(this.props, 'navigation.state.params.threadTitle')
    return (
      <Query query={Queries.getPostsByThread} variables={{ threadId }}>
        {({ data, refetch }) => {
          let posts = get(data, 'thread.posts', [])
          return (< Subscription
            shouldResubscribe
            subscription={Subscriptions.postAdded}
            variables={{ threadId }}
          >
            {({ data }) => {
              if (data) {
                posts = prepare([data.postAdded, ...posts], ['insertedAt', 'id'], false)
              }
              return (
                <Screen>
                  <Header
                    text={`${threadTitle}`}
                    allowGoBack
                    rightComponent={
                      <Icon
                        onPress={() => NavigatorService.navigate('CreatePost', { threadId, refetch })}
                        name='add'
                        color='#fff'
                      />
                    }
                  />
                  <ListPosts refetch={refetch} posts={posts} />
                </Screen>
              )
            }}</Subscription>)
        }}
      </Query>
    )
  }
}

Let's test it by adding a new post. We immediately see the new post on our phone! Awesome!

Summary

We have finished our React Native app and it is able to list categories, threads and posts. We can also create threads and posts. It also works with Subscriptions, so we can have real-time updates as the data changes.

Resources