Below I will walk through the steps to make a simple app with firebase and react native; my goal is to have anonymous but persistent ID associated with each phone, which securely logs into the server and can push/query only it's own data. This example also takes advantage of the caching associated with Firebase's library for react-native; even when the phone is offline, the firebase code seamlessly logs the user in *as if* they were online, and presents a cached/persistent datastore that will automatically push new data to the cloud on reconnect. It also serves cached data when offline, after the actual cloud connection attempts time out. It makes for a completely seamless experience of persisent/cached data, security, and user identity, with almost no effort.
Incorporating Persistent Firebase Authentication
First we'll set up our project:
npx react-native init ReactNativeTestFirebase
cd ReactNativeTestFirebase
npm install --save @react-native-firebase/app
open the .xcworkspace project. Click on the main project; change the bundle identifier and name to something nice. Copy the bundle name. Change signing and capabilities to our personal team.
create app on firebase named 'WatchV1App'. Now add an iOs app, copy the bundle identifier and register the app. Download the associated file and drop it into the xcode project folder where the Info.plist file is, selecting all targets.
In firebase, go to authentication, click get started, enable 'anonymous'.
Go to ios/WatchV1App/AppDelegate.m, at top add
#import <Firebase.h>
and at the beginning of didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
add
if ([FIRApp defaultApp] == nil) {
[FIRApp configure];
}
on the next line (first line of the method after the opening bracket).
Now we'll install the firebase tools we need to use, firestore and auth:
cd ios
pod install --repo-update
cd ..
npm install --save @react-native-firebase/auth
npm install --save @react-native-firebase/firestore
cd ios && pod install
cd ..
npx react-native run-ios
Now we're setup.
We'll replace our App.js file as follows:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow strict-local
*/
import React, {useState, useEffect} from 'react';
import {
SafeAreaView,
StyleSheet,
ScrollView,
View,
Text,
StatusBar,
} from 'react-native';
import {
Header,
LearnMoreLinks,
Colors,
DebugInstructions,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
import auth from '@react-native-firebase/auth';
auth()
.signInAnonymously()
.then(() => {
console.log('User signed in anonymously');
})
.catch(error => {
if (error.code === 'auth/operation-not-allowed') {
console.log('Enable anonymous in your firebase console.');
}
console.error(error);
});
function App() {
// Set an initializing state whilst Firebase connects
const [initializing, setInitializing] = useState(true);
const [user, setUser] = useState();
// Handle user state changes
function onAuthStateChanged(user) {
setUser(user);
if (initializing) setInitializing(false);
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber; // unsubscribe on unmount
}, []);
if (initializing) return null;
if (!user) {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Header />
<View style={styles.body}>
<View style={styles.sectionContainer}>
<Text>Error connecting to Firebase</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
</>
);
}
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Header />
<View style={styles.body}>
<View style={styles.sectionContainer}>
<Text>Welcome {user.email}</Text>
</View>
</View>
</ScrollView>
</SafeAreaView>
</>
);
}
const styles = StyleSheet.create({
scrollView: {
backgroundColor: Colors.lighter,
},
engine: {
position: 'absolute',
right: 0,
},
body: {
backgroundColor: Colors.white,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
color: Colors.black,
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
color: Colors.dark,
},
highlight: {
fontWeight: '700',
},
footer: {
color: Colors.dark,
fontSize: 12,
fontWeight: '600',
padding: 4,
paddingRight: 12,
textAlign: 'right',
},
});
export default App;
This now will connect with a persistent anonymized user ID; when sign in occurs and authenticates we get onAuthStateChanged() called. We should be able to see ourselves in the authorization section of firebase after we run the app.
Set up Firestore
Now we will set up 'conditions' and 'events' collections. These will be creatable by an anonymized but logged in user; they'll contain an 'id' field that matches their logged in key; and they will only be readable if the id field is their own. This will partition data by user.
Firestore is nice in that it is schema-less; we can simply push arbitrary json blobs to it any collection of documents.
We start in the firestore UI; we create a datastore in a central US region in 'production' mode. we then 'start a collection'. I'll make one called 'conditions', and it will have some fake data (autoid, timestamp:timestamp, temperature/humidity/lux/whitelux numbers, and uid string). For the uid string, I'll copy the UID from the authenticated user section. I'll also make a conditions document with fake data and a different uid so we can check access.
We'll do a similar thing for 'event's.
Next we'll go to rules. We want to add rules for all documents; we want to be able to read and create if the uid of the data equals the uid of the auth, otherwise nothing is allowed (no 'update', and write gives delete, create, and update permissions. Read can be broken into get and list (ability to access one document at a time vs list all of them)).
The default rule will match any document in our database (this is a recursive matching syntax). We will simply update this rule as follows:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow create:
if request.resource.data.uid == request.auth.uid;
allow read:
if resource.data.uid == request.auth.uid;
allow update, delete:
if false;
}
}
}
This should make it so only create/read are allowed, and users can only read/write data with their own UID (assigned anonymously with the session).
Now let's edit our app to be able to read and publish data.
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow strict-local
*/
import React, {useState, useEffect} from 'react';
import {
SafeAreaView,
StyleSheet,
ScrollView,
View,
Text,
StatusBar,
Button,
} from 'react-native';
import {
Header,
LearnMoreLinks,
Colors,
DebugInstructions,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
import auth from '@react-native-firebase/auth';
import firestore from '@react-native-firebase/firestore';
auth()
.signInAnonymously()
.then(() => {
console.log('User signed in anonymously');
})
.catch(error => {
if (error.code === 'auth/operation-not-allowed') {
console.log('Enable anonymous in your firebase console.');
}
console.error(error);
});
const conditionsCollection = firestore().collection('conditions');
const eventsCollection = firestore().collection('events');
function App() {
// Set an initializing state whilst Firebase connects
const [initializing, setInitializing] = useState(true);
const [user, setUser] = useState();
const [conditionsArray, setConditionsArray] = useState([]);
const [eventsArray, setEventsArray] = useState([]);
// Handle user state changes
function onAuthStateChanged(user) {
setUser(user);
conditionsCollection.where("uid","==", user.uid).get().then(querySnapshot => {
let cArray = [];
querySnapshot.forEach(doc => {
cArray.push(doc.data());
});
setConditionsArray(cArray);
eventsCollection.where("uid","==", user.uid).get().then(querySnapshot => {
let eArray = [];
querySnapshot.forEach(doc => {
eArray.push(doc.data());
});
setEventsArray(eArray);
if (initializing) setInitializing(false);
}, error => {console.log(error.code);});
}, error => {console.log(error.code);});
}
async function addEvent(timestamp, type, data){
console.log('sending event for user ' + user.uid);
let eventdoc = {
uid: user.uid,
timestamp: timestamp,
type: type,
data: data
};
setEventsArray([...eventsArray, eventdoc]);
await eventsCollection.add(eventdoc);
}
async function addCondition(timestamp, temp, humd, lux, wlux){
console.log('sending condition for user ' + user.uid);
let conditiondoc = {
uid: user.uid,
timestamp: timestamp,
temperature: temp,
humidity: humd,
lux: lux,
whitelux: wlux
};
setConditionsArray([...conditionsArray, conditiondoc]);
await conditionsCollection.add(conditiondoc);
}
function addRandomEvent(){
let ts = new Date();
addEvent(ts, 'TX_EXAMPLE_TYPE', 2);
}
function addRandomCondition(){
let ts = new Date();
addCondition(ts, Math.random(), Math.random(), Math.random(), Math.random());
}
async function getAllConditions(){
return await conditionsCollection.get();
}
async function getAllEvents(){
return await eventsCollection.get();
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber; // unsubscribe on unmount
}, []);
const conditionItems = conditionsArray.map((conditions) =>
<Text key={conditions.timestamp + conditions.uid}>
{conditions.timestamp.toString()} {"\n"} {conditions.temperature} {"\n\n"}
</Text> );
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Header />
<View style={styles.body}>
<View style={styles.sectionContainer}>
{user ?
<Text>Welcome {user.uid}</Text> :
<Text>User not logged in</Text>
}
{initializing ?
<Text>initializing</Text> :
<Text>initialized</Text>
}
<Button
title="Send Random Condition"
color="#010101"
onPress={addRandomCondition.bind(this)}
/>
{conditionItems}
</View>
</View>
</ScrollView>
</SafeAreaView>
</>
);
}
const styles = StyleSheet.create({
scrollView: {
backgroundColor: Colors.lighter,
},
engine: {
position: 'absolute',
right: 0,
},
body: {
backgroundColor: Colors.white,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
color: Colors.black,
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
color: Colors.dark,
},
highlight: {
fontWeight: '700',
},
footer: {
color: Colors.dark,
fontSize: 12,
fontWeight: '600',
padding: 4,
paddingRight: 12,
textAlign: 'right',
},
});
export default App;
We notice that *even when we are offline*, we log in as a user instantly. When we are offline, it takes about 10 seconds for Firebase to timeout trying to connect to the actual server, and instead 'initialize' offline using the cache. Data sent when we are offline is cached and makes it online when we get online; data 'sent' when we're offline before the cache is initialized is added to the cache seamlessly.
This will give the app user an anonymous ID that persists over time; it will log them into that ID even offline; it will allow you to send data even when offline, and will attempt to cache/persist some data when offline; it will give them create access to send data to the firebase server, and read access to their own data, which will be updated to the relevant state as soon as the user is authenticated.
Attempting to access data without the where("uid","==",user.uid)
phrase will give an unathorized error, exactly as we want.
It's a nice, seamless experience that handles local caching, updating, user sessions, data privacy, and authentication without having to worry about online/offline status ourselves.
Querying Chronologically
In order to filter our returned values chronologically and just pull the most recent, we need to set a composite index on our timestamp.
Go to 'indexes' in Cloud Firestore and add a composite index for 'uid' and 'timestamp'. We'll use this across collections, so give our collection id 'conditions'. The error when we ask for this in our javascript code actually generates a link in the error.message that will create this composite index automatically for us, which we can just copy. This is the preferred method to get things right.
Now we can query:
conditionsCollection
.where("uid","==", user.uid)
.orderBy("timestamp")
.get()
.then(querySnapshot => {
we can also limit our responses with .limit(num)
, we should be able to do a 'where' on timestamp before the orderBy statement as well.
Add a Chart
First we'll install some charting libraries:
npm install --save react-native-chart-kit react-native-svg
cd ios && pod install
cd ..
npx react-native run-ios
Now we'll edit our main App to be as follows:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow strict-local
*/
import React, {useState, useEffect} from 'react';
import {
SafeAreaView,
StyleSheet,
ScrollView,
View,
Text,
StatusBar,
Button,
} from 'react-native';
import {LineChart} from "react-native-chart-kit";
import { Dimensions } from 'react-native';
import {
Header,
LearnMoreLinks,
Colors,
DebugInstructions,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
import auth from '@react-native-firebase/auth';
import firestore from '@react-native-firebase/firestore';
auth()
.signInAnonymously()
.then(() => {
console.log('User signed in anonymously');
})
.catch(error => {
if (error.code === 'auth/operation-not-allowed') {
console.log('Enable anonymous in your firebase console.');
}
console.error(error);
});
const conditionsCollection = firestore().collection('conditions');
const eventsCollection = firestore().collection('events');
function App() {
// Set an initializing state whilst Firebase connects
const [initializing, setInitializing] = useState(true);
const [user, setUser] = useState();
const [conditionsArray, setConditionsArray] = useState([]);
const [eventsArray, setEventsArray] = useState([]);
// Handle user state changes
function onAuthStateChanged(user) {
setUser(user);
conditionsCollection.where("uid","==", user.uid).orderBy("timestamp").get().then(querySnapshot => {
let cArray = [];
querySnapshot.forEach(doc => {
cArray.push(doc.data());
});
setConditionsArray(cArray);
cArray.forEach(el => {console.log(el);});
eventsCollection.where("uid","==", user.uid).get().then(querySnapshot => {
let eArray = [];
querySnapshot.forEach(doc => {
eArray.push(doc.data());
});
setEventsArray(eArray);
if (initializing) setInitializing(false);
}, error => {console.log(error.code + ": " + error.message);});
}, error => {console.log(error.code + ": " + error.message);});
}
async function addEvent(timestamp, type, data){
console.log('sending event for user ' + user.uid);
let eventdoc = {
uid: user.uid,
timestamp: timestamp,
type: type,
data: data
};
setEventsArray([...eventsArray, eventdoc]);
await eventsCollection.add(eventdoc);
}
async function addCondition(timestamp, temp, humd, lux, wlux){
console.log('sending condition for user ' + user.uid);
let conditiondoc = {
uid: user.uid,
timestamp: timestamp,
temperature: temp,
humidity: humd,
lux: lux,
whitelux: wlux
};
setConditionsArray([...conditionsArray, conditiondoc]);
await conditionsCollection.add(conditiondoc);
}
function addRandomEvent(){
let ts = firestore.Timestamp.fromDate(new Date());
addEvent(ts, 'TX_EXAMPLE_TYPE', 2);
}
function addRandomCondition(){
let ts = firestore.Timestamp.fromDate(new Date());
addCondition(ts, Math.random(), Math.random(), Math.random(), Math.random());
}
async function getAllConditions(){
return await conditionsCollection.get();
}
async function getAllEvents(){
return await eventsCollection.get();
}
useEffect(() => {
const subscriber = auth().onAuthStateChanged(onAuthStateChanged);
return subscriber; // unsubscribe on unmount
}, []);
const conditionItems = conditionsArray.map((conditions) =>
<Text key={conditions.timestamp + conditions.uid + conditions.temperature}>
{conditions.timestamp.toString()} {"\n"} {conditions.temperature} {"\n\n"}
</Text> );
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.scrollView}>
<Header />
<View style={styles.body}>
<View style={styles.sectionContainer}>
{user ?
<Text>Welcome {user.uid}</Text> :
<Text>User not logged in</Text>
}
{initializing ?
<Text>initializing</Text> :
<Text>initialized</Text>
}
<Button
title="Send Random Condition"
color="#010101"
onPress={addRandomCondition.bind(this)}
/>
{conditionsArray.length?
<LineChart data={{
labels: [' ' + new Date(conditionsArray[0]['timestamp'].toDate()).toLocaleString()]
.concat(Array(2).fill("")
.concat([new Date(conditionsArray[conditionsArray.length-1]['timestamp'].toDate()).toLocaleString()])),
datasets: [
{
data: conditionsArray.map(el => {return el['temperature'];}), //[20, 45, 28, 80, 99, 43],
color: (opacity = 1) => `rgba(134, 65, 244, ${opacity})`, // optional
strokeWidth: 2
}
],
legend: ["Temperature"]
}}
width={0.85*Dimensions.get('window').width}
height={180}
chartConfig={chartConfig}
bezier
/>
:<Text> no data yet </Text>}
{conditionItems}
</View>
</View>
</ScrollView>
</SafeAreaView>
</>
);
}
const styles = StyleSheet.create({
scrollView: {
backgroundColor: Colors.lighter,
},
engine: {
position: 'absolute',
right: 0,
},
body: {
backgroundColor: Colors.white,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
color: Colors.black,
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
color: Colors.dark,
},
highlight: {
fontWeight: '700',
},
footer: {
color: Colors.dark,
fontSize: 12,
fontWeight: '600',
padding: 4,
paddingRight: 12,
textAlign: 'right',
},
});
const chartConfig = {
backgroundColor: '#ffffff',
backgroundGradientFrom: '#ffffff',
backgroundGradientTo: '#ffffff',
labelColor: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`
};
export default App;
Notice that we're now using timestamp objects from the firestore library instead of normal javascript datatime objects. These can easily be moved between– check the documentation. Our queries are now ordered by timestamp.
We should see the following app, with all the data also appearing in our firebase console. Clicking the 'Send Random Condition' will update our list, our chart, our displayed axis timestamp, and send the data to firebase:
For the code to this example, see the repo here: https://github.com/dramsay9/ReactNativeFirebaseTest. Feel free to reuse!