React Native, introduced at the 2015 Facebook F8 Developer Conference, lets you build native iOS apps using the same concepts found in the popular JavaScript UI library React. The same event also gave us Parse+React, which brings the React view concepts to the data layer.
Parse supports rapid development of your mobile apps by handling your application’s infrastructure needs for you. Parse services include data storage, social integration, push notifications, and analytics, along with client SDKs for various platforms such as iOS, Android, and JavaScript. Parse+React is built on top of the JavaScript SDK and provides hooks into React to make it easy to query and store data on Parse.
In this tutorial, you’ll learn more about React and to use it to build native apps. You’ll build upon the sample PropertyFinder
application from our introductory tutorial React Native: Building Apps with JavaScript. Be sure to check out that tutorial for all the React Native basics before continuing on here with integrating Parse.
Getting Started
To get started with the tutorial, download the starter project and unzip it.
This is essentially the same PropertyFinder
application with one important difference. Can you spot the difference by looking at some of the JavaScript files?
The original application used ECMAScript 6 (ES6) but the starter project for this tutorial doesn’t. This is because Parse+React relies on mixins to bring Parse functionality into React objects, which isn’t supported for ES6 classes. A future update to React that adds supports for the key observe
API will remove the need for using a mixin.
Make sure the React Native pre-requisites are installed. This should be the case if you worked through the previous tutorial.
In v8.0, React Native moved from using Node.js to io.js. If you don’t have io.js
installed, set it up via homebrew
by executing the following in a Terminal window:
brew unlink node brew install iojs brew link iojs --force |
This removes node
from your path, downloads the latest version of io.js
, and tells homebrew
to run io.js
whenever you run node
.
Next, verify that the starter project is set up correctly. Open Terminal, go to the ParseReactNative-PropertyFinder-Starter directory and execute the following command:
npm install |
Next, open PropertyFinder.xcodeproj then build and run the project. The simulator will start and display the app’s home page. Test that you’re able to search for UK listings as you did in the previous tutorial:
If everything looks good, then you’re ready to integrate Parse+React with your app.
The Parse+React Structure
The Parse+React layer sits on top of both React Native and the Parse JavaScript SDK:
It’s available via npm or GitHub.
Parse+React brings the same simplicity to your data management that React brings to your view layers. To understand how this works, consider the React component lifecycle shown below:
Parse+React mimics this flow by hooking into your component’s lifecycle. You first set up queries you want to associate with your component. Parse+React then subscribes your component to receive the query results, fetches the data in the background, passes it back to your component and triggers the component’s render cycle like so:
Co-locating the Parse query in your component with the view makes it much easier to understand your code. You can look at your component code and see exactly how the queries tie into the view.
Modeling Your Property Data
In this tutorial you’ll take out the calls to the Nestoria API and replace them with calls to Parse. In the next few sections, you’ll see how to set up Parse to do this.
Creating Your Parse App
The first thing you should do is sign up for a free Parse account if you haven’t done so. Next, go to the Parse Dashboard and click Create a new App:
Name your app PropertyFinder
, then click Create. You should see a success note as well as a link to grab your Parse application keys. Click that link and note your Application ID
and JavaScript Key
. You’ll need these later.
Click Core from the top menu to go to the Data Browser view, where you can see any data stored on Parse for your app. You should see a page stating that you have no classes to display. You’re going to take care of that by creating dummy data to represent property listings that you’ll pull into your app later on in the tutorial.
Defining your Schema
You can use the data displayed in the current PropertyFinder
app to figure out what your schema should be.
Open SearchResults.js and examine the renderRow
function. Look for the fields from the Nestoria API that display the data. Next, open PropertyView.js and look at the render
function to determine if there’s any additional information you’ll need for your schema.
When you’re done, your list of required data elements should match the following:
img_url
price_formatted
title
property_type
bedroom_number
bathroom_number
summary
Now you need to create a class in Parse with these fields to represent a property listing. In your Parse Data Browser, click Add Class and name your class Listing
:
Once you click Create Class, you should see a new Listing
class added to the left-hand side. There are other types of classes you can create, such as User
, which has some special methods and properties not present in a custom class.
However, your custom class will serve the needs of your app just fine. Think of a Parse class as a database table; the columns you’ll define next are similar in concept to database columns.
Click + Col to add a new column:
Select File from the type selection, enter img_url
, then click Create Column:
You should see a new column appear in the header of your class. Parse supports many data types including string, number, boolean, and binary data. Here you’re using the File
type to store binary data that represents a property’s image.
Next, add a column to represent the price. To keep things simple, instead of naming the column price_formatted
name it price
, and select Number
for the column type.
Now carry on and create columns for the rest of the fields as follows:
title
: TypeString
property_type
: TypeString
bedroom_number
: TypeNumber
bathroom_number
: TypeNumber
summary
: TypeString
Verify that all the columns and their types look like the ones shown below:
You may have noticed some starter columns were already there when you added the class. Here’s what those are for:
objectId
: uniquely identifies a row. This ID is auto-generated.createdAt
: contains the current timestamp when you add a new row.updatedAt
: the time you modified a row.ACL
: contains the permission information for a row. ACL stands for “access control list”.
Adding Some Sample Data
You’re almost ready to touch some actual code — oh the anticipation! :] But you’ll need to add some data to work with first.
You can download some sample property photos in this zip file. Download and unzip the file; the photos are contained in the Media
directory.
Still within the Data Browser on the Parse site, click + Row or + Add a row. Double-click inside the new row’s img_url
column to upload a photo. The label should change from undefined
to Upload File
as shown below:
Click Upload File, browse to house1.jpeg, then click Open. The Data Browser should now show a new row with img_url
set:
You should also see the objectId
, createdAt
, updatedAt
and ACL
columns set appropriately. By default, the ACL
permission is set to public read and write.
Click Security and change the Listing
class permission to public read only:
Click Save CLP. Note that the class level permission will supercede an individual row’s permission setting.
Note: There are many options you can use to secure your data. You can learn more from this series of blog posts from Parse.
Continue filling in data for this new row as follows:
price
: 390000title
: Grand mansionproperty_type
: housebedroom_number
: 5bathroom_number
: 4summary
: Luxurious home with lots of acreage.
Armed with this pricely listing, you’re ready to modify your app and test your Parse setup.
Swapping in Parse Calls
It’s finally time to get your hands on the code! You’ll start by retrieving all listings on Parse, without any filtering to begin.
Modifying the Query Logic
Open package.json and add the following two new dependencies:
{ "name": "PropertyFinder", "version": "0.0.1", "private": true, "scripts": { "start": "node_modules/react-native/packager/packager.sh" }, "dependencies": { "react-native": "^0.8.0", "parse": "^1.5.0", "parse-react": "^0.4.2" } } |
Don’t forget to add a comma (,) to the end of the react-native
dependency. With this change, you’ve added Parse and Parse+React to your list of dependencies.
Use Terminal to navigate to your project’s main directory and execute the following command:
npm install |
This should pull in the dependencies you just added. You should some output similar to the following:
parse-react@0.4.2 node_modules/parse-react parse@1.5.0 node_modules/parse └── xmlhttprequest@1.7.0 |
Next, you’ll initialize Parse and add your credentials to the app.
Open index.ios.js and add the following line beneath the other require
statements, but before the destructuring assignment of AppRegistry
and StyleSheet
:
var Parse = require('parse').Parse; |
This loads the Parse module and assigns it to Parse
.
Add the following code after the destructuring assignment:
Parse.initialize( 'YOUR_PARSE_APPLICATION_ID', 'YOUR_PARSE_JAVASCRIPT_KEY' ); |
Replace YOUR_PARSE_APPLICATION_ID
with your Parse application ID and YOUR_PARSE_JAVASCRIPT_KEY
with your Parse JavaScript key. You did write down your Parse application ID and JavaScript key, didn’t you? :] If not, you can always go to the Parse Dashboard and look at the Settings page for your app to find them again.
Open SearchPage.js to make your query logic changes. Add the following code near the top of the file, beneath the React require
statement:
var Parse = require('parse').Parse; var ParseReact = require('parse-react'); |
This loads the Parse and Parse+React modules and assigns them to Parse
and ParseReact
respectively.
Next, update the SearchPage
declaration to add the Parse+React mixin to the component, just above getInitialState
:
var SearchPage = React.createClass({ mixins: [ParseReact.Mixin], |
A React mixin is a way to share functionality across disparate components. It’s especially useful when you want to hook into a component’s lifecycle. For example, a mixin could define a componentDidMount
method. If a component adds this mixin, React will call the mixin’s componentDidMount
hook as well as the component’s componentDidMount
method.
ParseReact.Mixin
adds lifecycle hooks into a component when it’s mounted or it’s about to update. The mixin looks for an observe
method where the Parse queries of interest are defined.
Add the following method to your component after the getInitialState
definition:
observe: function(props, state) { var listingQuery = (new Parse.Query('Listing')).ascending('price'); return state.isLoading ? { listings: listingQuery } : null; }, |
This sets up a Parse.Query
for Listing
data and adds a query filter to sort the results by least expensive first. This query executes whenever isLoading
is true
— which is the case whenever you initiate a search.
The results from Parse.Query
will be attached to this.data.listings
based on the key — listings
— that’s paired with the listingQuery
query.
Modify _executeQuery
as shown below to only set the loading flag for now, rather than perform the call to the server:
_executeQuery: function() { this.setState({ isLoading: true }); }, |
Next, modify onSearchPressed
to call _executeQuery
:
onSearchPressed: function() { this._executeQuery(); }, |
Right now you’re not using the search term and loading all records instead; you’ll add this later on in the tutorial.
In a similar fashion, modify onLocationPressed
to call _executeQuery
as follows:
onLocationPressed: function() { navigator.geolocation.getCurrentPosition( location => { this._executeQuery(); }, error => { this.setState({ message: 'There was a problem with obtaining your locaton: ' + error }); }); }, |
Again, you’re calling `_executeQuery()` without using the location information just yet.
To clean up after yourself, delete _handleResponse
and urlForQueryAndPage
since you have no more need for these response handlers. Ahh, deleting code is so satisfying, isn’t it? :]
This is a good point to test your fetching logic. ParseReact.Mixin
forces a re-rendering of your component whenever the results return.
Add the following statement to render
just after the point where you set up the spinner:
console.log(this.data.listings); |
This logs the listing data each time you render the component — including after you run the query.
Close the React Native packager window if it’s running so you can start afresh.
Open PropertyFinder.xcodeproj and build and run; the simulator will start and display the same UI you know and love from the original app:
Tap Go and check the Xcode console. You should see some output like this:
2015-06-05 10:14:27.028 [info][tid:com.facebook.React.JavaScript] [] 2015-06-05 10:14:27.589 [info][tid:com.facebook.React.JavaScript] [{"id":{"className":"Listing","objectId":"vbHwqDH6n5"},"className":"Listing","objectId":"vbHwqDH6n5", "createdAt":"2015-06-05T16:23:14.252Z","updatedAt":"2015-06-05T16:24:39.842Z","bathroom_number":4,"bedroom_number":5, "img_url":{"_name":"tfss-30d28f46-1335-45d7-8d72-e02684c17d25-house1.jpeg", "_url":"http://files.parsetfss.com/ec34afd8-2b15-4aea-a904-c96e05b4c83a/tfss-30d28f46-1335-45d7-8d72-e02684c17d25-house1.jpeg"}, "price":390000,"property_type":"house","summary":"Luxurious home with lots of acreage.","title":"Grand mansion"}] |
The listing data is empty initially, but once you fetch the data it contains the single listing retrieved from Parse. You may also have noticed that the spinner remains once you’ve gotten the search results. This is because you haven’t done anything to properly handle the results. You’ll take care of this later on, but first you’ll take a brief detour into some UI modifications to use Parse data.
Modifying the UI
Open PropertyView.js and remove the following line in render
:
var price = property.price_formatted.split(' ')[0]; |
You no longer have to worry about reformatting price data, since now you have control over the input data.
Modify the related display code, so that instead of accessing the price
variable you just deleted, it uses the price
property:
<View style={styles.heading}> <Text style={styles.price}>${property.price}</Text> |
Also notice that you’re now in US territory, so you’ve deftly changed the currency symbol from pounds to dollars. :]
Next, modify the code to access the image’s URI
as follows:
<Image style={styles.image} source={{uri: property.img_url.url()}} /> |
You add the call to url()
to access the actual image data in Parse. Otherwise, you’d only get the string representation of the URL.
Open SearchResults.js and make a similar change in renderRow
by deleting this line:
var price = rowData.price_formatted.split(' ')[0]; |
You’ll have to modify the related display code like you did before. Since it’s now showing a dollar value, not a pound value, modify the code as follows:
<View style={styles.textContainer}> <Text style={styles.price}>${rowData.price}</Text> |
Next, update the image access as shown below:
<Image style={styles.thumb} source={{ uri: rowData.img_url.url() }} /> |
Still in SearchResults.js, change rowPressed
to check a different property for changes:
rowPressed: function(propertyGuid) { var property = this.props.listings .filter(prop => prop.id === propertyGuid)[0]; |
Parse+React identifies unique rows through an id
property; therefore you’re using this property instead of the guid
.
Similarly, change the implementation of getInitialState
to the following:
getInitialState: function() { var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1.id !== r2.id }); return { dataSource: dataSource.cloneWithRows(this.props.listings) }; }, |
Finally, modify renderRow
to use id
:
<TouchableHighlight onPress={() => this.rowPressed(rowData.id)} underlayColor='#dddddd'> |
These changes will use id
instead of guid
to match up the data records properly.
You’re not quite ready to test your UI code changes. You’ll first need to modify the data fetching logic to transition to your new UI.
Handling the Results
Open SearchPage.js to properly handle your query results. You’ll be camped in this file for the rest of the tutorial, so get comfortable! :]
Earlier on in this tutorial, your data fetch simply logged the results. Remove the debug statement in render
as it’s no longer needed:
console.log(this.data.listings); |
To properly handle the results, you’ll reset the loading flag in the SearchPage
component and navigate to the SearchResults
component with the listing data. Keep in mind that ParseReact.Mixin
forces a re-rendering of your component whenever the results return.
How can you detect that a fetched result triggered the rendering? Furthermore, where should you check this and trigger the navigation?
ParseReact.Mixin
exposes pendingQueries
, which returns an array with the names of the in-progress queries. During the search, you can check for a zero length array to indicate the results have returned and hook your completion check in componentDidUpdate
that triggers post-render.
Add the following method just above render
:
componentDidUpdate: function(prevProps, prevState) { if (prevState.isLoading && (this.pendingQueries().length == 0)) { this.setState({ isLoading: false }); this.props.navigator.push({ title: 'Results', component: SearchResults, passProps: { listings: this.data.listings } }); } }, |
This code first checks isLoading
and if true
, checks that the query results are in. If these conditions are met, you reset isLoading
and push SearchResults
with this.data.listings
passed to it.
It’s generally frowned upon to change state in componentDidUpdate
, since this forces another render call. The reason you can get away with this here is that the first forced render call doesn’t actually change the underlying view.
Keep in mind that React makes use of a virtual DOM and only updates the view if the render call changes any part of that view. The second render call triggered by setting isLoading
does update the view. That means you only get a single view change when results come in.
Press Cmd+R in the simulator, then tap Go and view your one lonely, yet very satisfying, result:
It’s not much fun returning every listing regardless of the search query. It’s time to fix this!
Adding Search Functionality
You may have noticed that your current data schema doesn’t support a search flow since there’s no way to filter on a place name.
There are many ways to set up a sophisticated search, but for the purposes of this tutorial you’re going to keep it simple: you’ll set up a new column that will contain an array of search query terms. If a text search matches one of the terms, you’ll return that row.
Go to your Data Browser and add a column named place_name
of type Array
, like so:
Click inside the place_name
field of the existing row and add the following data:
["campbell","south bay","bay area"] |
Head back to your React Native code. Still in SearchPage.js, modify getInitialState
to add a new state variable for the query sent to Parse and also modify the default search string displayed:
getInitialState: function() { return { searchString: 'Bay Area', isLoading: false, message: '', queryName: null, }; }, |
Next, you’ll need to modify observe
to check for the existence of a place name query.
Add the following filter to your Parse.Query
to look for the place name:
observe: function(props, state) { var listingQuery = (new Parse.Query('Listing')).ascending('price'); if (state.queryName) { listingQuery.equalTo('place_name', state.queryName.toLowerCase()); } return state.isLoading ? { listings: listingQuery } : null; }, |
The equalTo
filter looks through the values of an array type and returns objects where a match exists. The filter you’ve defined looks at the place_name
array and returns Listing
objects where the queryName
value is contained in the array.
Now, modify _executeQuery
to take in a query argument and set the queryName
state variable:
_executeQuery: function(nameSearchQuery) { this.setState({ isLoading: true, message: '', queryName: nameSearchQuery, }); }, |
Then, modify onSearchPressed
to pass the search string from the text input:
onSearchPressed: function() { this._executeQuery(this.state.searchString); }, |
Finally, modify onLocationPressed
to pass in null
to _executeQuery
:
onLocationPressed: function() { navigator.geolocation.getCurrentPosition( location => { this._executeQuery(null); }, error => { this.setState({ message: 'There was a problem with obtaining your locaton: ' + error }); } ); }, |
You do this as you don’t want to execute a place name search when a location query triggers.
In your simulator, press Cmd+R; your application should refresh and you should see the new default search string.
Tap Go and verify that you get the same results as before.
Now go back to the home page of your app, enter neverland in the search box and tap Go:
Uh-oh. Your app pushed the new view with an empty result set. This would be a great time to add some error handling! :]
Update componentDidUpdate to the following implementation:
componentDidUpdate: function(prevProps, prevState) { if (prevState.isLoading && (this.pendingQueries().length == 0)) { // 1 this.setState({ isLoading: false }); // 2 if (this.queryErrors() !== null) { this.setState({ message: 'There was a problem fetching the results' }); } else // 3 if (this.data.listings.length == 0) { this.setState({ message: 'No search results found' }); } else { // 4 this.setState({ message: '' }); this.props.navigator.push({ title: 'Results', component: SearchResults, passProps: {listings: this.data.listings} }); } } }, |
Taking the code step-by-step you’ll see the following:
- Here you turn off
isLoading
to clear out the loading indicator. - Here you check
this.queryErrors
, which is another method thatParseReact.Mixin
exposes. The method returns a non-null object if there are errors; you’ve updated the message to reflect this. - Here you check if there are no results returned; if so, you set the appropriate message.
- If there are no errors and there is data, push the results component.
Press Cmd+R and test the empty results case once again. You should now see the relevant message without the empty results component pushed:
Feel free to add more rows to your Listing
class in the Parse Data Browser to test additional search queries; you can make use of the sample photos available in the Media folder you downloaded earlier.
Adding Location Queries
Querying locations is really easy to do with Parse, since Parse supports a GeoPoint data type and provides API methods to peform a variety of geo-based queries, such as searching for locations within a certain radius.
Go to your Data Browser and add a column named location
of type GeoPoint
:
You’ll need to add some location data for your initial row.
Double-click in the location field and add 37.277455 for latitude area and -121.937503 for the longitude:
Head back to SearchPage.js and modify getInitialState
as follows:
getInitialState: function() { return { searchString: 'Bay Area', isLoading: false, message: '', queryName: null, queryGeo: {}, }; }, |
This adds a new queryGeo
state to hold location data.
Next, modify _executeQuery
to take in location data like so:
_executeQuery: function(nameSearchQuery, geoSearchQuery) { this.setState({ isLoading: true, message: '', queryName: nameSearchQuery, queryGeo: geoSearchQuery, }); }, |
Here, you’ve added an additional parameter for the location-based query and then add whatever’s passed in to the current state.
Next, modify onSearchPressed
to pass an empty location to _executeQuery
:
onSearchPressed: function() { this._executeQuery(this.state.searchString, {}); }, |
The search button is for when you’re searching by place name rather than by location, which means you can just pass in an empty object for the geoSearchQuery
.
Modify onLocationPressed
to finally make use of this precious location data by passing it on to _executeQuery
:
onLocationPressed: function() { navigator.geolocation.getCurrentPosition( location => { this._executeQuery( null, { latitude: location.coords.latitude, longitude: location.coords.longitude, } ); }, error => { this.setState({ message: 'There was a problem with obtaining your locaton: ' + error }); }); }, |
This time, the updated call to _executeQuery
passes in null
for the search string and actual coordinates for the geoSearchQuery
.
Finally, modify observe
to add the location-based search filter:
observe: function(props, state) { var listingQuery = (new Parse.Query('Listing')).ascending('price'); if (state.queryName) { listingQuery.equalTo('place_name', state.queryName.toLowerCase()); } else // 1 if (state.queryGeo && state.queryGeo.latitude && state.queryGeo.longitude) { // 2 var geoPoint = new Parse.GeoPoint({ latitude: state.queryGeo.latitude, longitude: state.queryGeo.longitude, }); // 3 listingQuery.withinMiles('location', geoPoint, 25); } return state.isLoading ? { listings: listingQuery } : null; }, |
Taking each numbered comment in turn:
- Here you check if this is a location query.
- Next, you create a Parse.GeoPoint based on the location coordinates.
- Finally, you add a filter for locations within 25 miles of the point of interest.
Before you can test the location-based search, you’ll need to specify a location that will yield results.
From the simulator menu, select Debug\Location\Apple to set your simulated location to a spot near Apple headquarters.
In the simulator, press Cmd+R. Tap Location, permit the app to receive location, then verify that you see the expected result:
Adding More Test Data
The folder you downloaded earlier contains a JSON test data file — Listing.json
— that you can import instead of entering your own data.
To import this data go to your Data Browser and perform the following actions:
- Click Import.
- Drag Listing.json into the upload area.
- Make sure the Custom option is selected and click Finish Import.
- Dismiss the pop-up.
You should receive a confirmation email once it’s done; since you’re importing a very small amount of data, this should happen very quickly.
Once the import is complete, you’ll need to fix the image URLs. These will contain incorrect information and you need to upload the photos yourself. Go to all the newly imported rows and, one by one, delete the existing img_url
entry, then upload the corresponding photo from the Media folder.
You’ll notice you have a duplicate for the “Grand mansion” property, since you created it manually and it’s also in the import file. Delete one of the copies to keep things clean.
In your simulator, press Cmd+R, click Location and verify that you see the additional results from your imported test data:
Where to Go From Here?
You can download the completed project here. Remember to update index.ios.js with your own Parse application and Javascript keys so you connect to your own data set!
You’ve only scratched the surface of what you can do with Parse+React; there’s a whole world out there beyond simply fetching data. You can save data and even use the underlying Parse JavaScript SDK APIs to create users and or add Parse analytics. Check out Parse+React on GitHub for more details.
For more information on Parse itself, check out our Parse Tutorial: Getting Started with Web Backends. If you want to hear more about React Native, have a listen at our podcast episode with Nick Lockwood, who works on the React Native team at Facebook.
If you have comments or questions, feel free to share them in the discussion below!
The post Integrating Parse and React Native for iOS appeared first on Ray Wenderlich.