Chapter 8: Client CRUD
Full Stack TypeScript
link Client CRUD
By the end of this lesson, developers will be able to:
- Create, update and destroy Places
Introduction
Now that we have authenticated users, we can include Full CRUD functionality.
Setting Up
Let's start by opening and running both projects in two separate windows.
cd <app-name>
yarn start
cd <server-name>
yarn start
Once you have confirmed both projects are running, we will add a client to the React Native project.
Heads Up! This lesson's code edits will only affect your React Native project.
link Generate Mutations
Finishing up with Update and Destroy, let's create their mutations.
Create a new file called graphql/updatePlace.graphql
:
mutation UpdatePlace(
$id: String
$title: String
$description: String
$imageUrl: String
) {
updatePlace(
place: {
id: $id
title: $title
description: $description
imageUrl: $imageUrl
}
) {
id
title
description
imageUrl
creationDate
}
}
Create a new file called graphql/deletePlace.graphql
:
mutation DeletePlace($id: Float!) {
deletePlace(id: $id)
}
Now let's try to generate the mutation hooks! With the server running, go ahead and re-generate the GraphQL queries:
yarn generate
Your new graphql/graphql-hooks.ts
file should include new hooks for our project.
link PlaceForm
Let's integrate the new hooks with a new PlaceForm
screen.
Create a new file called src/screens/PlaceForm.tsx
:
import React, { useState } from 'react';
import { View, StyleSheet, Dimensions, ScrollView } from 'react-native';
import { useRoute } from '@react-navigation/native';
import {
useCreatePlaceMutation,
useUpdatePlaceMutation,
useDeletePlaceMutation
} from '../../graphql';
import { Button, TextInput, useTheme } from 'react-native-paper';
const { width } = Dimensions.get('window');
export default function LoginScreen(props) {
const theme = useTheme();
const route = useRoute();
const { navigation } = props;
const { item } = route.params as any;
const [title, setTitle] = useState(item.title || '');
const [description, setDescription] = useState(item.description || '');
const [imageUrl, setImageUrl] = useState(item.imageUrl || '');
// Create Place
const [createPlaceMutation] = useCreatePlaceMutation({
async onCompleted({ createPlace }) {
navigation.goBack();
}
});
// Update Place
const [updatePlaceMutation] = useUpdatePlaceMutation({
async onCompleted({ updatePlace }) {
navigation.navigate('Detail', { item: updatePlace });
}
});
// Delete Place
const [deletePlaceMutation] = useDeletePlaceMutation({
async onCompleted(id) {
navigation.navigate('Profile');
}
});
return (
<ScrollView
contentContainerStyle={[
styles.container,
{ backgroundColor: theme.colors.background }
]}
>
<TextInput
onChangeText={text => setTitle(text)}
value={title}
placeholder="Title"
label="Title"
mode="outlined"
autoCorrect={false}
autoCapitalize="none"
style={styles.input}
/>
<TextInput
onChangeText={text => setDescription(text)}
value={description}
placeholder="Description"
label="Description"
mode="outlined"
autoCorrect={false}
autoCapitalize="none"
style={styles.input}
/>
<TextInput
onChangeText={text => setImageUrl(text)}
value={imageUrl}
placeholder="Image URL"
label="Image URL"
mode="outlined"
autoCorrect={false}
autoCapitalize="none"
style={styles.input}
/>
<View style={styles.buttonContainer}>
<Button
labelStyle={{ color: theme.colors.text }}
style={{
backgroundColor: theme.colors.accent,
marginTop: 20
}}
onPress={() =>
item.id
? updatePlaceMutation({
variables: {
id: parseInt(item.id),
title,
description,
imageUrl
}
})
: createPlaceMutation({
variables: { title, description, imageUrl }
})
}
>
Save Place
</Button>
<Button
labelStyle={{ color: theme.colors.text }}
style={{
backgroundColor: theme.colors.accent,
marginTop: 20
}}
onPress={() =>
deletePlaceMutation({ variables: { id: parseInt(item.id) } })
}
>
Delete Place
</Button>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-start',
alignItems: 'center',
paddingHorizontal: 15,
paddingVertical: 20
},
input: {
width: width - 40,
height: 60,
marginTop: 5
},
buttonContainer: {
width: '100%'
}
});
We will now export the new screen inside src/screens/index.ts
:
import AuthLoading from './AuthLoading';
import Login from './LoginScreen';
import Profile from './ProfileScreen';
+import Form from './PlaceForm';
-export { Places, PlaceDetail, AuthLoading, Login, Profile };
+export { Places, PlaceDetail, AuthLoading, Login, Profile, Form };
link Routing PlaceForm
Let's give users a chance to edit places inside src/screens/PlaceDetail.tsx
:
+import { Button } from 'react-native-paper';
{/* ... */}
const PlaceDetail: React.FC<Props> = props => {
const route = useRoute();
const { item } = route.params as any;
+const { navigation } = props;
return (
<SafeAreaView>
<CardView {...(item as any)} />
+ <Button
+ style={{
+ marginTop: 20
+ }}
+ onPress={() => {
+ navigation.navigate('Form', { item });
+ }}
+ >
+ Edit Place
+ </Button>
</SafeAreaView>
);
};
Let's also route to the PlaceForm
inside src/navigation/MainTabNavigator.tsx
:
-import { PlaceDetail, AuthLoading, Login, Profile } from '../screens';
+import { PlaceDetail, AuthLoading, Login, Profile, Form } from '../screens';
{/* ... */}
<Stack.Screen name="Places" component={Places} />
<Stack.Screen name="Detail" component={PlaceDetail} />
+ <Stack.Screen name="Form" component={Form} />
{/* ... */}
-export const MainTabNavigator = () => {
+export const MainTabNavigator = ({ navigation }) => {
{/* ... */}
<FAB
visible={isFocused}
icon="feather"
- onPress={() => console.log('pressed FAB')}
+ onPress={() => navigation.navigate('Form', { item: {} })}
Try creating updating and deleting a place. If everything works, congratulations!
WELL DONE!
Wrapping Up
Today we added Full CRUD to the places app. Anyone can now create, update and destroy their favorite Places. Up next, we will cover subscriptions and deployment.