CRUD (Create Read Update Delete) / AWS Amplify + GraphQL + React Native

serverlesskiy

Server Serverlesskiy

Posted on December 10, 2019

CRUD (Create Read Update Delete) / AWS Amplify + GraphQL + React Native

This part II is a logical continuation of part I, the series Full Stack Serverless.
At the end of this article, we will make this mobile application:

mobileapp

The final code for this part can be found on Github.

Introduction

Creating a backend on AWS Amplify is working with serverless technology, so before proceeding with coding, we will figure out what serverless computing is and what are their advantages over server computing.

Prediction of pundits from Berkeley University about how the backend technology will develop:

By providing a simplified programming environment, serverless computing makes the cloud much easier to use, thereby attracting more people who can and will use it. Serverless computing comprises FaaS and BaaS offerings, and marks an important maturation of cloud programming. It obviates the need for manual resource management and optimization that today’s serverful computing imposes on application developers, a maturation akin to the move from assembly language to high-level languages more than four decades ago.

We predict that serverless use will skyrocket. We also project that hybrid cloud on-premises applications will dwindle over time, though some deployments might persist due to regulatory constraints and data governance rules.

Serverless computing will become the default computing paradigm of the Cloud Era, largely replacing serverful computing and thereby bringing closure to the Client-Server Era.

Cloud Programming Simplified: A Berkeley View on Serverless Computing

Serverless computing is a natural cloud architecture that transfers most of AWS's operational responsibility, and thus provides more flexibility and innovative capabilities. Serverless computing allows you to create and run applications and services without worrying about servers. They eliminate the need to deal with infrastructure management issues, such as allocating servers or clusters, necessary resources, as well as installing patches and maintaining the operating system. They can be used for almost any type of application or server-side services, while everything that is required to run and scale an application with high availability is performed without client intervention.

Backend - Create an API

We will now create the GraphQL API, which interacts with the DynamoDB NoSQL database to perform CRUD operations (create, read, update, delete).

amplify add api 
Enter fullscreen mode Exit fullscreen mode

addapi

After the selected items, a diagram will open, which is always available for editing at ./amplify/backend/api/messaga/schema.graphql
Where we add the following code:

type Job 
  @model
  @auth(
    rules: [
      {allow: owner, ownerField: "owner", operations: [create, update, delete]},
    ])
{
  id: ID!
  position: String!
  rate: Int!
  description: String!
  owner: String
}
Enter fullscreen mode Exit fullscreen mode

This is a GraphQL schema. GraphQL Transform provides an easy-to-use abstraction that helps you quickly create server parts for web and mobile applications in AWS. Using GraphQL Transform, you define the data model of your application using the GraphQL Schema Definition Language (SDL), and the library handles the conversion of the SDL definition to a set of fully descriptive AWS CloudFormation templates that implement your data model.

When used with tools like the Amplify CLI, GraphQL Transform simplifies the process of developing, deploying, and supporting GraphQL APIs. With it, you define your API using the GraphQL Schema Definition Language (SDL) and then use automation to transform it into a fully descriptive cloud information template that implements the specification.

GraphQL is an API specification. This is the query language for the API and the runtime to execute these queries with your data. It has some similarities with REST and is the best replacement for REST.

GraphQL was introduced by Facebook in 2015, although it has been used internally since 2012. GraphQL allows clients to determine the structure of the required data, and it is this structure that is returned from the server. Querying data in this way provides a much more efficient way for client-side applications to interact with APIs, reducing the number of incomplete samples and preventing excessive data samples.

Learn more about the benefits of GraphQL here.

Let us return to our scheme, where the main components of the GraphQL scheme are object types, which simply represent the type of object that you can extract from your service, and what fields it has.

Job is a type of a GraphQL object (GraphQL Object Type), that is, a type with some fields. Most types in your schema will be object types.

id position rate description owner - fields in type Job. This means that these are the only fields that can appear in any part of a GraphQL query that works with the Job type.

String is one of the built-in scalar types - these are types that are resolved into a single scalar object and cannot have subselects in the query. We will look at scalar types later.

String! - the field is not NULL, means that the GraphQL service promises to always give you a value when requesting this field. In general, this is a required field.

GraphQL comes with a set of default scalar types out of the box:

Int 32-bit signed integer.

Float double-precision floating point value.

String character sequence UTF - 8.

Boolean true or false.

ID scalar type ID is a unique identifier often used to retrieve an object or as a key for a cache. The type of identifier is serialized in the same way as a string; however, the definition of it as an identifier means that it is not intended for human perception.

Directives

@model — Object types marked with @model are top level objects in the generated API. Objects marked with @model are stored in Amazon DynamoDB and can be protected with @auth, linked to other objects via @connection

@auth - Authorization is required for applications to interact with your GraphQL API. API keys are best used for public APIs.
The @auth object types annotated by @auth are protected by a set of authorization rules that provide you with additional controls than top-level authorization in the API. You can use the @ auth directive to determine the types of objects and fields in your project schema.
When using the @ auth directive for object type definitions, which are also annotated by @model, all recognition tools that return objects of this type will be protected.

Other directives and details in official documentation.

@auth directive rules

@auth( rules: [ {allow: owner, ownerField: "owner", operations: [create, update, delete]} ])
Enter fullscreen mode Exit fullscreen mode

mean that the operations CREATE, UPDATE, DELETE are allowed exclusively to the owner, and the read operation is for everyone.

It's time to test it in practice! Therefore, we write the command in the console:

amplify mock api
Enter fullscreen mode Exit fullscreen mode

mockapi

With this team, you can quickly test your achievements of change without the need to allocate or update the cloud resources that you use at each stage. In this way, you can configure unit and integration tests that can be performed quickly without affecting your cloud backend.

Three whales on which GraphQL stands:

Query (READ)

Simply put, queries in GraphQL are how you intend to query data. You will receive exactly the data that you need. No more no less.

Mutation (CREATE UPDATE DELETE)

Mutations in GraphQL are a way to change data on a server and get updated data back.

Subscriptions

A way to maintain a connection to the server in real time. This means that whenever an event occurs on the server and when this event is called, the server will send the appropriate data to the client.

You can see all available methods by clicking on Docs (Documentation Explorer) in the upper right corner. The values are clickable, so you can see all the possible queries.

docs

CREATE

We open our API at the address that issued (each has its own) the result of the amplify mock api command

mockapi

and execute the CREATE query by pressing the play button.

mutation Create {
  __typename
  createJob(input: {position: "React Native Developer", rate: 3000, description: "We are looking for a React Native developer (St. Petersburg) to develop from scratch a mobile version of the main cs.money platform  Our product is an international trading platform for the exchange of virtual items. (CS: GO, Dota 2) which is shared by more than 5 million users. The project has existed for more than 3 years. and takes a leading position in its field. The platform is used in more than 100 countries of the world.  Now we want to make a mobile application and decided to do it on React Native. You have to develop an application from scratch and this is a great opportunity to build a good architecture without resting on legacy.  Requirements:  Knowledge of react patterns Knowledge of SOLID principles Knowledge of the basics of mobile specifics (caching, working with the native API, rendering optimization) Knowledge of RN"}) {
    description
    id
    owner
    position
    rate
  }
}
Enter fullscreen mode Exit fullscreen mode

create

To consolidate the material, create some more vacancies.

READ

We get a list of all the vacancies. Insert request:

query Read {
  __typename
  listJobs {
    items {
      description
      id
      owner
      position
      rate
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

read

UPDATE

To update, we need to take the vacancy ID (be sure to enter your own, and not from the example) and pass it to this request with the data changed. For example, update the position and rate fields

mutation Update {
  __typename
  updateJob(input: {id: "1a8a763f-28b8-450a-96f0-73e0d1d8ac04", position: "Full Stack Serverless", rate: 5000}) {
    id
    description
    owner
    position
    rate
  }
}
Enter fullscreen mode Exit fullscreen mode

update

DELETE

To delete, as well as in the case of the update, we need to transfer the vacancy ID (be sure to enter your own, and not from the example).

mutation Delete {
  __typename
  deleteJob(input: {id: "1a8a763f-28b8-450a-96f0-73e0d1d8ac04"}) {
    id
  }
}
Enter fullscreen mode Exit fullscreen mode

delete

Permissions

Now let's check if our rules work, which we indicated in the scheme. Only the owner can update, delete and create.

@auth( rules: [ {allow: owner, ownerField: owner, operations: [create, update, delete]} ])
Enter fullscreen mode Exit fullscreen mode

To change the user, click on UpdateAuth in the main menu. Where we randomly update Username and Email.

updateauth

authoptions

If we send a READ request, then it works, but if we send an UPDATE or DELETE request and we get an error.

error

The rules work, as required!

Now that we have tested the functionality of the API, we can publish it to the cloud with the command:

amplify push
Enter fullscreen mode Exit fullscreen mode

push

Or immediately proceed to create a mobile interface to the created API in the React Native application.

FRONTEND

Preparation

To implement this workflow, we need the following libraries:
react-navigation-tabs
@aws-amplify/api
@aws-amplify/pubsub
aws-amplify-react-hooks

We put them at once:

yarn add react-navigation-tabs @aws-amplify/api@1.2.4 @aws-amplify/core@1.2.4 @aws-amplify/pubsub@1.2.4 aws-amplify-react-hooks
Enter fullscreen mode Exit fullscreen mode

Creation and layout of components

Card

Create a Card Wrap Component
src/components/Card/index.js

import React, { memo } from 'react'
import { StyleSheet, View } from 'react-native'
import { BLUE } from '../../constants'

const styles = StyleSheet.create({
  card: {
    borderRadius: 17,
    borderWidth: 0.5,
    borderColor: BLUE,
    padding: 25,
    marginBottom: 20
  }
})

const Card = memo(({ children }) => {
  const { card } = styles
  return <View style={card}>{children}</View>
})

export { Card }
Enter fullscreen mode Exit fullscreen mode

CardJob

Create the CardJob component
src/components/CardJob/index.js

cardjob

import React, { memo } from 'react'
import { StyleSheet, TouchableWithoutFeedback, Text, View } from 'react-native'
import { BLUE } from '../../constants'

const styles = StyleSheet.create({
  card: {
    borderRadius: 17,
    borderWidth: 0.5,
    borderColor: BLUE,
    padding: 25,
    height: 200,
    marginBottom: 20
  },
  h1: {
    fontFamily: '3270Narrow',
    color: '#dbdbdb',
    fontSize: 18
  },
  h2: {
    fontFamily: '3270Narrow',
    color: '#6a676a',
    marginVertical: 15,
    fontSize: 13,
    letterSpacing: 0.92,
    flexGrow: 1
  },
  footer: {
    flexDirection: 'row',
    justifyContent: 'space-between'
  },
  h3: {
    color: '#cfcfcf',
    fontFamily: '3270Narrow',
    textTransform: 'uppercase',
    fontSize: 18,
    letterSpacing: 0.97
  },
  h4: {
    color: '#cfcfcf',
    fontFamily: '3270Narrow',
    textTransform: 'uppercase',
    fontSize: 18,
    letterSpacing: 1.17
  }
})

const CardJob = memo(({ item: { position, description, rate, owner }, onPress }) => {
  const { card, h1, h2, footer, h3, h4 } = styles
  const userSlice = owner.slice(0, 10)
  return (
    <TouchableWithoutFeedback onPress={onPress}>
      <View style={card}>
        <Text style={h1}>{position}</Text>
        <Text style={h2} numberOfLines={5} ellipsizeMode="tail">
          {description}
        </Text>
        <View style={footer}>
          <Text style={h3}>{userSlice}</Text>
          <Text style={h4}>{rate}$</Text>
        </View>
      </View>
    </TouchableWithoutFeedback>
  )
})

export { CardJob }
Enter fullscreen mode Exit fullscreen mode

CardJobDetail

Creating the CardJobDetail Component
src/components/CardJobDetail/index.js
cardjobdetail

import React, { memo } from 'react'
import { StyleSheet, TouchableWithoutFeedback, Text, View } from 'react-native'
import { BLUE } from '../../constants'

const styles = StyleSheet.create({
  card: {
    borderRadius: 17,
    borderWidth: 0.5,
    borderColor: BLUE,
    padding: 25,
    marginBottom: 20
  },
  h1: {
    fontFamily: '3270Narrow',
    color: '#dbdbdb',
    fontSize: 18
  },
  h2: {
    fontFamily: '3270Narrow',
    color: '#6a676a',
    marginVertical: 15,
    fontSize: 13,
    letterSpacing: 0.92,
    flexGrow: 1
  },
  footer: {
    flexDirection: 'row',
    justifyContent: 'space-between'
  },
  h3: {
    color: '#cfcfcf',
    fontFamily: '3270Narrow',
    textTransform: 'uppercase',
    fontSize: 18,
    letterSpacing: 0.97
  },
  h4: {
    color: '#cfcfcf',
    fontFamily: '3270Narrow',
    textTransform: 'uppercase',
    fontSize: 18,
    letterSpacing: 1.17
  }
})

const CardJobDetail = memo(({ item: { position, description, rate, owner }, onPress }) => {
  const { card, h1, h2, footer, h3, h4 } = styles
  const userSlice = owner.slice(0, 10)
  return (
    <TouchableWithoutFeedback onPress={onPress}>
      <View style={card}>
        <Text style={h1}>{position}</Text>
        <Text style={h2}>{description}</Text>
        <View style={footer}>
          <Text style={h3}>{userSlice}</Text>
          <Text style={h4}>{rate}$</Text>
        </View>
      </View>
    </TouchableWithoutFeedback>
  )
})

export { CardJobDetail }
Enter fullscreen mode Exit fullscreen mode

InputMuliline

Create InputMuliline
Edit src/screens/Authenticator/Form/index.js
input

import { Platform } from 'react-native'
import t from 'tcomb-form-native'
import FloatingLabel from 'react-native-floating-label'
import {
  LABEL_COLOR,
  INPUT_COLOR,
  ERROR_COLOR,
  HELP_COLOR,
  BORDER_COLOR,
  DISABLED_COLOR,
  DISABLED_BACKGROUND_COLOR
} from '../../../constants'

const formValidation = {
  email: t.refinement(t.String, value => /@/.test(value)),
  password: t.refinement(t.String, value => value.length >= 8)
}

+export const structJob = t.struct({
+  position: t.refinement(t.String, value => value.length >= 3 && value <= 30),
+  rate: t.Number,
+  description: t.String
+})

export const structSignIn = t.struct({
  email: formValidation.email,
  password: formValidation.password
})

export const structSignUp = t.struct({
  email: formValidation.email,
  password: formValidation.password,
  passwordConfirmation: formValidation.password
})

export const structForgot = t.struct({
  email: formValidation.email
})

export const structForgotPass = t.struct({
  email: formValidation.email,
  code: t.Number,
  password: formValidation.password,
  passwordConfirmation: formValidation.password
})

export const structConfirmSignUp = t.struct({
  code: t.Number
})

const FONT_SIZE = 17
const FONT_WEIGHT = '500'

// Fixme
const Form = t.form.Form // eslint-disable-line

const formStyles = {
  ...Form.stylesheet,
  fieldset: {},
  // the style applied to the container of all inputs
  formGroup: {
    normal: {
      marginBottom: 8
    },
    error: {
      marginBottom: 8,
      marginLeft: 5,
      marginRight: 5
    }
  },
  controlLabel: {
    normal: {
      color: LABEL_COLOR,
      fontFamily: '3270Narrow',
      fontSize: FONT_SIZE,
      marginLeft: 5,
      marginBottom: 7,
      fontWeight: FONT_WEIGHT
    },
    // the style applied when a validation error occours
    error: {
      marginLeft: 5,
      marginRight: 5,
      fontFamily: '3270Narrow',
      color: ERROR_COLOR,
      fontSize: FONT_SIZE,
      marginBottom: 7
    }
  },
  helpBlock: {
    normal: {
      color: HELP_COLOR,
      fontFamily: '3270Narrow',
      fontSize: FONT_SIZE,
      marginBottom: 2
    },
    // the style applied when a validation error occours
    error: {
      marginLeft: 5,
      marginRight: 5,
      color: HELP_COLOR,
      fontFamily: '3270Narrow',
      fontSize: FONT_SIZE,
      marginBottom: 2
    }
  },
  errorBlock: {
    fontFamily: '3270Narrow',
    fontSize: 12,
    marginBottom: 2,
    marginLeft: 5,
    marginRight: 5,
    color: ERROR_COLOR
  },
  textboxView: {
    normal: {},
    error: {},
    notEditable: {}
  },
  textbox: {
    normal: {
      color: INPUT_COLOR,
      fontFamily: '3270Narrow',
      fontSize: FONT_SIZE,
      height: 50,
      paddingVertical: Platform.OS === 'ios' ? 7 : 0,
      paddingHorizontal: 12,
      borderRadius: 4,
      borderColor: BORDER_COLOR,
      borderWidth: 0.5,
      marginLeft: 5,
      marginRight: 5
    },
    // the style applied when a validation error occours
    error: {
      color: INPUT_COLOR,
      fontFamily: '3270Narrow',
      fontSize: FONT_SIZE,
      height: 50,
      paddingVertical: Platform.OS === 'ios' ? 7 : 0,
      paddingHorizontal: 12,
      borderRadius: 4,
      borderColor: ERROR_COLOR,
      borderWidth: 0.5,
      marginBottom: 5
    },
    // the style applied when the textbox is not editable
    notEditable: {
      fontFamily: '3270Narrow',
      fontSize: FONT_SIZE,
      height: 50,
      paddingVertical: Platform.OS === 'ios' ? 7 : 0,
      paddingHorizontal: 7,
      borderRadius: 4,
      borderColor: BORDER_COLOR,
      borderWidth: 0.5,
      marginBottom: 5,
      color: DISABLED_COLOR,
      backgroundColor: DISABLED_BACKGROUND_COLOR
    }
  },
  checkbox: {
    normal: {
      marginBottom: 4
    },
    // the style applied when a validation error occours
    error: {
      marginBottom: 4
    }
  },
  pickerContainer: {
    normal: {
      marginBottom: 4,
      borderRadius: 4,
      borderColor: BORDER_COLOR,
      borderWidth: 0.5
    },
    error: {
      marginBottom: 4,
      borderRadius: 4,
      borderColor: ERROR_COLOR,
      borderWidth: 0.5
    },
    open: {
      // Alter styles when select container is open
    }
  },
  select: {
    normal: Platform.select({
      android: {
        paddingLeft: 7,
        color: INPUT_COLOR
      },
      ios: {}
    }),
    // the style applied when a validation error occours
    error: Platform.select({
      android: {
        paddingLeft: 7,
        color: ERROR_COLOR
      },
      ios: {}
    })
  },
  pickerTouchable: {
    normal: {
      height: 44,
      flexDirection: 'row',
      alignItems: 'center'
    },
    error: {
      height: 44,
      flexDirection: 'row',
      alignItems: 'center'
    },
    active: {
      borderBottomWidth: 1,
      borderColor: BORDER_COLOR
    },
    notEditable: {
      height: 44,
      flexDirection: 'row',
      alignItems: 'center',
      backgroundColor: DISABLED_BACKGROUND_COLOR
    }
  },
  pickerValue: {
    normal: {
      fontFamily: '3270Narrow',
      fontSize: FONT_SIZE,
      paddingLeft: 7
    },
    error: {
      fontSize: FONT_SIZE,
      paddingLeft: 7
    }
  },
  datepicker: {
    normal: {
      marginBottom: 4
    },
    // the style applied when a validation error occours
    error: {
      marginBottom: 4
    }
  },
  dateTouchable: {
    normal: {},
    error: {},
    notEditable: {
      backgroundColor: DISABLED_BACKGROUND_COLOR
    }
  },
  dateValue: {
    normal: {
      color: INPUT_COLOR,
      fontSize: FONT_SIZE,
      padding: 7,
      marginBottom: 5
    },
    error: {
      color: ERROR_COLOR,
      fontSize: FONT_SIZE,
      padding: 7,
      marginBottom: 5
    }
  },
  buttonText: {
    fontFamily: '3270Narrow',
    fontSize: 18,
    color: 'white',
    alignSelf: 'center'
  },
  button: {
    height: 36,
    backgroundColor: INPUT_COLOR,
    borderColor: INPUT_COLOR,
    borderWidth: 0.5,
    borderRadius: 8,
    marginBottom: 10,
    alignSelf: 'stretch',
    justifyContent: 'center'
  }
}

export const options = {
  fields: {
    email: {
      stylesheet: formStyles,
      placeholder: 'Email',
      secureTextEntry: false,
      keyboardType: 'email-address',
      autoCapitalize: 'none',
      error: 'Without an email address how are you going to reset your password when you forget it?',
      factory: FloatingLabel
    },
    password: {
      stylesheet: formStyles,
      placeholder: 'Password',
      secureTextEntry: true,
      error: "Choose something you use on a dozen other sites or something you won't remember",
      factory: FloatingLabel
    },
    passwordConfirmation: {
      stylesheet: formStyles,
      placeholder: 'Confirm Password',
      secureTextEntry: true,
      error: "Choose something you use on a dozen other sites or something you won't remember",
      factory: FloatingLabel
    },
    code: {
      stylesheet: formStyles,
      placeholder: 'Verification code',
      secureTextEntry: false,
      keyboardType: 'numeric',
      error: 'Confident in their actions?',
      factory: FloatingLabel
    },
+    position: {
+      stylesheet: formStyles,
+      placeholder: 'Position',
+      secureTextEntry: false,
+      autoCapitalize: 'none',
+      error: 'Please enter position name',
+      factory: FloatingLabel
+    },
+    rate: {
+      stylesheet: formStyles,
+      placeholder: 'Rate',
+      secureTextEntry: false,
+      autoCapitalize: 'none',
+      keyboardType: 'numeric',
+      error: 'Please enter rate',
+      factory: FloatingLabel
+    },
+    description: {
+      stylesheet: {
+        ...Form.stylesheet,
+        controlLabel: {
+          normal: {
+            color: LABEL_COLOR,
+            fontFamily: '3270Narrow',
+            fontSize: FONT_SIZE,
+            marginLeft: 5,
+            marginBottom: 7,
+            justifyContent: 'center',
+            fontWeight: FONT_WEIGHT
+          },
+          error: {
+           color: ERROR_COLOR,
+            fontFamily: '3270Narrow',
+            fontSize: FONT_SIZE,
+            marginLeft: 5,
+            marginRight: 7,
+            fontWeight: FONT_WEIGHT
+          }
+        },
+        textbox: {
+          normal: {
+            height: 300,
+            color: INPUT_COLOR,
+            fontFamily: '3270Narrow',
+            fontSize: FONT_SIZE,
+            paddingHorizontal: 12,
+            borderRadius: 4,
+            textAlignVertical: 'top',
+            borderColor: BORDER_COLOR,
+            borderWidth: 0.5,
+            paddingTop: 13,
+            paddingBottom: 0,
+            marginLeft: 5,
+           marginRight: 5
+          },
+          error: {
+            ...Form.stylesheet.textbox.error,
+            height: 150
+          }
+        }
+      },
+      placeholder: 'Description',
+      secureTextEntry: false,
+      multiline: true,
+      autoCapitalize: 'none',
+      error: 'Please enter rate',
+      factory: FloatingLabel
+    }
  }
}
Enter fullscreen mode Exit fullscreen mode

AmplifyProvider

Essentially the same as ApolloProvider for Apollo and Provider for Redux.
Provides the ability to pass Auth authentication parameters, access to the API, and conducting graphqlOperation from anywhere in the application.

API - This is the GraphQL client that we will use to interact with the AppSync endpoint (analogue to fetch or axios)

Auth - This is a class from AWS Amplify that handles user management. You can use this class for everything from registering a user to resetting his password. In this component, we will call the Auth.user.attributes.sub method, which will return the current user ID

graphqlOperation - This is a JavaScript utility that parses GraphQL operations

In order to connect it. Editing the src/index.js file

import React from 'react'
import { StatusBar } from 'react-native'
import Amplify from '@aws-amplify/core'
+ import { Auth, API, graphqlOperation } from 'aws-amplify'
import * as Keychain from 'react-native-keychain'
import AppNavigator from './AppNavigator'
import awsconfig from '../aws-exports'
+ import { AmplifyProvider } from 'aws-amplify-react-hooks'

const MEMORY_KEY_PREFIX = '@MyStorage:'
let dataMemory = {}

+ const client = {
+   Auth,
+   API,
+   graphqlOperation
+ }

+ AmplifyProvider(client)

class MyStorage {
  static syncPromise = null

  static setItem(key, value) {
    Keychain.setGenericPassword(MEMORY_KEY_PREFIX + key, value)
    dataMemory[key] = value
    return dataMemory[key]
  }

  static getItem(key) {
    return Object.prototype.hasOwnProperty.call(dataMemory, key) ? dataMemory[key] : undefined
  }

  static removeItem(key) {
    Keychain.resetGenericPassword()
    return delete dataMemory[key]
  }

  static clear() {
    dataMemory = {}
    return dataMemory
  }
}

Amplify.configure({
  ...awsconfig,
  Analytics: {
    disabled: false
  },
  storage: MyStorage
})

const App = () => {
  return (
    <>
+     <AmplifyProvider client={client}>
        <StatusBar barStyle="dark-content" />
        <AppNavigator />
+      </AmplifyProvider>
    </>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Screens

JobsMain

Create a JobsMain screen
src/screens/Jobs/JobsMain.js

jobsmain

On this screen, we will make a Query query, with the pagination option, where the number is through the useQuery hook and it will return an array to us, which we will send to Flatlist.

import React from 'react'
import { FlatList } from 'react-native'
import { Auth } from 'aws-amplify'
import { useQuery, getNames } from 'aws-amplify-react-hooks'
import { listJobs } from '../../graphql/queries'
import { AppContainer, CardJob } from '../../components'
import { onScreen, BG } from '../../constants'
import { onCreateJob, onUpdateJob, onDeleteJob } from '../../graphql/subscriptions'

const JobsMain = ({ navigation }) => {
  const owner = Auth.user.attributes.sub

  const { data, loading, error, fetchMore } = useQuery(
    {
      listJobs,
      onCreateJob,
      onUpdateJob,
      onDeleteJob
    },
    {
      variables: { limit: 5 }
    },
    getNames({ listJobs, onCreateJob, onUpdateJob, onDeleteJob })
  )

  const _renderItem = ({ item }) => {
    const check = owner === item.owner
    return <CardJob item={item} onPress={onScreen(check ? 'JOB_ADD' : 'JOB_DETAIL', navigation, item)} />
  }

  const _keyExtractor = obj => obj.id.toString()

  return (
    <AppContainer
      flatlist
      message={error}
      loading={loading}
      iconRight="plus-a"
      colorLeft={BG}
      title=" "
      onPressRight={onScreen('JOB_ADD', navigation)}
    >
      <FlatList
        scrollEventThrottle={16}
        data={data}
        renderItem={_renderItem}
        keyExtractor={_keyExtractor}
        onEndReachedThreshold={0.5}
        onEndReached={fetchMore}
      />
    </AppContainer>
  )
}

export { JobsMain }
Enter fullscreen mode Exit fullscreen mode

Flatlist - Productive interface for rendering lists
listJobs - GraphQL query operation to retrieve an array of data
limit - The number of elements in the pagination response.
useQuery - The hook to retrieve data per page.
onCreateJob onUpdateJob onDeleteJob - GraphQL query operations that implement real-time updating.
getNames - Function for obtaining names from generated Amplify queries

Next, we get the data through a hookQuery which has the following API:

useQuery

const { 
  data: Array<mixed>,
  loading: string,
  error: string,
  fetchMore: function
} = useQuery(query {}, options: { variables: {[key: string]: any }}, queryData: Array<string>)
Enter fullscreen mode Exit fullscreen mode

query - The first argument is a GraphQL query READ operation, the second is a CREATE subscription operation, the third is an UPDATE subscription operation and the fourth is a DELETE subscription operation.

option - An object containing all the variables that your request should fulfill.

queryData - An array of GraphQL operation names in the READ, CREATE, UPDATE, DELETE sequence.

data — The returned data array.

loading - Loading indicator.

error - Error.

fetchMore - Often in your application there will be some views in which you need to display a list that contains too much data so that it can either be retrieved or displayed immediately. Pagination is the most common solution to this problem, and the useQuery hook has built-in functionality that makes it pretty simple. The easiest way to do pagination is to use the fetchMore function, which is included in the result object returned by the useQuery hook. This basically allows you to make a new GraphQL query and combine the result with the original result.

JobsDetail

Create a JobsDetail screen src/screens/Jobs/JobDetail.js

jobsdetail

import React from 'react'
import {AppContainer, CardJobDetail} from '../../components'
import {goBack} from '../../constants'

const JobDetail = ({navigation}) => (
   <AppContainer title = "" onPress = {goBack (navigation)}>
     <CardJobDetail item = {navigation.state.params} />
   </AppContainer>
)

export { JobDetail }
Enter fullscreen mode Exit fullscreen mode

JobAdd

Create a JobAdd screen
src/screens/Jobs/JobAdd.js

jobadd

import React, { useRef, useEffect, useState } from 'react'
import t from 'tcomb-form-native'
import { useMutation } from 'aws-amplify-react-hooks'
import { AppContainer, Card, Button, Space, TextLink } from '../../components'
import { structJob, options } from '../Authenticator/Form'
import { goBack, PINK } from '../../constants'
import { createJob, updateJob, deleteJob } from '../../graphql/mutations'

const Form = t.form.Form // eslint-disable-line

const JobAdd = ({ navigation }) => {
  const [check, setOwner] = useState(false)
  const [input, setJob] = useState({
    position: '',
    rate: '',
    description: ''
  })

  const onChange = item => setJob(item)

  useEffect(() => {
    const obj = navigation.state.params
    typeof obj !== 'undefined' && setOwner(true)
    setJob(obj)
  }, [navigation])

  const [setCreate, setUpdate, setDelete, { loading, error }] = useMutation(input)

  const onCreate = async () => (await setCreate(createJob)) && goBack(navigation)()
  const onUpdate = async () => (await setUpdate(updateJob)) && goBack(navigation)()
  const onDelete = async () => (await setDelete(deleteJob)) && goBack(navigation)()

  const registerForm = useRef('')
  return (
    <AppContainer loading={loading} message={error} title="Add" onPress={goBack(navigation)}>
      <Card>
        <Form ref={registerForm} type={structJob} options={options} value={input} onChange={text => onChange(text)} />
        <Space height={40} />
        <Button title="DONE" onPress={() => (check ? onUpdate() : onCreate())} />
        {check && (
          <>
            <TextLink title="or" />
            <Space height={10} />
            <Button title="DELETE" color={PINK} onPress={onDelete} />
          </>
        )}
        <Space />
      </Card>
    </AppContainer>
  )
}

export { JobAdd }

Enter fullscreen mode Exit fullscreen mode

useMutation

const [
  setCreate: Promise<{}>,
  setUpdate: Promise<{}>,
  setDelete: Promise<{}>
{ 
  loading: string,
  error: string
}
] = useMutation(input: {})
Enter fullscreen mode Exit fullscreen mode

setCreate setUpdate setDelete - Functions CREATE, UPDATE, DELETE

loading - Loading indicator.

error - Error.

input - Mutation value.

Navigation

Connect screens in stack navigator
src/screens/Jobs/index.js

import { createStackNavigator } from 'react-navigation-stack'
import { JobsMain } from './JobsMain'
import { JobDetail } from './JobDetail'
import { JobAdd } from './JobAdd'

const Jobs = createStackNavigator(
  {
    JOBS_MAIN: { screen: JobsMain },
    JOB_DETAIL: { screen: JobDetail },
    JOB_ADD: { screen: JobAdd }
  },
  {
    headerMode: 'none'
  }
)

Jobs.navigationOptions = ({ navigation }) => {
  let tabBarVisible = true

  if (navigation.state.index > 0) {
    tabBarVisible = false
  }

  return {
    tabBarVisible
  }
}

export { Jobs }
Enter fullscreen mode Exit fullscreen mode

For the logic of navigation, we transfer the User component from Authenticator to the screens directory, where we edit the imports

-import { AppContainer, Button } from '../../../components'
-import { goHome } from '../../../constants'
+import { AppContainer, Button } from '../../components'
+import { goHome } from '../../constants'
Enter fullscreen mode Exit fullscreen mode

and do not forget to remove the export from src/screens/Authenticator/index.js

export * from './Hello'
- export * from './User'
export * from './SignIn'
export * from './SignUp'
export * from './Forgot'
export * from './ForgotPassSubmit'
export * from './ConfirmSignUp'
Enter fullscreen mode Exit fullscreen mode

Connect all created components to src/screens/index.js

export * from './User'
export * from './Jobs'
Enter fullscreen mode Exit fullscreen mode

TabBar

Create a TabBar
src/components/TabBar/index.js

import React, { Component } from 'react'
import { View, StyleSheet, TouchableWithoutFeedback } from 'react-native'
import { ButtonTab } from './ButtonTab'
import { Device } from '../../constants'

const styles = StyleSheet.create({
  tabbar: {
    ...Device.select({
      iphone5: {
        height: 70
      },
      mi5: {
        height: 70
      },
      iphone678: {
        height: 100
      },
      googlePixel: {
        height: 90
      },
      redmiNote5: {
        height: 100
      }
    }),
    flexDirection: 'row',
    backgroundColor: '#0F0F0F',
    borderTopColor: '#0F0F0F',
    overflow: 'hidden',
    borderTopWidth: 1,
    paddingTop: 5,
    paddingBottom: 10
  },
  tab: {
    alignItems: 'center',
    justifyContent: 'center',
    flex: 1
  }
})

const MainMenuIcons = ['user-secret', 'home']

class TabBar extends Component {
  render() {
    const { navigation, jumpTo, activeTintColor, inactiveTintColor } = this.props
    const { routes } = navigation.state
    const { tabbar, tab } = styles

    return (
      <View style={tabbar}>
        {routes.map((route, index) => {
          const { key } = route
          const focused = index === navigation.state.index
          const textColor = focused ? activeTintColor : inactiveTintColor
          return (
            <TouchableWithoutFeedback key={`${key}`} onPress={() => jumpTo(key)}>
              <View style={tab}>
                <ButtonTab icon={MainMenuIcons[index]} tintColor={textColor} />
              </View>
            </TouchableWithoutFeedback>
          )
        })}
      </View>
    )
  }
}
export { TabBar }
Enter fullscreen mode Exit fullscreen mode

ButtonTab

Create a component ButtonTab
src/components/TabBar/ButtonTab.js

// @flow
import React, { memo } from 'react'
import Fontisto from 'react-native-vector-icons/Fontisto'

type Props = {
  icon: 'user-secret' | 'home',
  tintColor: string,
  mainTab: boolean
}

const ButtonTab = memo<Props>(({ icon, tintColor }) => <Fontisto name={icon} size={35} color={tintColor} />)

export { ButtonTab }
Enter fullscreen mode Exit fullscreen mode

Add export to src/components/index.js

export * from './Localei18n'
export * from './AppContainer'
export * from './Header'
export * from './Loading'
export * from './Space'
export * from './Button'
export * from './TextLink'
export * from './TextError'
export * from './Input'
+export * from './TabBar'
+export * from './CardJob'
+export * from './CardJobDetail'
+export * from './Card'
Enter fullscreen mode Exit fullscreen mode

AppNavigator

Editing the navigation configuration file for our custom authentication src/AppNavigator.js

import { createAppContainer } from 'react-navigation'
import { createStackNavigator } from 'react-navigation-stack'
import { createBottomTabNavigator } from 'react-navigation-tabs'
import { ConfirmSignUp, Hello, SignIn, SignUp, Forgot, ForgotPassSubmit } from './screens/Authenticator'
import { User, Jobs } from './screens'
import { TabBar } from './components'
import { PURPLE } from './constants'

const TabNavigator = {
  screen: createBottomTabNavigator(
    {
      JOBS: { screen: Jobs },
      USER: { screen: User }
    },
    {
      initialRouteName: 'JOBS',
      tabBarComponent: TabBar,
      tabBarPosition: 'bottom',
      animationEnabled: true,
      lazy: true,
      backBehavior: 'initialRoute',
      tabBarOptions: {
        showLabel: false,
        activeTintColor: PURPLE,
        inactiveTintColor: '#390032'
      }
    }
  )
}

const AppNavigator = createStackNavigator(
  {
    HELLO: { screen: Hello },
    SIGN_IN: { screen: SignIn },
    SIGN_UP: { screen: SignUp },
    FORGOT: { screen: Forgot },
    CONFIRM_SIGN_UP: { screen: ConfirmSignUp },
    FORGOT_PASSWORD_SUBMIT: { screen: ForgotPassSubmit },
    MAIN: TabNavigator
  },
  {
    initialRouteName: 'HELLO',
    headerMode: 'none',
    defaultNavigationOptions: {
      gesturesEnabled: false
    }
  }
)

export default createAppContainer(AppNavigator)
Enter fullscreen mode Exit fullscreen mode

Build the application and test

Done✅

REFERENCES:

  1. AWS Amplify

  2. Fullstack Serverless

  3. Core Features, Architecture, Pros and Cons

  4. graphql.org

💖 💪 🙅 🚩
serverlesskiy
Server Serverlesskiy

Posted on December 10, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related