React Native

React Native GraphQL CRUD tutorial

Introduction

Managing data on Back4App using GraphQL is a powerful option for any type of application, speeding up queries and simplifying the most complex ones. Back4App uses common conventions for GraphQL configuration and provides great tools to help your environment setup for development.

In this guide, you will learn how to perform basic data operations through a CRUD example app, which will show you how to create, read, update and delete data from your Parse server database in React Native using GraphQL and Relay.

You will first create your component functions for each CRUD operation, using them later in a complete screen layout, resulting in a to-do list app.

At any time, you can access this project via our GitHub repositories to checkout the styles and complete code.

Prerequisites

  • For this tutorial we will use the Parse Server in the 4.4 version. If you want to use other versions you can check the corresponding mutation code at GraphQL Logout Guide example for your respective version.
  • A React Native App created and connected to Back4App;
  • Conclude the Relay Environment setup tutorial;
  • Good understanding of Relay Store and Relay Connection Updater; you can read more on Relay Modern Docs;
  • We will be using JavaScript as the default implementation.

Goal

To build a basic CRUD application in React Native using Parse, GraphQL, and Relay.

Step 1 - Creating the Todo class

In this first step, we need to define the class that the application will be operating with. The Todo class needs only to have a title(string) field describing the task and a done(boolean) field indicating if the task is completed. You can use a generic mutation to create the class into your database if you don’t have it, following our guide in the GraphQL Cookbook. You can also create the class using Back4App’s dashboard, through the database browser itself or from the JS, or GraphQL consoles.

After creating this new class, remember to download the schema.json file from the dashboard and save it in your application inside the data directory. This is necessary for the Relay Compiler to automatically generate the types from queries, mutations, etc.

At this point, your Back4App dashboard will have automatically generate CRUD Mutations for the class object, to see them you can go to the GraphQL Console, open the docs tab, and search for Todo:

Step 2 - Querying and Rendering the Objects

Let’s now create a component that will be responsible for making the list query in the Back4App server, establishing a connection for the Objects, and automatically rendering and updating the list contents as they change.

Create a new file named TodoListQueryRenderer.js in your src directory with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React from 'react';
import { View, Text } from 'react-native';
import { graphql, QueryRenderer } from 'react-relay';

// prerequisite: properly configured relay environment
import environment from '../../relay/environment';

// This will be created in the next steps
import TodoList from './TodoList';

const TodoListQueryRenderer = () => {
    return (
        // The QueryRenderer acts as a wrapper to any code, providing query results to
        // the child components and updating them as needed
        // Note that the query is using a fragment called TodoList_query,
        // which will be created in the next steps
        <QueryRenderer
            environment={environment}
            query={graphql`
            query TodoListQueryRendererQuery {
              ...TodoList_query
            }
          `}
            variables={null}
            render={({ error, props }) => {
                if (error) {
                    return (
                        <View>
                            <Text>{error.message}</Text>
                        </View>
                    );
                } else if (props) {
                    return <TodoList query={props} />;
                }
                return (
                    <View>
                        <Text>Loading...</Text>
                    </View>
                );
            }}
        />
    );
}

export default TodoListQueryRenderer;

Step 3 - Create the List component and Fragment of Object

Let’s now create our list component, which will render the data retrieved from the QueryRenderer. The component will for now contain only a simple scroll view containing a map function rendering each node. Go ahead and create a new file in your src directory called TodoList.js and add the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import React, {useState} from 'react';
import {
  View,
  SafeAreaView,
  Image,
  ScrollView,
  StatusBar,
  StyleSheet,
  TouchableOpacity,
} from 'react-native';

import {
  List,
  Text as PaperText,
  Button as PaperButton,
  TextInput as PaperTextInput,
} from 'react-native-paper';
import {createFragmentContainer} from 'react-relay';

const TodoList = props => {
  const [newTodoTitle, setNewTodoTitle] = useState('');

  const {query} = props;
  const {todos} = query;

  const renderTodos = () => {
    if (!todos) {
      return null;
    }

    return todos.edges.map(({node: todo}) => (
      <List.Item
        key={todo.id}
        title={todo.title}
        titleStyle={todo.done ? Styles.todo_text_done : Styles.todo_text}
        style={Styles.todo_item}
        right={props => (
          <>
            {!todo.done && (
              <TouchableOpacity onPress={() => updateTodo(todo.id, true)}>
                <List.Icon {...props} icon="check" color={'#4CAF50'} />
              </TouchableOpacity>
            )}

            <TouchableOpacity onPress={() => deleteTodo(todo.id)}>
              <List.Icon {...props} icon="close" color={'#ef5350'} />
            </TouchableOpacity>
          </>
        )}
      />
    ));
  };

  return (
    <>
      <StatusBar backgroundColor="#208AEC" />
      <SafeAreaView style={Styles.container}>
        <View style={Styles.header}>
          <PaperText style={Styles.header_text_bold}>
            {'React Native on Back4App'}
          </PaperText>
          <PaperText style={Styles.header_text}>{'Product Creation'}</PaperText>
        </View>
        <View style={Styles.create_todo_container}>
        </View>
        <ScrollView style={Styles.todo_list}>{renderTodos()}</ScrollView>
      </SafeAreaView>
    </>
  );
};

const TodoListFragmentContainer = createFragmentContainer(TodoList, {
  query: graphql`
    fragment TodoList_query on Query {
      todos(first: 50) @connection(key: "TodoList_todos", filters: []) {
        edges {
          node {
            id
            title
            done
          }
        }
      }
    }
  `,
});

The Query Renderer in Step 2 expects a fragment of data named TodoList_query, so we added it to the list component as well at the end of the file. Remember that GraphQL fragments are structures responsible for calling the data the components need to render and specifying what needs to be returned.

Step 4 - Creating the Objects

The first step to manage your data in your Back4App GraphQL database is to have some on it. We need to create a function to call to create a new Todo in our list component using a mutation responsible for it.

Let’s begin with the mutation, so create a new file named CreateTodoMutation.js in the src/mutations directory containing the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { commitMutation, graphql } from "react-relay";
import { ROOT_ID, ConnectionHandler } from 'relay-runtime';

const connectionCreateEdgeUpdater = (store, nodeId) => {
  const parentProxy = store.get(ROOT_ID);
  const todoConnection = ConnectionHandler.getConnection(parentProxy, 'TodoList_todos');

  const newTodo = store.get(nodeId);
  const edge = ConnectionHandler.createEdge(store, todoConnection, newTodo, 'TodoEdge');
  
  // No cursor provided, append the edge at the end.
  ConnectionHandler.insertEdgeAfter(todoConnection, edge);
}

const mutation = graphql`
  mutation CreateTodoMutation($input: CreateTodoInput!) {
    createTodo(input: $input) {
      todo {
        id
        title
        done
      }
    }
  }
`;

function commit({ environment, input, onCompleted, onError }) {
  const variables = { input };

  commitMutation(environment, {
    mutation,
    variables,
    onCompleted,
    onError,
    updater: (store) => {
      const todoId = store.getRootField('createTodo').getLinkedRecord('todo').getValue('id');

      connectionCreateEdgeUpdater(store, todoId)
    }
  });
}

Relay Modern provides us a bunch of API endpoints to be used to update the data after mutation calls. In this function, we used some of its functions to get the new Todo created and update the already existing list on the frontend. With this approach, we avoid a new call to the backend saving time that would be spent on a new request, user internet, over fetching, and more.

Now add the following function to the TodoList component, which will call the CreateTodoMutation and will be referenced later by a button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const createTodo = () => {
    const input = {
        fields: {
            title: newTodoTitle,
            done: false,
        },
    };

    CreateTodoMutation.commit({
        environment,
        input: input,
        onCompleted: () => {
            Alert.alert('Success!', 'Todo created!');
            setNewTodoTitle('');
        },
        onError: (errors) => {
            Alert.alert('Error!', errors);
        },
    });
}

Step 5 - Updating the Objects

Updating an object is similar to creating it, with the addition that, when you are updating an object using Relay Modern, you just need to ask the fields that you want to update with the new state on the output of the mutation.

Create a new file named UpdateTodoMutation.js in the src/mutations directory containing the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { commitMutation, graphql } from "react-relay";

const mutation = graphql`
  mutation UpdateTodoMutation($input: UpdateTodoInput!) {
    updateTodo(input: $input) {
      todo {
        id
        title
        done
      }
    }
  }
`;

function commit({ environment, input, onCompleted, onError }) {
  const variables = { input };

  commitMutation(environment, {
    mutation,
    variables,
    onCompleted,
    onError,
  });
}

export default {
  commit,
};

Just like in the previous step, add the following function to the TodoList component, which will call the UpdateTodoMutation and will be referenced later by a button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const updateTodo = (todoId, done) => {
    const input = {
        id: todoId,
        fields: {
            done,
        }
    }

    UpdateTodoMutation.commit({
        environment,
        input: input,
        onCompleted: () => {
            Alert.alert('Success!', 'Todo updated!');
        },
        onError: (errors) => {
            Alert.alert('Error!', errors);
        },
    });
}

Step 6 - Deleting the Objects

When you are deleting an object, with the Relay Store APIs you can update the list and removing the old object from the frontend. We will call an updater callback, similar to the update from createTodo, but we will pass the connection and the id of the to-do to remove it from the list. In this way, we keep the frontend faithfully updated to our server.

Create a new file named DeleteTodoMutation.js in the src/mutations directory containing the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { commitMutation, graphql } from "react-relay";
import { ROOT_ID, ConnectionHandler } from 'relay-runtime';

const connectionDeleteEdgeUpdater = (store, nodeId) => {
  const parentProxy = store.get(ROOT_ID);
  const connection = ConnectionHandler.getConnection(parentProxy, 'TodoList_todos');

  const newCount = connection.getValue('count');
  connection.setValue(newCount - 1, 'count');

  ConnectionHandler.deleteNode(connection, nodeId);
}

const mutation = graphql`
  mutation DeleteTodoMutation($input: DeleteTodoInput!) {
    deleteTodo(input: $input) {
      todo {
        id
      }
    }
  }
`;

function commit({ environment, input, onCompleted, onError }) {
  const variables = { input };

  commitMutation(environment, {
    mutation,
    variables,
    onCompleted,
    onError,
    updater: (store) => {
      connectionDeleteEdgeUpdater(store, input.id)
    }
  });
}

export default {
  commit,
};

Just like in the previous step, add the following function to the TodoList component, which will call the DeleteTodoMutation and will be referenced later by a button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const deleteTodo = (todoId) => {
    const input = {
        id: todoId,
    }

    DeleteTodoMutation.commit({
        environment,
        input: input,
        onCompleted: () => {
            Alert.alert('Success!', 'Todo deleted!');
        },
        onError: (errors) => {
            Alert.alert('Error!', errors);
        },
    });
}

Step 7 - Using CRUD in a React Native component

Let´s now complete our TodoList component code with the styled user interface elements, state variables, and calls to your CRUD functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import React, {useState} from 'react';
import {
  Alert,
  View,
  SafeAreaView,
  Image,
  ScrollView,
  StatusBar,
  StyleSheet,
  TouchableOpacity,
} from 'react-native';

import {
  List,
  Text as PaperText,
  Button as PaperButton,
  TextInput as PaperTextInput,
} from 'react-native-paper';
import {createFragmentContainer} from 'react-relay';
import CreateTodoMutation from './mutations/CreateTodoMutation';
import UpdateTodoMutation from './mutations/UpdateTodoMutation';
import DeleteTodoMutation from './mutations/DeleteTodoMutation';

import environment from '../../relay/environment';

const TodoList = props => {
  const [newTodoTitle, setNewTodoTitle] = useState('');

  const {query} = props;
  const {todos} = query;

  const createTodo = () => {
    const input = {
      fields: {
        title: newTodoTitle,
        done: false,
      },
    };

    CreateTodoMutation.commit({
      environment,
      input: input,
      onCompleted: () => {
        Alert.alert('Success!', 'Todo created!');
        setNewTodoTitle('');
      },
      onError: errors => {
        Alert.alert('Error!', errors);
      },
    });
  };

  const updateTodo = (todoId, done) => {
    const input = {
      id: todoId,
      fields: {
        done,
      },
    };

    UpdateTodoMutation.commit({
      environment,
      input: input,
      onCompleted: () => {
        Alert.alert('Success!', 'Todo updated!');
      },
      onError: errors => {
        Alert.alert('Error!', errors);
      },
    });
  };

  const deleteTodo = todoId => {
    const input = {
      id: todoId,
    };

    DeleteTodoMutation.commit({
      environment,
      input: input,
      onCompleted: () => {
        Alert.alert('Success!', 'Todo deleted!');
      },
      onError: errors => {
        Alert.alert('Error!', errors);
      },
    });
  };

  const renderTodos = () => {
    if (!todos) {
      return null;
    }

    return todos.edges.map(({node: todo}) => (
      <List.Item
        key={todo.id}
        title={todo.title}
        titleStyle={todo.done ? Styles.todo_text_done : Styles.todo_text}
        style={Styles.todo_item}
        right={props => (
          <>
            {!todo.done && (
              <TouchableOpacity onPress={() => updateTodo(todo.id, true)}>
                <List.Icon {...props} icon="check" color={'#4CAF50'} />
              </TouchableOpacity>
            )}

            <TouchableOpacity onPress={() => deleteTodo(todo.id)}>
              <List.Icon {...props} icon="close" color={'#ef5350'} />
            </TouchableOpacity>
          </>
        )}
      />
    ));
  };

  return (
    <>
      <StatusBar backgroundColor="#208AEC" />
      <SafeAreaView style={Styles.container}>
        <View style={Styles.header}>
          <Image
            style={Styles.header_logo}
            source={ {
              uri:
                'https://blog.back4app.com/wp-content/uploads/2019/05/back4app-white-logo-500px.png',
            } }
          />
          <PaperText style={Styles.header_text_bold}>
            {'React Native on Back4App'}
          </PaperText>
          <PaperText style={Styles.header_text}>{'Product Creation'}</PaperText>
        </View>
        <View style={Styles.create_todo_container}>
          {/* Todo create text input */}
          <PaperTextInput
            value={newTodoTitle}
            onChangeText={text => setNewTodoTitle(text)}
            label="New Todo"
            mode="outlined"
            style={Styles.create_todo_input}
          />
          {/* Todo create button */}
          <PaperButton
            onPress={() => createTodo()}
            mode="contained"
            icon="plus"
            color={'#208AEC'}
            style={Styles.create_todo_button}>
            {'Add'}
          </PaperButton>
        </View>
        <ScrollView style={Styles.todo_list}>{renderTodos()}</ScrollView>
      </SafeAreaView>
    </>
  );
};

const TodoListFragmentContainer = createFragmentContainer(TodoList, {
  query: graphql`
    fragment TodoList_query on Query {
      todos(first: 1000) @connection(key: "TodoList_todos", filters: []) {
        edges {
          node {
            id
            title
            done
          }
        }
      }
    }
  `,
});

const Styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFF',
  },
  wrapper: {
    width: '90%',
    alignSelf: 'center',
  },
  header: {
    alignItems: 'center',
    paddingTop: 10,
    paddingBottom: 20,
    backgroundColor: '#208AEC',
  },
  header_logo: {
    width: 170,
    height: 40,
    marginBottom: 10,
    resizeMode: 'contain',
  },
  header_text_bold: {
    color: '#fff',
    fontSize: 14,
    fontWeight: 'bold',
  },
  header_text: {
    marginTop: 3,
    color: '#fff',
    fontSize: 14,
  },
  flex_between: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  create_todo_container: {
    flexDirection: 'row',
    paddingLeft: 10,
    paddingRight: 10,
  },
  create_todo_input: {
    flex: 1,
    height: 38,
    marginBottom: 16,
    backgroundColor: '#FFF',
    fontSize: 14,
  },
  create_todo_button: {
    marginTop: 6,
    marginLeft: 15,
    height: 40,
  },
  todo_list: {
    paddingLeft: 10,
    paddingRight: 10,
  },
  todo_item: {
    borderBottomWidth: 1,
    borderBottomColor: 'rgba(0, 0, 0, 0.12)',
  },
  todo_text: {
    fontSize: 15,
  },
  todo_text_done: {
    color: 'rgba(0, 0, 0, 0.3)',
    fontSize: 15,
    textDecorationLine: 'line-through',
  },
});

export default TodoListFragmentContainer;

Before running your project, don´t forget to run yarn relay and update the Relay __generated__ types:

If your component is properly set up, you should see something like this after building and running the app:

Go ahead and add some to-dos by typing its titles in the input box one at a time and pressing the Add button. Note that after every successful creation, the createTodo function triggers the updater callback into the Mutation, refreshing your task list automatically. You should now have a sizeable to-do list like this:

You can now mark your tasks as done by clicking in the checkmark beside it, causing their done value to be updated to true and changing its icon status on the left. As said in the update function step, the Relay will update automatically the todo with the new value of the field done.

The only remaining data operation is now the delete one, which can be done by pressing on the trash can icon at the far right of your to-do list object. After successfully deleting an object, you should get an alert message like this:

Conclusion

At the end of this guide, you learned how to perform basic data operations (CRUD) with GrapqhQL and Relay Modern on React Native, while also learning the Relay Connection APIs that help us updating our frontend.