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.

format_list_bulleted
help_outline