Creating a photo sharing app with React Native
You will need Node 8+ and the Expo CLI and SDK installed on your machine. Some knowledge of React Native development will be helpful.
In this tutorial, we’ll be creating a realtime photo-sharing app with React Native and Pusher Channels.
Prerequisites
Basic knowledge of React Native is required is in order to follow along. We’ll also be using Redux in some parts of the app so basic knowledge of it will be helpful as well.
We’ll be using Expo in order to easily test the app on multiple devices. Download the Expo client app for your iOS or Android device.
These are the package versions used in creating the app:
- Node 8.3.0
- Yarn 1.7.0
- Expo CLI 2.0.0
- Expo SDK 30.0.0
- Pusher 4.3.1
- React Navigation 2.14.0
You don’t necessarily have to use the versions above, but if you encounter problems when using other versions, I recommend you to use the ones above instead. For other packages used in the app, be sure to check out the package.json
file found in the GitHub repo.
We’ll be using Pusher and Imgur in this tutorial so you need to have an account on both of those services:
App overview
When the user first opens the app, they’ll be greeted by the following screen. From here, they can either choose to share photos or view them by subscribing to another user who chose to share their photo:
When a user chooses Share, they’ll be assigned a unique username, which they can share with anyone. This sharing mechanism will be entirely outside the app, so it can be anything (For example, email or SMS):
Here’s what it looks like when someone chooses View. On this screen, they have to enter the username assigned to the user they want to follow:
Going back to the user who selected Share, here’s what their screen will look like when they click on the camera icon from earlier. This will allow the user to take a photo, flip the camera, or close it:
Once they take a snap, the camera UI will close and the photo will be previewed. At this point, the photo should have already started uploading in the background using the Imgur API:
Switching back to the follower (the user who clicked on View), once the upload is finished, the Imgur API should return the image URL and its unique ID. Those data are then sent to the Pusher channel which the follower has subscribed to. This allows them to also see the shared photo:
It’s not shown in the screenshot above, but everytime a new photo is received, it will automatically be appended to the top of the list.
You can find the app’s source code in this GitHub repo.
Create Pusher and Imgur apps
On your Pusher dashboard, create a new app and name it RNPhotoShare. Once it’s created, go to app settings and enable client events. This will allow us to directly trigger events from the app:
Next, after logging in to your Imgur account, go to this page and register an app. The most important setting here is the Authorization type. Select Anonymous usage without user authorization as we will only be uploading images anonymously. Authorization callback URL can be any value because we won’t really be using it. Other than that, you can enter any value for the other fields:
Click Submit to create the app. This will show you the app ID and app secret. We’re only going to need the app ID so take note of that. In case you lose the app ID, you can view all the Imgur apps you created here.
Building the app
Start by cloning the project repo and switch to the starter
branch:
git clone https://github.com/anchetaWern/RNPhotoShare.git
cd RNPhotoShare
git checkout starter
The starter
branch contains the bare-bones app template, navigation, components, and all of the relevant styles which we will be using later on. Having all of those in the starter allows us to focus on the main meat of the app.
Install the packages using Yarn:
yarn install
Here’s a quick overview of what each package does:
expo
- the Expo SDK. This includes the Camera API and the icons that we will be using in the app.random-animal-name-generator
- for generating the unique usernames for users who want to share photos.pusher-js
- the JavaScript library for working with Pusher.react-navigation
- for implementing navigation within the app.prop-types
- for validating the props added to components on runtime.whatwg-fetch
- there’s a recent issue with the latest version of thewhatwg-fetch
package that Expo uses, so we need to install a lower version through the main project in order to fix the issue.redux
- for adding and managing global app state.react-redux
- for working with Redux within the React environment.
Home screen
Let’s first start with the Home screen by importing all the necessary packages:
// src/screens/HomeScreen.js
import React, { Component } from "react";
import { View, Text, Button } from "react-native";
import Pusher from "pusher-js/react-native";
By default, React Navigation will display a header on every page, we don’t want that in this page so we disable it. In the constructor, we initialize the value of the Pusher client. We will be using this to connect to Pusher and trigger and subscribe to events:
export default class HomeScreen extends Component {
static navigationOptions = {
header: null // don't display header
};
constructor(props) {
super(props);
this.pusher = null;
}
// next: add componentDidMount
}
Once the component is mounted, we initialize the Pusher client using the app key and app cluster from your app settings. As for the authEndpoint
, retain the value below for now, we will be updating it later before we run the app:
componentDidMount() {
this.pusher = new Pusher("YOUR PUSHER APP KEY", {
authEndpoint: "YOUR_NGROK_URL/pusher/auth",
cluster: "YOUR PUSHER APP CLUSTER",
encrypted: true // false doesn't work, you need to always use https for the authEndpoint
});
}
// next: add render method
Next, we render the UI for the Home screen. This contains two buttons that allow the user to navigate to either the Share screen or the View screen. In both cases, we pass in the reference to the Pusher client as a navigation param. This allows us to use Pusher on both pages:
render() {
return (
<View style={styles.container}>
<Text style={styles.mainText}>What to do?</Text>
<View style={styles.buttonContainer}>
<Button
title="Share"
color="#1083bb"
onPress={() => {
this.props.navigation.navigate("Share", {
pusher: this.pusher
});
}}
/>
</View>
<View style={styles.buttonContainer}>
<Button
title="View"
color="#2f9c0a"
onPress={() => {
this.props.navigation.navigate("View", {
pusher: this.pusher
});
}}
/>
</View>
</View>
);
}
Share screen
Next is the Share screen. This is where the user can take pictures with the in-app camera and share it on realtime to people who have followed their username.
Start by importing all the packages we’ll need. Most of these should look familiar, except for Clipboard
. We’ll be using it to copy the user’s username to the clipboard so they can easily share it on another app:
// src/screens/ShareScreen.js
import React, { Component } from "react";
import {
View,
Text,
TouchableOpacity,
Clipboard,
Alert,
Image,
Dimensions,
Button,
ScrollView
} from "react-native";
Next are the Expo packages and the random animal name generator. For Expo, we need the Camera
for rendering a bare-bones camera UI and the Permissions
to ask the user to access the camera:
import { MaterialIcons } from "@expo/vector-icons";
import { Camera, Permissions } from "expo";
import generateRandomAnimalName from "random-animal-name-generator"; // for generating unique usernames
Next, add a button in the header. This will allow the user to stop sharing their photos. When this button is clicked, all users who are currently subscribed to this user will stop receiving updates:
export default class ShareScreen extends Component {
static navigationOptions = ({ navigation }) => {
const { params } = navigation.state;
return {
title: "Share Photos",
headerTransparent: true,
headerRight: (
<Button
title="Finish"
color="#333"
onPress={() => params.finishSharing()}
/>
),
headerTintColor: "#333"
};
};
// next: initialize state
}
Next, initialize the state:
state = {
hasCameraPermission: null, // whether the user has allowed the app to access the device's camera
cameraType: Camera.Constants.Type.front, // which camera to use? front or back?
isCameraVisible: false, // whether the camera UI is currently visible or not
latestImage: null // the last photo taken by the user
};
// next: add constructor
In the constructor, we generate a unique username for the user. This is composed of the funny animal name from the random-animal-name-generator
library and a random number. Here, we also initialize the value for the Pusher client (we’ll get it from the navigation params shortly) and the user_channel
where we will emit the event for sharing photos. Since this screen is where the Camera UI will be rendered, we also want the user to be able to change the screen orientation. That way, they can capture both portrait and landscape photos:
constructor(props) {
super(props);
// generate unique username
const animalName = generateRandomAnimalName()
.replace(" ", "_")
.toLowerCase();
const min = 10;
const max = 99;
const number = Math.floor(Math.random() * (max - min + 1)) + min;
const username = animalName + number;
this.username = username;
// initialize pusher
this.pusher = null;
this.user_channel = null;
// allow changing of screen orientation
Expo.ScreenOrientation.allow(
Expo.ScreenOrientation.Orientation.ALL_BUT_UPSIDE_DOWN // enable all screen orientations except upside-down/reverse portrait
);
}
// next: add componentDidMount
Once the component is mounted, we set the finishSharing
method as a navigation param. We’ll define this method later, but for now, know that this is used for unsubscribing the user from their own channel. We’re subscribing to that channel right below that code. This allows us to listen to or trigger messages from this channel. Lastly, we ask for permission from the user to access the camera:
async componentDidMount() {
const { navigation } = this.props;
navigation.setParams({
finishSharing: this.finishSharing
});
// subscribe to channel
this.pusher = navigation.getParam("pusher");
this.user_channel = this.pusher.subscribe(`private-user-${this.username}`);
// ask user to access device camera
const { status } = await Permissions.askAsync(Permissions.CAMERA);
this.setState({ hasCameraPermission: status === "granted" });
}
// next: add render method
For those who are working with Pusher for the first time, the way it works is that you first have to subscribe the users to a channel. Anyone who is subscribed to this channel will be able to trigger and listen for messages sent through that channel by means of “events”. Not all users who are subscribed to the channel need to know all about the events being sent through that channel, that’s why users can selectively bind to specific events only.
Next, we render the contents of the Share screen. In this case, there are only two possible contents: one where only the camera UI is visible, and the other where only the box containing the username and a button (for opening the camera) is visible:
render() {
return (
<View style={styles.container}>
{!this.state.isCameraVisible && (
<ScrollView contentContainerStyle={styles.scroll}>
<View style={styles.mainContent}>
<TouchableOpacity onPress={this.copyUsernameToClipboard}>
<View style={styles.textBox}>
<Text style={styles.textBoxText}>{this.username}</Text>
</View>
</TouchableOpacity>
<View style={styles.buttonContainer}>
<TouchableOpacity onPress={this.openCamera}>
<MaterialIcons name="camera-alt" size={40} color="#1083bb" />
</TouchableOpacity>
</View>
{this.state.latestImage && (
<Image
style={styles.latestImage}
resizeMode={"cover"}
source={{ uri: this.state.latestImage }}
/>
)}
</View>
</ScrollView>
)}
{this.state.isCameraVisible && (
<Camera
style={styles.camera}
type={this.state.cameraType}
ref={ref => {
this.camera = ref;
}}
>
<View style={styles.cameraFiller} />
<View style={styles.cameraContent}>
<TouchableOpacity
style={styles.buttonFlipCamera}
onPress={this.flipCamera}
>
<MaterialIcons name="flip" size={25} color="#e8e827" />
</TouchableOpacity>
<TouchableOpacity
style={styles.buttonCamera}
onPress={this.takePicture}
>
<MaterialIcons name="camera" size={50} color="#e8e827" />
</TouchableOpacity>
<TouchableOpacity
style={styles.buttonCloseCamera}
onPress={this.closeCamera}
>
<MaterialIcons name="close" size={25} color="#e8e827" />
</TouchableOpacity>
</View>
</Camera>
)}
</View>
);
}
// next: add copyUsernameToClipboard
If you’ve read the app overview earlier, you should already have a general idea on what’s going on in the code above so I’ll no longer elaborate. Take note of the ref
prop we’ve passed to the Camera
component though. This allows us to get a reference to that instance of the Camera
component and assign it to a local variable called this.camera
. We will be using it later to take a picture using that camera instance.
When the user clicks on the box containing the user’s username, this method is called and it sets the username to the clipboard:
copyUsernameToClipboard = () => {
Clipboard.setString(this.username);
Alert.alert("Copied!", "Username was copied clipboard");
};
// next: add openCamera
Next, are the methods for opening the camera UI, flipping it (use either back or front camera), and closing it:
openCamera = () => {
const { hasCameraPermission } = this.state;
if (!hasCameraPermission) {
Alert.alert("Error", "No access to camera");
} else {
this.setState({ isCameraVisible: true });
}
};
flipCamera = () => {
this.setState({
cameraType:
this.state.cameraType === Camera.Constants.Type.back
? Camera.Constants.Type.front
: Camera.Constants.Type.back
});
};
closeCamera = () => {
this.setState({
isCameraVisible: false
});
};
// next: add takePicture
Next is the method for taking pictures. This is where we use the camera reference from earlier (this.camera
) to call the takePictureAsync
method from the Camera API. By default, the takePictureAsync
method only returns an object containing the width
, height
and uri
of the photo that was taken. That’s why we’re passing in an object containing the options we want to use. In this case, base64
allows us to return the base64 representation of the image. This is what we set in the request body of the request we send to the Imgur API. Once we receive a response from the Imgur API, we extract the data that we need from the response body and trigger the client-posted-photo
event so any subscriber who is currently listening to that event will receive the image data:
takePicture = async () => {
if (this.camera) {
let photo = await this.camera.takePictureAsync({ base64: true }); // take a snap, and return base64 representation
// construct
let formData = new FormData();
formData.append("image", photo.base64);
formData.append("type", "base64");
this.setState({
latestImage: photo.uri, // preview the photo that was taken
isCameraVisible: false // close the camera UI after taking the photo
});
const response = await fetch("https://api.imgur.com/3/image", {
method: "POST",
headers: {
Authorization: "Client-ID YOUR_IMGUR_APP_ID" // add your Imgur App ID here
},
body: formData
});
let response_body = await response.json(); // get the response body
// send data to all subscribers who are listening to the client-posted-photo event
this.user_channel.trigger("client-posted-photo", {
id: response_body.data.id, // unique ID assigned to the image
url: response_body.data.link // Imgur link pointing to the actual image
});
}
};
// next: add finishSharing
Note that the name of the event has to have client-
as its prefix, just like what we did above. This is because we’re triggering this event from the client side. It’s a naming convention used by Pusher so your event won’t work if you don’t follow it. Check out the docs for more information about this.
Once the user clicks on the Finish button, we unsubscribe them from their own channel. This effectively cuts off all communication between this user and all their followers:
finishSharing = () => {
this.pusher.unsubscribe(`private-user-${this.username}`);
this.props.navigation.goBack(); // go back to home screen
};
For production apps, it’s a good practice to first trigger an “ending” event right before the main user (the one who mainly triggers events) unsubscribes from their own channel. This way, all the other users will get notified and they’ll be able to clean up their connection before their source gets completely shut off.
View screen
The View screen is where users who want to follow another user go. Again, start by importing all the packages we need:
// src/screens/ViewScreen.js
import React, { Component } from "react";
import {
View,
Text,
TextInput,
ScrollView,
Dimensions,
Button,
Alert
} from "react-native";
import CardList from "../components/CardList";
Nothing really new in the code above, except for the CardList
component. This component is already included in the starter project so we don’t have to create it separately. What it does is render all the images that were sent by the user followed by the current user.
Next, import all the Redux-related packages:
// src/screens/ViewScreen.js
import { Provider } from "react-redux";
import { createStore } from "redux";
import reducers from "../reducers";
import { addedCard } from "../actions";
const store = createStore(reducers);
Next, we also add a button in the header. This time, to unfollow the user. We’re also passing in the function used here (params.unfollow
) as a navigation param later inside the componentDidMount
method:
export default class ViewScreen extends Component {
static navigationOptions = ({ navigation }) => {
const { params } = navigation.state;
return {
title: "View Photos",
headerTransparent: true,
headerTintColor: "#333",
headerRight: (
<Button
title="Unfollow"
color="#333"
onPress={() => params.unFollow()}
/>
)
};
};
// next: initialize state
}
Next, initialize the state:
state = {
subscribedToUsername: "", // the username of the user the current user is subscribed to
isSubscribed: false // is the user currently subscribed to another user?
};
In the constructor, we also set the default value for the Pusher client and the user channel. In this case, the user channel will be whoever the current user is subscribed to. The current user doesn’t really need to trigger any events in the user channel, so we don’t have to generate a unique username and subscribe them to their own channel as we did in the Share screen earlier:
constructor(props) {
super(props);
this.pusher = null;
this.user_channel = null;
}
// next: add componentDidMount
Once the component is mounted, we set the unFollow
function as a navigation param and initialize the Pusher client:
componentDidMount() {
const { navigation } = this.props;
navigation.setParams({ unFollow: this.unFollow }); // set the unFollow function as a navigation param
this.pusher = navigation.getParam("pusher");
}
// next: add render
Next, we render the UI of the of the View screen. Here, we wrap everything in the Provider
component provided by react-redux
. This allows us to pass down the store
so we could use it inside the followUser
to dispatch the action for adding a new Card to the CardList:
render() {
return (
<Provider store={store}>
<View style={styles.container}>
{!this.state.isSubscribed && (
<View style={styles.initialContent}>
<Text style={styles.mainText}>User to follow</Text>
<TextInput
style={styles.textInput}
onChangeText={subscribedToUsername =>
this.setState({ subscribedToUsername })
}
>
<Text style={styles.textInputText}>
{this.state.subscribedToUsername}
</Text>
</TextInput>
<View style={styles.buttonContainer}>
<Button
title="Follow"
color="#1083bb"
onPress={this.followUser}
/>
</View>
</View>
)}
{this.state.isSubscribed && (
<ScrollView>
<View style={styles.mainContent}>
<CardList />
</View>
</ScrollView>
)}
</View>
</Provider>
);
}
// next: add followUser
The followUser
method is where we add the code for subscribing to the username entered by the user in the text field. Once the subscription succeeds, only then can we listen for the client-posted-photo
event. When we receive this event, we expect the id
and url
of the image to be present. We then use those to dispatch the action for adding a new Card on top of the CardList:
followUser = () => {
this.setState({
isSubscribed: true
});
// subscribe to the username entered in the text field
this.user_channel = this.pusher.subscribe(
`private-user-${this.state.subscribedToUsername}`
);
// alert the user if there's an error in subscribing
this.user_channel.bind("pusher:subscription_error", status => {
Alert.alert(
"Error occured",
"Cannot connect to Pusher. Please restart the app."
);
});
this.user_channel.bind("pusher:subscription_succeeded", () => { // subscription successful
this.user_channel.bind("client-posted-photo", data => { // listen for the client-posted-photo event to be triggered from the channel
store.dispatch(addedCard(data.id, data.url)); // dispatch the action for adding a new card to the list
});
});
};
// next: add unFollow
Lastly, add the unFollow
method. This gets called when the user clicks on the Unfollow button in the header. This allows us to unsubscribe from the user we subscribed to earlier inside the followUser
method:
unFollow = () => {
this.pusher.unsubscribe(`private-user-${this.state.subscribedToUsername}`);
this.props.navigation.goBack(); // go back to the home page
};
Unsubscribing from a channel automatically unbinds the user from all the events they’ve previously bound to. This means they’ll no longer receive any new photos.
Adding the action and reducer
Earlier in the followUser
method of the src/screens/ViewScreen.js
file, we dispatched the addedCard
action. We haven’t really defined it yet so let’s go ahead and do so. Create an actions
and reducers
folder inside the src
directory to house the files we’re going to create.
To have a single place where we define all the action types in this app, create a src/actions/types.js
file and add the following:
export const ADDED_CARD = "added_card";
In the code above, all we do is export a constant which describes the action type. Nothing really mind-blowing, but this allows us to import and use this constant every time we need to use this specific action. This prevents us from making any typo when using this action.
Next, create a src/actions/index.js
file, this is where we define and export the action. We pass in the ADDED_CARD
constant as a type along with the id
and url
. These are the unique ID and URL of the image which is received by the reducer everytime this action is dispatched:
// src/actions/index.js
import { ADDED_CARD } from "./types";
export const addedCard = (id, url) => {
return {
type: ADDED_CARD,
id: id,
url: url
};
};
Next, create a src/``reducers/CardsReducer.js
file, this is where we add the reducer responsible for modifying the value of the cards
array in the state. This gets executed every time we dispatch the addedCard
action. When that happens, we simply return a new array containing the existing card objects and the new card object:
// src/reducers/CardsReducer.js
import { ADDED_CARD } from "../actions/types";
const INITIAL_STATE = {
cards: []
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case ADDED_CARD:
const cards = [...state.cards, { id: action.id, url: action.url }]; // return a new array containing the existing card objects and the new card object
return { ...state, cards };
default:
return state;
}
};
Note that we’re adding it to the end of the new array instead of in the beginning. This is because the FlatList
component which is responsible for rendering this data is inverted
. This means that the items are rendered from bottom to top.
Lastly, combine all the reducers in a single file:
// src/reducers/index.js
import { combineReducers } from "redux";
import CardsReducer from "./CardsReducer";
export default combineReducers({
cards: CardsReducer
});
The code above enabled us to import only a single file to include the reducers and use it for creating the store. Don’t add this, as it was already added earlier:
// src/screens/ViewScreen.js (don't add as it was already added earlier)
import reducers from "../reducers";
const store = createStore(reducers);
Update the CardList component
If you saw the CardList
component from the codes of the View screen earlier, you might have noticed that we haven’t really passed any props to it. So how will it have any data to render?
// src/screens/ViewScreen.js
{this.state.isSubscribed && (
<ScrollView>
<View style={styles.mainContent}>
<CardList />
</View>
</ScrollView>
)}
The answer is it doesn’t. Currently, the CardList
component doesn’t really have the ability to render cards, so we have to update it. Start by importing the connect
method from the react-redux
library. This will allow us to create a “connected” component:
// src/components/CardList.js
import { connect } from "react-redux";
After the CardList
prop types, add a mapStateToProps
method. This allows us to map out any value in the store as a prop for this component. In this case, we only want the cards
array:
CardList.propTypes = {
// previous CardList propTypes code here...
};
// add this:
const mapStateToProps = ({ cards }) => { // extract the cards array from the store
return cards; // make it available as props
};
// replace export default CardList with this:
export default connect(mapStateToProps)(CardList);
Now, every time the addedCard
action is dispatch, the value of this.props.cards
inside this component will always be in sync with the value of the cards
array in the store.
Creating the server
The server is mainly used for authenticating a user who tries to connect to Pusher. If you open the file for the Home screen, we’ve added this code earlier:
// src/screens/HomeScreen.js
componentDidMount() {
this.pusher = new Pusher("YOUR PUSHER APP KEY", {
authEndpoint: "YOUR_NGROK_URL/pusher/auth",
cluster: "YOUR PUSHER APP CLUSTER",
encrypted: true
});
}
This is where we establish the connection to Pusher’s servers. The authEndpoint
is responsible for authenticating the user to verify that they’re really a user of your app. So the app hits the server every time the code above is executed.
Now that you know what the server is used for, we’re ready to add its code. Start by navigating inside the server
directory and install all the packages:
cd server
npm install
Import all the libraries we need and intialize them. This includes Express and a couple of middlewares (JSON and URL encoded body parser), and dotenv
which allows us to load values from the .env
file:
var express = require("express");
var bodyParser = require("body-parser");
var Pusher = require("pusher");
var app = express(); // Express server
app.use(bodyParser.json()); // for parsing the request body into JSON object
app.use(bodyParser.urlencoded({ extended: false })); // for parsing URL encoded request body
require("dotenv").config(); // initialize dotenv
Next, initialize the Pusher server component using the values from the .env
file inside your server
directory:
var pusher = new Pusher({
// connect to pusher
appId: process.env.APP_ID,
key: process.env.APP_KEY,
secret: process.env.APP_SECRET,
cluster: process.env.APP_CLUSTER
});
Next, add the route for testing if the server is working correctly:
app.get("/", function(req, res) {
res.send("all green...");
});
Next, add the route for authenticating user requests:
app.post("/pusher/auth", function(req, res) {
var socketId = req.body.socket_id;
var channel = req.body.channel_name;
var auth = pusher.authenticate(socketId, channel);
res.send(auth);
});
Note that in the code above, we haven’t really added any form of authentication. All we’re really doing is authenticating the user as they hit this route. This is not what you want to do for production apps. For production apps, you will most likely have some sort of user authentication before a user can use your app. That’s what you need to integrate into this code so you can ensure that the users who are making requests to your Pusher app are real users of your app.
Next, make the server listen to the port indicated in the .env
file:
var port = process.env.PORT || 5000;
app.listen(port);
Lastly, update the .env
file and update it with your Pusher app details:
APP_ID=YOUR_PUSHER_APP_ID
APP_KEY=YOUR_PUSHER_APP_KEY
APP_SECRET=YOUR_PUSHER_APP_SECRET
APP_CLUSTER=YOUR_PUSHER_APP_CLUSTER
PORT=3000
Running the app
To run the app, you need to create an account on ngrok.com. Once you have an account, go to your account dashboard and download the ngrok binary for your operating system. Extract the zip file and you’ll see an ngrok
file. Execute that file from the terminal (Note: you’ll probably need to add execution permissions to it if you’re on Linux) to add your auth token:
./ngrok authToken YOUR_NGROK_AUTH_TOKEN
Once that’s done, run the server and expose port 3000 using ngrok:
node server.js
./ngrok http 3000
Ngrok will provide you with an https URL. Use that as the value for the authEndpoint
in the src/screens/HomeScreen.js
file:
componentDidMount() {
this.pusher = new Pusher("YOUR PUSHER APP KEY", {
authEndpoint: "YOUR_NGROK_HTTPS_URL/pusher/auth",
});
}
Lastly, navigate inside the root directory of the app and start it:
expo start
You can test the app on your machine using the emulator if you have a powerful machine. Personally, I tested it on my iOS and Android device so you might have better luck when running it on your device also.
Conclusion
That’s it! In this tutorial, you learned how to create a realtime photo-sharing app with React Native and Pusher. Along the way, you learned how to use Expo’s Camera API, Imgur API to anonymously upload images, and Pusher to send and receive data in realtime.
You can find the app’s source code in this GitHub repo.
12 November 2018
by Wern Ancheta