CRUD (Create Read Update Delete) / AWS Amplify + GraphQL + React Native
Server Serverlesskiy
Posted on December 10, 2019
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:
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
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
}
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]} ])
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
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.
CREATE
We open our API at the address that issued (each has its own) the result of the amplify mock api
command
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
}
}
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
}
}
}
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
}
}
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
}
}
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]} ])
To change the user, click on UpdateAuth in the main menu. Where we randomly update Username and Email.
If we send a READ request, then it works, but if we send an UPDATE or DELETE request and we get an 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
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
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 }
CardJob
Create the CardJob component
src/components/CardJob/index.js
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 }
CardJobDetail
Creating the CardJobDetail Component
src/components/CardJobDetail/index.js
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 }
InputMuliline
Create InputMuliline
Edit src/screens/Authenticator/Form/index.js
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
+ }
}
}
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
Screens
JobsMain
Create a JobsMain screen
src/screens/Jobs/JobsMain.js
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 }
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>)
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
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 }
JobAdd
Create a JobAdd screen
src/screens/Jobs/JobAdd.js
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 }
useMutation
const [
setCreate: Promise<{}>,
setUpdate: Promise<{}>,
setDelete: Promise<{}>
{
loading: string,
error: string
}
] = useMutation(input: {})
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 }
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'
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'
Connect all created components to src/screens/index.js
export * from './User'
export * from './Jobs'
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 }
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 }
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'
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)
Build the application and test
Done✅
REFERENCES:
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
December 10, 2019