This is an excerpt from my book, the React Native Cookbook, 2nd Edition, from Packt Publishing, due to be published this winter.

Push notifications are a great way to provide a constant feedback loop between your app and the user by continually providing app specific data that’s relevant to the user. Messaging apps send notifications when new messages arrive. Reminder apps display a notification to remind the user of a task at a specific time or location. A podcast app might use notifications to inform the user that a new episode has been published. A shopping app could use notifications to alert the user to checkout a limited time deal.

Push notifications are a proven way to increase user interaction and retention. If your app makes use of time sensitive or event based data, push notifications could be a valuable asset. In this tutorial, we’l be using Expo’s push notification implementation, which simplifies some of the setup that would be required with a vanilla React Native project. If the needs or your app demands a non-Expo project, I would recommend considering the react-native-push-notification package at https://github.com/zo0r/react-native-push-notification.

In this tutorial, we’ll be making a very simplistic messaging app with push notifications. We’ll request proper permissions, then register a push notification token to an Express server we’ll be building. We’ll also render a TextInput for the user to enter a message into. When the Send button is pressed, the message will be sent to our server, and the server will send a push notification via Expo’s push notification server, with the message from the app, to all devices that have registered a token with our Express server.

Thanks to Expo’s built-in push notification service, the complicated work of creating a notification for each native device is offloaded to an Expo hosted backend. The Express server we build in this tutorial will just pass off JSON objects for each push notification to the Expo backend, and the rest is taken care of. The following diagram from the Expo docs illustrates the lifecycle of a push notification.

While implementing push notifications using Expo is less setup work than it would otherwise be, the requirements of the technology still mean we will need to run a server for handling registering for and sending notifications, which means this tutorial will be a little longer than most. Let’s get started!

Getting Ready

One of the first things we’ll need to do in this app is request permission from the device to use push notifications. Unfortunately push notification permissions do not work properly in the emulators, so a real device will be needed to test this app.

We’ll also need to be able to access the push notification server from an address outside of localhost. In a real world setup, the push notification server would already have a public url, but in a development environment, the easiest solution is to create a tunnel that exposes the development push notification server to the internet. We’ll be using the tool ngrok for this purpose, since it is a mature, robust, and incredible easy to use solution. You can read more about the software at https://ngrok.com.

First install ngrok globally via npm using the following command:

npm i -g ngrok

Once it’s installed, you can create a tunnel from the internet to a port on your local machine by executing ngrok with the https parameter:

ngrok https [port-to-expose]

We’ll use this command later in the tutorial to expose the development server.

Let’s create a new app for this tutorial using Expo. We’ll call it push-notifications . We’re going to need three extra npm packages for this tutorial: express for the push notification server, esm for using ES6 syntax support on the server, and expo-server-sdk for processing the push notifications. Install them with yarn:

yarn add express esm expo-server-sdk

or npm:

npm install express esm expo-server-sdk --save

How to

1. Let’s start with building the App. We’ll start with adding the dependencies we’ll need to App.js .

import React from 'react';

import {

StyleSheet,

Text,

View,

TextInput,

Modal,

TouchableOpacity

} from 'react-native';

import { Permissions, Notifications } from 'expo';

2. We’re going two declare to constants for the API endpoints on our server, but the url will be generated by ngrok when we run the server later in the tutorial, so we’ll update the value of these constants at that point.

const PUSH_REGISTRATION_ENDPOINT = 'http://generated-ngrok-url/token';

const MESSAGE_ENPOINT = 'http://generated-ngrok-url/message';

3. Let’s create the App component and initialize the state object. We’ll need a notification property to hold notifications received by the Notifications listener, which we will define in a later step.

export default class App extends React.Component {

state = {

notification: null,

messageText: ''

} // Defined in following steps

}

4. Let’s define the method that will handle registering the push notification token to the server. We’ll ask for notification permission from the user via the askAsync method on the Permissions component. If permission is granted, get the token from the device from the getExpoPushTokenAsync method of the Notifications component.

registerForPushNotificationsAsync = async () => {

const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);

if (status !== 'granted') {

return;

}

let token = await Notifications.getExpoPushTokenAsync(); // Defined in following steps

}

5. Once we have the appropriate token, we’ll send it over to the push notification server for registration. We will then make a POST request to the PUSH_REGISTRATION_ENDPOINT sending a token object and user object in the request body. I’ve hard coded the values in the user object, but in a real app this would be the metadata you’ve stored for the current user.

registerForPushNotificationsAsync = async () => {

// Defined in above step return fetch(PUSH_REGISTRATION_ENDPOINT, {

method: 'POST',

headers: {

'Accept': 'application/json',

'Content-Type': 'application/json',

},

body: JSON.stringify({

token: {

value: token,

},

user: {

username: 'warly',

name: 'Dan Ward'

},

}),

}); // Defined in next step

}

6. After the token is registered, we’ll setup an event listener to listen to any notifications that occur while the app is open and foregrounded. In certain cases we will need to manually handle displaying the information from an incoming push notification. Check the How it Works section at the end of this tutorial for more on why this is necessary and how it can be leveraged. We’ll define the handler in the next step.

registerForPushNotificationsAsync = async () => {

// Defined in above steps this.notificationSubscription = Notifications.addListener(this.handleNotification);

}

7. Whenever a new notification is received, the handleNotification method will be run. We’ll just store the new notification passed to this callback on the state object for later use in the render function.

handleNotification = (notification) => {

this.setState({ notification });

}

8. We want our app to ask for permission to use push notifications, and to register the push notification token when the app launches. We’ll utilize the componentDidMount lifecycle hook to run our registerForPushNotificationsAsync method.

componentDidMount() {

this.registerForPushNotificationsAsync();

}

9. The UI will be very minimal to keep the tutorial simple. It’s made up of a TextInput for the message text, a Send button for sending the message, and a View for displaying any notifications heard by the notification listener.

render() {

return (

<View style={styles.container}>

<TextInput

value={this.state.messageText}

onChangeText={this.handleChangeText}

style={styles.textInput}

/>

<TouchableOpacity

style={styles.button}

onPress={this.sendMessage}

>

<Text style={styles.buttonText}>Send</Text>

</TouchableOpacity>

{this.state.notification ?

this.renderNotification()

: null}

</View>

);

}

10. The TextInput component defined in the previous step is missing the method it needs for its onChangeText prop. Let’s create that method next. It just saves the text input by the user to this.state.messageText so it can be used by the value prop and elsewhere.

handleChangeText = (text) => {

this.setState({ messageText: text });

}

11. The TouchableOpacity component’s onPress prop is calling the sendMessage method to send the message text when the user presses the button. In this function we’ll just take the message text and POST it to the MESSAGE_ENDPOINT on our push notification server. The server will handle things from there. Once the message is sent we’ll clear the messageText property on state .

sendMessage = async () => {

fetch(MESSAGE_ENPOINT, {

method: 'POST',

headers: {

Accept: 'application/json',

'Content-Type': 'application/json',

},

body: JSON.stringify({

message: this.state.messageText,

}),

});

this.setState({ messageText: '' });

}

12. The last piece we need for the App is the styles. Since this tutorial is about notifications and not styling a React Native app, we won’t be deep-diving into the styles and layout, though nothing very complicated is happening here. You can read more about React Native’s StyleSheet component in the docs here, and how flexbox works in React Native in this doc.

const styles = StyleSheet.create({

container: {

flex: 1,

backgroundColor: '#474747',

alignItems: 'center',

justifyContent: 'center',

},

textInput: {

height: 50,

width: 300,

borderColor: '#f6f6f6',

borderWidth: 1,

backgroundColor: '#fff',

padding: 10

},

button: {

padding: 10

},

buttonText: {

fontSize: 18,

color: '#fff'

},

label: {

fontSize: 18

}

});

13. With the React Native App portion out of the way, let’s move to the server portion. First we will create a new server folder in the root of the project with an index.js file inside of it. Let’s start by importing express to run the server and expo-server-sdk to handle registering and sending push notifications. We will create an express server app and store in in the const app , and new instance of the expo server SDK in the const expo . We’ll also add a savedPushTokens array for storing any tokens that are registered with the React Native app, and a PORT_NUMBER const for the port we want to run the server on.

import express from 'express';

import Expo from 'expo-server-sdk'; const app = express();

const expo = new Expo(); let savedPushTokens = [];

const PORT_NUMBER = 3000;

14. Our server will need to expose two endpoints (one for registering tokens, and one for accepting messages from the React Native app), so we’ll create two functions that will be executed when these routes are hit. We’ll define the saveToken function first. It just takes a token, checks if it’s stored in the savedPushTokens array, and pushes it to the array if it isn’t there already.

const saveToken = (token) => {

if (savedPushTokens.indexOf(token === -1)) {

savedPushTokens.push(token);

}

}

15. The other function our server needs is a handler for sending the push notifications when a message is received from the React Native app. We’ll loop over all of the tokens that have been saved to the savedPushTokens array and create a message object for each token. Each message object has a title of “Message received!” which will display in bold on the push notification, and the message text as the body of the notification.

const handlePushTokens = (message) => {

let notifications = [];

for (let pushToken of savedPushTokens) {

if (!Expo.isExpoPushToken(pushToken)) {

console.error(`Push token ${pushToken} is not a valid Expo push token`);

continue;

}

notifications.push({

to: pushToken,

sound: 'default',

title: 'Message received!',

body: message,

data: { message }

})

} // Defined in following step

}

16. Once we have an array of messages we can send them to Expo’s server, which in turn will send the push notification to all registered devices. We’ll send the messages array via the expo server’s chunkPushNotifications and sendPushNotificationsAsync methods, and console.log the success receipts or an error as appropriate to the server console. There’s more on how this works in the How it Works section at the end of this tutorial.

const handlePushTokens = (message) => {

// Defined in previous step let chunks = expo.chunkPushNotifications(notifications); (async () => {

for (let chunk of chunks) {

try {

let receipts = await expo.sendPushNotificationsAsync(chunk);

console.log(receipts);

} catch (error) {

console.error(error);

}

}

})();

}

17. Now that we have the functions defined for handling push notifications and messages, let’s expose those functions by creating API endpoints. If you’re not familiar with Express, it’s a powerful and easy to use framework for running a web server in Node. You can quickly get up to speed on the basics of routing with the Basic Routing docs at https://expressjs.com/en/starter/basic-routing.html.

We’ll be working with JSON data, so the first step will be applying the JSON parser middleware with a call to express.json() .

app.use(express.json());

18. Even though we won’t really be using the root path ( / ) of the server, it’s good practice to define one. We’ll just respond with a message that the server is running.

app.get('/', (req, res) => {

res.send('Push Notification Server Running');

});

19. First let’s implement the endpoint for saving a push notification token. When a POST request is sent to the /token endpoint, we’ll pass the token value to the saveToken function and return a response stating that the token was received.

app.post('/token', (req, res) => {

saveToken(req.body.token.value);

console.log(`Received push token, ${req.body.token.value}`);

res.send(`Received push token, ${req.body.token.value}`);

});

20. Likewise the /message endpoint will take the message from the request body and pass it to the handlePushTokens function for processing. Then, we’ll send back a response that the message was received.

app.post('/message', (req, res) => {

handlePushTokens(req.body.message);

console.log(`Received message, ${req.body.message}`);

res.send(`Received message, ${req.body.message}`);

});

21. The last piece to the server is the call to Express’s listen method on the server instance, which will start the server.

app.listen(PORT_NUMBER, () => {

console.log('Server Online on Port ${PORT_NUMBER}');

});

22. We’re going to need a way to start the server, so we’ll add a custom script to the package.json file called serve. Open the package.json file and update it to have a scripts object with a new serve script. With this added, we can run the server with yarn via the yarn run serve command or with npm via the npm run serve command.

{

"main": "node_modules/expo/AppEntry.js",

"private": true,

"dependencies": {

"esm": "^3.0.28",

"expo": "^27.0.1",

"expo-server-sdk": "^2.3.3",

"express": "^4.16.3",

"react": "16.3.1",

"react-native": "https://github.com/expo/react-native/archive/sdk-27.0.0.tar.gz"

},

"scripts": {

"serve": "node -r esm server/index.js"

}

}

23. We’ve got all the code in place, let’s use it! As mentioned previously, push notification permissions do not work properly on the emulator, so a real device will be needed to test the push notification functionality. First we’ll fire up our newly created server by running yarn run serve or npm run serve . You should be greeted by the “Server Online” message we defined in the listen method call in step 21.

24. Next we’ll need to run ngrok to expose our server to the internet. Open a new terminal window and create an ngrok tunnel with the command ngrok http 3000 . You should see the ngrok interface in the terminal. This displays the urls generated by ngrok. In this case, ngrok is forwarding my server located at http://localhost:3000 to the url http://ddf558bd.ngrok.io . Let’s copy that url.

25. You can test that the server is running and accessible from the internet by visiting the generated URL in a browser. Navigating directly to this url behaves exactly the same as navigating to http://localhost:300 , which means the GET endpoint we defined in step should run. That function returns the string “Push Notification Server Running,” and should display in your browser.

26. Now that we’ve confirmed the server is running, let’s update the React Native app to use the correct server URL. In step 2 we added to constants for holding our API endpoints, but we didn’t have the correct URL yet. Let’s update these URLs to reflect the tunnel url generated by ngrok.

const PUSH_REGISTRATION_ENDPOINT = 'http://ddf558bd.ngrok.io/token';

const MESSAGE_ENPOINT = 'http://ddf558bd.ngrok.io/message';

27. As mentioned previously, you’ll need to run this app on a real device for the permissions request to work correctly. As soon as you open the app, you should be prompted by the device, asking if you’d like to allow the app to send notifications.

28. As soon as Allow is selected the push notification token will be sent to the server’s /token endpoint to be saved. This should also print the associated console.log statement in the server terminal with the saved token. In this case, my iPhone’s push token is the string ExponentPushToken[g5sIEbOm2yFdzn5VdSSy9n] .

29. At this point, if you have a second Android or iOS device, go ahead and open the React Native app on this device as well. If not, don’t worry. There’s another easy way to test that our push notification functionality is working without using a second device.

30. Use the React Native app to send a message. If you’ve got a second device that has registered a token with the server it should receive a push notification corresponding to the newly sent message! You should also see two new console.log s in the server: one that displays the received message, and another that displays the “receipts” array received back from the Expo servers. Each receipt object in the array has a status property with the value ‘ok’ if the operation was successful.

31. If you don’t have a second device to test on, you can use Expo’s Push Notification Tool, hosted at https://expo.io/dashboard/notifications. Just copy the push token from the server terminal and paste it into the input labeled “EXPO PUSH TOKEN (FROM YOUR APP).” To emulate a message sent from our React Native app, set the MESSAGE TITLE to “Message received!”, MESSAGE BODY to the message text you’d like to send, and check the Play Sound checkbox. If you’d like you can also emulate the data object by adding a JSON object with a key of message and a value of your message text, i.e. { “message”: “This is a test message.” } .

How it Works

The app we built here is a little contrived, but the core concepts needed to request permissions, register tokens, accept app data, and send push notifications in response to app data are all there.

In step 4, we defined the first part of the registerForPushNotificationsAsync function. We began by asking the user for their permission to send them notifications from our app via the Permissions.askAsync method, passing in the enum for the push notifications permission, Permissions.NOTIFICATIONS . We then saved the status property from the resolved return object, which will have the value ‘granted’ if the user granted permission. If we don’t get permission, we return right away, otherwise we get the token from Expo’s Notifications component by calling getExpoPushTokenAsync . This function returns a token string, which will be in the format:

ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]

In step 5, we defined the POST call to the server’s registration endpoint ( /token ). This function sends the token in the request body, which is then saved on the server using the saveToken function defined in step 14.

In step 6, we create an event listener that will listen for any new incoming push notifications. This is done by calling Notifications.addListener and passing in a callback function to be executed every time a new notification is received. On iOS devices the system is designed to only produce a push notification if the app sending the push notification isn’t open and foregrounded. That means, if you try to send your user a push notification while they’re currently using your app, they will never receive it.

To overcome this issue, Expo suggests manually displaying the push notification data from within your app. This Notifications.addListener method was create to fulfill this need. When a push notification is received the callback passed to addListener will be executed and will receive the new notification object as a parameter. In step 7 we save this notification to state so that the UI will be re-rendered accordingly. We only display the message text in a Text component in this tutorial, but you could also a modal for a more notification-like presentation.

In step 11, we created the sendMessage function, which posts the message text stored on state to the server’s /message endpoint. This will execute the server function handlePushToken defined in step 15.

In step 13, we started working on the server, which utilizes Express and the Expo server SDK. A new server is created with express by calling express() directly, as a local const, usually named app by convention. We were able to create a new Expo server SDK instance with new Expo() , storing it in the const expo . We later use the Expo server SDK to send the push notification using expo , define routes using app in steps 17–20, and initiate the server by calling app.listen() in step 22.

In step 14, we defined the saveToken function, which will be executed when the /token endpoint is used by the React Native app to register a token. This function saves the incoming token to the savedPushTokens array, to be used later when a message arrives from a user. In a real app, this is where you would likely want to save the tokens to a persistent database of some kind, such as SQL, MongoDB, or Firebase Database.

In step 15, we started defining the handlePushTokens function which runs when the React Native app uses the /message endpoint. The function loops over the savedPushTokens array for processing. Each token is checked for validity using the Expo server SDK method isExpoPushToken , which takes in a token and returns true if the token is valid. If it’s invalid we log an error to the server console. If it’s valid we push a new notification object onto the local notifications array for batch processing in the next step. Each notification object requires a to property with the value set to a valid Expo push token. All other properties are optional. The optional properties we set were:

sound — can be ‘default’ to play default notification sound or null for no sound.

for no sound. title — the title of the push notification, usually displayed in bold.

body — the body of the push notification.

data — a custom data JSON object.

In step 16, we use the Expo server SDK instance method chunkPushNotifications to create an array of data chunks optimized for sending to Expo’s push notification server. We then loop over the chunks, and send each chunk to Expo’s push notification server via the expo.sendPushNotificationsAsync method. It returns a promise that resolves to an array of receipts for each push notification. If the process was successful there will be an object of { status: ‘ok’ } for each notification in the array.

This endpoint’s behavior is simpler than a real server would probably be, because most message apps would have a more complicated way of handling a message. At the very least there would likely be a list of recipients that would dictate which registered devices would in turn receive a particular push notification. The logic was intentionally kept simple to portray the basic flow.

In step 18, we defined the first accessible route on our server, the root (/) path. Express provides the get and post helper methods for easily making API endpoints for GET and POST requests respectively. The callback function receives a request object and response object as parameters. All server urls need to respond to the request, otherwise the request would timeout. The response is sent via the send method on the response object. This route doesn’t process any data, so we just returned the string indicating that our server is running.

In steps 19 and 20, we defined POST endpoints for /token and /message , which will execute saveToken and handlePushTokens respectively. We also added console.log statements to each to log the token and the message to the server terminal for ease of development.

In step 21, we defined the listen method on our Express server, which starts the sever. The first parameter is the port number to listen for requests on, and the second parameter is a callback function, usually used to console.log a message to the server terminal that the server has been started.

In step 22, we added a custom script to the package.json file of our project. Any command that can be run in the terminal can be made a custom npm script by adding a scripts key to the package.json file set to an object whose keys are the name of the custom script, and whose values are the command that should be executed when that custom script is run. In this tutorial we defined a custom scripted named serve that runs the command node -r esm server/index.js . This command runs our server file ( server/index.js ) with Node using the esm npm package we installed at the beginning of this tutorial. Custom scripts can be executed with npm:

npm run [custom-script-name]

or yarn:

yarn run [custom-script-name]

Push notifications can be complicated, but thankfully Expo simplifies the process in a number of ways. There’s great documentation on Expo’s push notification service that covers specifics on notification timing, Expo server SDKs in other languages, and how to implement notifications over HTTP/2. I encourage you to read more at https://docs.expo.io/versions/latest/guides/push-notifications.

You can also take a look at the source code for this tutorial on GitHub at https://github.com/warlyware/react-native-cookbook/tree/master/chapter-5/push-notifications.