React Native

Using Cloud Functions in a React Native App

Introduction

In this guide, you will learn how to use the Parse Cloud Code Functions from a React Native App. You will see examples of triggers implemented using cloud functinos, and also check how to implement a React Native component using these functions and Back4App.

Prerequisites

To complete this tutorial, you will need:

Goal

Run Parse Cloud Code on Back4App from a React Native App.

Step 1 - Understanding Cloud Code Functions

In your applications, there will be times that you will need to perform certain data operations or heavy processing that shouldn’t be done on mobile devices. For these cases, you may use Cloud Code functions, that run directly on your Parse server and are called using Parse API. Note that this also enables changing some part of your app’s logic without needing to release a new version on the app stores, which can be useful in some cases.

There are two main types of Cloud Code functions, generic functions, using define and holding any type of code you want, and triggers, that are fired when certain events occur in your Parse server automatically.

In the next step, you will be presented with examples for most of these function types, which can be created and deployed directly in your Back4App dashboard or via CLI. You can check how to perform this in our Cloud functions starter guide.

Step 2 - Cloud Code Reference

Cloud Functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// This a generic cloud function that may contain any type of operation,
// these should be called via API in your app to be run
Parse.Cloud.define("getAverageMovieReviews", async (request) => {
  const query = new Parse.Query("Review");
  // Parameters can be passed in your request body and are accessed inside request.params
  query.equalTo("movie", request.params.movie);
  const results = await query.find();
  let sum = 0;
  for (let review of results) {
    sum += review.get("stars");
  }
  return {
    result: sum / results.length,
  }
});

Response:

1
{ "result": 3.5 }

Save Triggers

1
2
3
4
5
6
7
8
9
10
// This will run every time an object of this class will be saved on the server
Parse.Cloud.beforeSave("Product", (request) => {
  // You can change any data before saving, can be useful for trimming text
  // or validating unique fields
  const name = request.object.get("description");
  if (description.length > 140) {
    // Truncate and add a ...
    request.object.set("description", comment.substring(0, 137) + "...");
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// This will always run after saving an object of this class
Parse.Cloud.afterSave("Comment", async (request) => {
  // You can use this method for changing other classes, like handling
  // a Comment counter for a Post
  const query = new Parse.Query("Post");
  try {
    let post = await query.get(request.object.get("post").id);
    post.increment("comments");
    return post.save();
  } catch (error) {
    console.error("Got an error " + error.code + " : " + error.message);
  }
});

Delete Triggers

1
2
3
4
5
6
7
8
9
10
11
12
// This will be called before deleting an object, useful for checking
// for existing dependent conditions
Parse.Cloud.beforeDelete("Album", async (request) => {
  // This won't proceed deleting if the condition is not met, protection your
  // data
  const query = new Parse.Query("Photo");
  query.equalTo("album", request.object);
  const count = await query.count();
  if (count > 0) {
    throw "Can't delete album if it still has photos.";
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// Called after deleting an object, useful for cascade deleting related
// objects
Parse.Cloud.afterDelete("Post", async (request) => {
  // Delete all post Comments after deleting the Post
  const query = new Parse.Query("Comment");
  query.equalTo("post", request.object);
  try {
    let results = await query.find();
    await Parse.Object.destroyAll(results);
  } catch (error) {
    console.error("Error finding related comments " + error.code + ": " + error.message);
  }
});

File Triggers

1
2
3
4
5
6
7
8
// Can be called to change any file properties, like renaming it
Parse.Cloud.beforeSaveFile(async (request) => {
  const { file } = request;
  const fileData = await file.getData();
  // Note that the new file will be saved instead of the user submitted one
  const newFile = new Parse.File('a-new-file-name.txt', { base64: fileData });
  return newFile;
});
1
2
3
4
5
6
7
8
9
10
11
// Can be called for managing file metadata and creating
// your file referencing object
Parse.Cloud.afterSaveFile(async (request) => {
  const { file, fileSize, user } = request;
  const fileObject = new Parse.Object('FileObject');
  fileObject.set('file', file);
  fileObject.set('fileSize', fileSize);
  fileObject.set('createdBy', user);
  const token = { sessionToken: user.getSessionToken() };
  await fileObject.save(null, token);
});
1
2
3
4
5
6
7
8
9
10
11
// Useful for checking conditions in objects using the file to be deleted
Parse.Cloud.beforeDeleteFile(async (request) => {
  // Only deletes file if the object storing it was also deleted before
  const { file, user } = request;
  const query = new Parse.Query('FileObject');
  query.equalTo('fileName', file.name());
  const fileObjectCount = await query.count({ useMasterKey: true });
  if (fileObjectCount > 0) {
    throw 'The FileObject should be delete first!';
  }
});
1
2
3
4
5
6
7
8
9
10
// Useful for cleaning up objects related to the file
Parse.Cloud.afterDeleteFile(async (request) => {
  // Note that this example is the opposite of the beforeDeleteFile one,
  // this one will clean up FileObjects after deleting the file
  const { file } = request;
  const query = new Parse.Query('FileObject');
  query.equalTo('fileName', file.name());
  const fileObject = await query.first({ useMasterKey: true });
  await fileObject.destroy({ useMasterKey: true });
});

Find Triggers

1
2
3
4
5
6
7
// This will be called before any queries using this class
Parse.Cloud.beforeFind('MyObject', (request) => {
  // Useful for hard setting result limits or forcing
  // certain conditions
  let query = request.query;
  query.limit(5);
});
1
2
3
4
5
6
7
// This will be called after the query is done in the database
Parse.Cloud.afterFind('MyObject', (req) => {
  // Can be used in rare cases, like force reversing result ordering
  let results = req.objects;
  results = results.reverse();
  return req.objects;
});

Session Triggers

1
2
3
4
5
6
7
8
// Can be used for checking for user permissions and state
Parse.Cloud.beforeLogin(async (request) => {
  // Doesn't allow banned users to login
  const { object: user } = request;
  if (user.get('isBanned') === true) {
    throw 'Access denied, you have been banned.';
  }
});
1
2
3
4
5
6
7
8
// Can be used for cleaning up user actions after logging out
Parse.Cloud.afterLogout(async (request) => {
  // Sets custom online flag to false
  const { object: session } = request;
  const user = session.get('user');
  user.set('isOnline', false);
  await user.save(null, { useMasterKey: true } );
});

Step 3 - Using Cloud Code from a React Native component

Let’s now create an example Parse Cloud Code function and call it inside a component in React Native, with a simple interface having a label showing the function result and also a form for adding new objects.

Our cloud function will calculate the average of the ratings of every movie Review object in our app, which is a Parse.Object class composed of text, rate and movie fields. Here is the function code:

1
2
3
4
5
6
7
8
9
10
11
12
Parse.Cloud.define("getMovieAverageRating", async (request) => {
  const query = new Parse.Query("Review");
  query.equalTo("movie", request.params.movie);
  const results = await query.find();
  let sum = 0;
  for (let review of results) {
    sum += review.get("rate");
  }
  return {
    result: sum / results.length,
  };
});

To call this cloud function in React Native, you need to use the Parse.Cloud.run method, passing as an argument the function name and also any necessary parameters inside an object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const runGetMovieAverageRating = async function () {
  try {
    const params = {
      movie: 'Mission Very Possible',
    };
    let resultObject = await Parse.Cloud.run(
      'getMovieAverageRating',
      params,
    );
    // Set query results to state variable using state hook
    setRatingsAverage(resultObject.result.toFixed(1));
    return true;
  } catch (error) {
    // Error can be caused by lack of Internet connection
    // or by not having an valid Review object yet
    Alert.alert('Error!', error.message);
    return false;
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const runGetMovieAverageRating = async function (): Promise<boolean> {
  try {
    const params: {movie: string} = {
      movie: 'Mission Very Possible',
    };
    let resultObject: {result: number} = await Parse.Cloud.run(
      'getMovieAverageRating',
      params,
    );
    // Set query results to state variable using state hook
    setRatingsAverage(resultObject.result.toFixed(1));
    return true;
  } catch (error) {
    // Error can be caused by lack of Internet connection
    // or by not having an valid Review object yet
    Alert.alert('Error!', error.message);
    return false;
  }
};

We can also enforce that the reviews’ texts are kept short by using a beforeSave trigger function for the Review object. Here is the function code:

1
2
3
4
5
6
7
Parse.Cloud.beforeSave("Review", (request) => {
  const text = request.object.get("text");
  if (text.length > 20) {
    // Truncate and add a ...
    request.object.set("text", text.substring(0, 17) + "...");
  }
});

This is how the full component code is laid out:

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
import React, {useState} from 'react';
import {
  Alert,
  Image,
  View,
  Platform,
  ScrollView,
  StyleSheet,
} from 'react-native';
import Parse from 'parse/react-native';
import {
  List,
  Title,
  TextInput as PaperTextInput,
  Button as PaperButton,
  Text as PaperText,
} from 'react-native-paper';

export const MovieRatings = () => {
  // State variable
  const [queryResults, setQueryResults] = useState(null);
  const [ratingsAverage, setRatingsAverage] = useState('');
  const [reviewText, setReviewText] = useState('');
  const [reviewRate, setReviewRate] = useState('');

  const runGetMovieAverageRating = async function () {
    try {
      const params = {
        movie: 'Mission Very Possible',
      };
      let resultObject = await Parse.Cloud.run(
        'getMovieAverageRating',
        params,
      );
      // Set query results to state variable using state hook
      setRatingsAverage(resultObject.result.toFixed(1));
      return true;
    } catch (error) {
      // Error can be caused by lack of Internet connection
      // or by not having an valid Review object yet
      Alert.alert(
        'Error!',
        'Make sure that the cloud function is deployed and that the Review class table is created',
      );
      return false;
    }
  };

  const doReviewQuery = async function () {
    // Create our query
    let parseQuery = new Parse.Query('Review');
    try {
      let results = await parseQuery.find();
      // Set query results to state variable
      setQueryResults(results);
      return true;
    } catch (error) {
      // Error can be caused by lack of Internet connection
      Alert.alert('Error!', error.message);
      return false;
    }
  };

  const createReview = async function () {
    try {
      // This values come from state variables linked to
      // the screen form fields
      const reviewTextValue = reviewText;
      const reviewRateValue = Number(reviewRate);

      // Creates a new parse object instance
      let Review = new Parse.Object('Review');

      // Set data to parse object
      // Simple title field
      Review.set('text', reviewTextValue);

      // Simple number field
      Review.set('rate', reviewRateValue);

      // Set default movie
      Review.set('movie', 'Mission Very Possible');

      // After setting the values, save it on the server
      try {
        await Review.save();
        // Success
        Alert.alert('Success!');
        // Updates query result list
        doReviewQuery();
        runGetMovieAverageRating();
        return true;
      } catch (error) {
        // Error can be caused by lack of Internet connection
        Alert.alert('Error!', error.message);
        return false;
      }
    } catch (error) {
      // Error can be caused by lack of field values
      Alert.alert('Error!', error.message);
      return false;
    }
  };

  return (
    <>
      <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}>
          <PaperText style={Styles.header_text_bold}>
            {'React Native on Back4App - '}
          </PaperText>
          {' Cloud Code Movie Ratings'}
        </PaperText>
      </View>
      <ScrollView style={Styles.wrapper}>
        <View>
          <Title>{'Mission Very Possible Reviews'}</Title>
          <PaperText>{`Ratings Average: ${ratingsAverage}`}</PaperText>
          {/* Query list */}
          {queryResults !== null &&
            queryResults !== undefined &&
            queryResults.map((result) => (
              <List.Item
                key={result.id}
                title={`Review text: ${result.get('text')}`}
                description={`Rate: ${result.get('rate')}`}
                titleStyle={Styles.list_text}
                style={Styles.list_item}
              />
            ))}
          {queryResults === null ||
          queryResults === undefined ||
          (queryResults !== null &&
            queryResults !== undefined &&
            queryResults.length <= 0) ? (
            <PaperText>{'No results here!'}</PaperText>
          ) : null}
        </View>
        <View>
          <Title>Action Buttons</Title>
          <PaperButton
            onPress={() => runGetMovieAverageRating()}
            mode="contained"
            icon="search-web"
            color={'#208AEC'}
            style={Styles.list_button}>
            {'Calculate Review Average'}
          </PaperButton>
          <PaperButton
            onPress={() => doReviewQuery()}
            mode="contained"
            icon="search-web"
            color={'#208AEC'}
            style={Styles.list_button}>
            {'Query Reviews'}
          </PaperButton>
        </View>
        <View>
          <Title>Add new review</Title>
          <PaperTextInput
            value={reviewText}
            onChangeText={text => setReviewText(text)}
            label="Text"
            mode="outlined"
            style={Styles.form_input}
          />
          <PaperTextInput
            value={reviewRate}
            onChangeText={text => setReviewRate(text)}
            keyboardType={'number-pad'}
            label="Rate (1-5)"
            mode="outlined"
            style={Styles.form_input}
          />
          <PaperButton
            onPress={() => createReview()}
            mode="contained"
            icon="plus"
            style={Styles.submit_button}>
            {'Add'}
          </PaperButton>
        </View>
      </ScrollView>
    </>
  );
};

// These define the screen component styles
const Styles = StyleSheet.create({
  header: {
    alignItems: 'center',
    paddingTop: 30,
    paddingBottom: 50,
    backgroundColor: '#208AEC',
  },
  header_logo: {
    height: 50,
    width: 220,
    resizeMode: 'contain',
  },
  header_text: {
    marginTop: 15,
    color: '#f0f0f0',
    fontSize: 16,
  },
  header_text_bold: {
    color: '#fff',
    fontWeight: 'bold',
  },
  wrapper: {
    width: '90%',
    alignSelf: 'center',
  },
  list_button: {
    marginTop: 6,
    marginLeft: 15,
    height: 40,
  },
  list_item: {
    borderBottomWidth: 1,
    borderBottomColor: 'rgba(0, 0, 0, 0.12)',
  },
  list_text: {
    fontSize: 15,
  },
  form_input: {
    height: 44,
    marginBottom: 16,
    backgroundColor: '#FFF',
    fontSize: 14,
  },
  submit_button: {
    width: '100%',
    maxHeight: 50,
    alignSelf: 'center',
    backgroundColor: '#208AEC',
  },
});
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
import React, {FC, ReactElement, useState} from 'react';
import {
  Alert,
  Image,
  View,
  Platform,
  ScrollView,
  StyleSheet,
} from 'react-native';
import Parse from 'parse/react-native';
import {
  List,
  Title,
  TextInput as PaperTextInput,
  Button as PaperButton,
  Text as PaperText,
} from 'react-native-paper';

export const MovieRatings: FC<{}> = ({}): ReactElement => {
  // State variable
  const [queryResults, setQueryResults] = useState(null);
  const [ratingsAverage, setRatingsAverage] = useState('');
  const [reviewText, setReviewText] = useState('');
  const [reviewRate, setReviewRate] = useState('');

  const runGetMovieAverageRating = async function (): Promise<boolean> {
    try {
      const params: {movie: string} = {
        movie: 'Mission Very Possible',
      };
      let resultObject: {result: number} = await Parse.Cloud.run(
        'getMovieAverageRating',
        params,
      );
      // Set query results to state variable using state hook
      setRatingsAverage(resultObject.result.toFixed(1));
      return true;
    } catch (error) {
      // Error can be caused by lack of Internet connection
      // or by not having an valid Review object yet
      Alert.alert(
        'Error!',
        'Make sure that the cloud function is deployed and that the Review class table is created',
      );
      return false;
    }
  };

  const doReviewQuery = async function (): Promise<boolean> {
    // Create our query
    let parseQuery: Parse.Query = new Parse.Query('Review');
    try {
      let results: [Parse.Object] = await parseQuery.find();
      // Set query results to state variable
      setQueryResults(results);
      return true;
    } catch (error) {
      // Error can be caused by lack of Internet connection
      Alert.alert('Error!', error.message);
      return false;
    }
  };

  const createReview = async function (): Promise<boolean> {
    try {
      // This values come from state variables linked to
      // the screen form fields
      const reviewTextValue: string = reviewText;
      const reviewRateValue: number = Number(reviewRate);

      // Creates a new parse object instance
      let Review: Parse.Object = new Parse.Object('Review');

      // Set data to parse object
      // Simple title field
      Review.set('text', reviewTextValue);

      // Simple number field
      Review.set('rate', reviewRateValue);

      // Set default movie
      Review.set('movie', 'Mission Very Possible');

      // After setting the values, save it on the server
      try {
        await Review.save();
        // Success
        Alert.alert('Success!');
        // Updates query result list
        doReviewQuery();
        runGetMovieAverageRating();
        return true;
      } catch (error) {
        // Error can be caused by lack of Internet connection
        Alert.alert('Error!', error.message);
        return false;
      }
    } catch (error) {
      // Error can be caused by lack of field values
      Alert.alert('Error!', error.message);
      return false;
    }
  };

  return (
    <>
      <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}>
          <PaperText style={Styles.header_text_bold}>
            {'React Native on Back4App - '}
          </PaperText>
          {' Cloud Code Movie Ratings'}
        </PaperText>
      </View>
      <ScrollView style={Styles.wrapper}>
        <View>
          <Title>{'Mission Very Possible Reviews'}</Title>
          <PaperText>{`Ratings Average: ${ratingsAverage}`}</PaperText>
          {/* Query list */}
          {queryResults !== null &&
            queryResults !== undefined &&
            queryResults.map((result: Parse.Object) => (
              <List.Item
                key={result.id}
                title={`Review text: ${result.get('text')}`}
                description={`Rate: ${result.get('rate')}`}
                titleStyle={Styles.list_text}
                style={Styles.list_item}
              />
            ))}
          {queryResults === null ||
          queryResults === undefined ||
          (queryResults !== null &&
            queryResults !== undefined &&
            queryResults.length <= 0) ? (
            <PaperText>{'No results here!'}</PaperText>
          ) : null}
        </View>
        <View>
          <Title>Action Buttons</Title>
          <PaperButton
            onPress={() => runGetMovieAverageRating()}
            mode="contained"
            icon="search-web"
            color={'#208AEC'}
            style={Styles.list_button}>
            {'Calculate Review Average'}
          </PaperButton>
          <PaperButton
            onPress={() => doReviewQuery()}
            mode="contained"
            icon="search-web"
            color={'#208AEC'}
            style={Styles.list_button}>
            {'Query Reviews'}
          </PaperButton>
        </View>
        <View>
          <Title>Add new review</Title>
          <PaperTextInput
            value={reviewText}
            onChangeText={text => setReviewText(text)}
            label="Text"
            mode="outlined"
            style={Styles.form_input}
          />
          <PaperTextInput
            value={reviewRate}
            onChangeText={text => setReviewRate(text)}
            keyboardType={'number-pad'}
            label="Rate (1-5)"
            mode="outlined"
            style={Styles.form_input}
          />
          <PaperButton
            onPress={() => createReview()}
            mode="contained"
            icon="plus"
            style={Styles.submit_button}>
            {'Add'}
          </PaperButton>
        </View>
      </ScrollView>
    </>
  );
};

// These define the screen component styles
const Styles = StyleSheet.create({
  header: {
    alignItems: 'center',
    paddingTop: 30,
    paddingBottom: 50,
    backgroundColor: '#208AEC',
  },
  header_logo: {
    height: 50,
    width: 220,
    resizeMode: 'contain',
  },
  header_text: {
    marginTop: 15,
    color: '#f0f0f0',
    fontSize: 16,
  },
  header_text_bold: {
    color: '#fff',
    fontWeight: 'bold',
  },
  wrapper: {
    width: '90%',
    alignSelf: 'center',
  },
  list_button: {
    marginTop: 6,
    marginLeft: 15,
    height: 40,
  },
  list_item: {
    borderBottomWidth: 1,
    borderBottomColor: 'rgba(0, 0, 0, 0.12)',
  },
  list_text: {
    fontSize: 15,
  },
  form_input: {
    height: 44,
    marginBottom: 16,
    backgroundColor: '#FFF',
    fontSize: 14,
  },
  submit_button: {
    width: '100%',
    maxHeight: 50,
    alignSelf: 'center',
    backgroundColor: '#208AEC',
  },
});

This is how the component should look like after rendering and querying by one of the query functions:

React Native Back4App

Conclusion

At the end of this guide, you learned how Parse Cloud Code functions work and how to perform them on Back4App from a React Native App. In the next guide, you will learn how to work with Users in Parse.