Using redux-saga to handle (and test) Push Notifications in React Native
— JavaScript, Software Development — 6 min read
Half way through December 2019 I was asked to build a new React Native app for the company I maintain an existing React Native app for and I was really excited to do so, seeing it as a means to do some greenfield development and do things properly from the start.
As part of the maintenance work I had been building a React component library for the existing native app, in order to abstract the components from the app logic and make them re-usable (should we end up building a new app, good foresight on my part there!).
While building that component library I had used Expo as a means to speed up development and found it an amazing tool for React Native, so I decided I would build this new project with Expo as I was given a deadline of 2 months to build the app.
The new app’s stack consisted of:
- React — Because React is a great component library
- React Native — Because how else would I build a React Native app?
- Expo — Offers amazing deployment functionality and has a great set of libraries for speeding up development
- My component library — To re-use as many components as possible from the company’s existing React Native app
- Redux — While people argue that React Hooks does away with the need for it I think it’s still a great state management solution and it’s more widespread that React Hooks are meaning other developers can pick up the codebase easily
- Redux Saga — Because the existing app had a lot of side-effects and I was going to be re-using some of that app’s logic
- React Navigation — The existing app used it and I was going to be re-using some of the navigation code
One of Expo’s benefits, aside from its awesome deployment tools (more on that in a future blog post) is that it also makes push notifications much easier to deal with.
You still need to jump through the usual hoops with Apple to create a .p8
certification but once this is uploaded to Expo’s servers you can use a simple API call to send the notifications, provided you know the Expo push token of the device to send it to.
This API comes in very handy when testing or debugging your app as you can trigger the notifications easily via a POST request.
I found this extremely useful when I introduced notification categories to the app as the category ID changes between apps in Expo and the standalone versions.
redux-saga Basics
redux-saga is ‘an alternative side effect model for Redux apps’ but it’s the only side effect model for Redux I’ve ever known! Although I believe the term generally used for this type of abstraction is an ‘Epic’.
redux-saga offers a set of functions for dealing with the Redux store and process forking, the most common are:
put
— Dispatch an action to the Redux storetakeEvery
— Perform a specified function (task) every time the action is added to the Redux storetakeLatest
— Perform a specified function every time the action is added to the Redux store but if there’s a currently running task for that action kill that before running the new onetake
— Wait for the specified action to be added to the Redux store and store that action (if we need to)call
— Call a function (the function and it’s arguments are params sofunction(arg1, arg2)
would becall(function, arg1, arg2)
)fork
— Fork the process for the specified task (useful for long running tasks), you can usecancel
to cancel that task
redux-saga uses generator functions to handle the operations it needs to perform, which to summarise (poorly) means that it returns an iterator with each operation being yield
ed so the next operation can be performed on a call of next()
.
The use of the generator functions makes it incredibly easy to test a redux-saga function as you can call next()
and ensure that the outcome is what you expect to happen.
These generator functions aren’t called directly however but instead called by watcher functions which take
actions being dispatched to the Redux store and then iterate over the generator function’s operations with one of those operations dispatching an action to the Redux store to update the app state on the outcome.
A common pattern would be:
- The app dispatches a ‘request’ action (such as a submit form) which gets picked up by the watcher which then calls the generator function (saga)
- The saga will then do a bunch of operations (such as an API call to send the form data to a server) and if successful will
put
a ‘response’ action or an ‘error’ action is unsuccessful - The app’s reducer then takes the action dispatched by the saga and following the standard Redux flow updates the app’s state
This approach works well as you can unit test every aspect of it:
- The actions can be tested to ensure they return the correct object
- The reducer can be tested to ensure on processing the response action the app’s state is correctly updated
- The saga can be tested to ensure that on a successful API call it dispatches the response action and on the API call throwing an exception it dispatches the error action
- You can use something like Enzyme to ensure when the button is pressed that it dispatches the request action
So you know how redux-saga works, now let’s cover how push notifications work in Expo apps.
Push Notifications using Expo
In order to handle push notifications in your app using Expo you need to create an event listener using Expo’s Notifications.addListener
function.
This event listener’s callback will then receive the following object on a notification being sent:
- remote — Was the notification a remote or local one?
- data — The data object sent with the notification
- actionId — The action the user pressed (if categories are used)
- origin — Used to determine if the notification was received when the app was open or not
- userText — If the notification allows user input this stores that input
The event listener works well for React components as you can use the useEffect
hook to set up the listener when the component is first mounted and then use the functions created by useState
to update the application state on receiving a notification.
Things get a little tricky however when you want to abstract the notification handling logic out into a separate process running in the background.
Handling Push Notifications using redux-saga
I made the decision to abstract out the push notification logic in the app into a saga as I made the design decision with the code base that all side-effects (which I consider push notifications to be) were to be handled via redux-saga meaning that there’s only one place any non-display related logic lives in the codebase.
The design decision came out of looking at the existing app code and how it had functions that called other functions without callbacks/promises/async await and how much of a ball-ache that was to follow.
The first step to handling push notifications with redux-saga is knowing how to deal with event listeners with redux-saga — Event Channels.
Event Channels are used essentially like a wrapper around the event listener function and expose an emit
function that can be used within the event listener callback to ‘dispatch’ to the channel.
After the channel is set up a watcher on the channel can then take
the events ‘dispatched’ to the channel and process them within the standard redux-saga manner using put
or call
.
This watcher does need to be in a while loop so it can take
every event and due to this the watcher process needs to be run as a separate task via fork
to prevent it from blocking the function that sets up the event channel.
Testing this approach
As stated previously the benefit of using redux-saga is that it’s incredibly easy to test everything and event channels are no different.
Before testing the event channel you’ll first need to find a means of calling the callback passed to the event listener that the event channel wraps, as I use Jest for my testing I used jest.mock
to mock out the expo
module with an object that provides Notifications.addListener
and then using mockImplentation
save the callback into an object I can call later.
Once you have the callback stored you can then create an instance of the event channel that will be used in a Promise that will resolve once the callback has been called and the event channel has an event ‘dispatched’ to it.
Once this Promised is resolved you can then assert on the event that was taken from the event channel.
Here’s an example of such a test:
You can then test the watcher saga in the same way that you would test other redux-saga generators but without the last assertion that the generator is done as the while
loop prevents that.
And you can test the saga that creates the event channel, watcher and fork
s them in the same way you test your other redux-saga generators.
Summary
By using an event channel to manage the push notification listener used by Expo you can handle the push notification objects as you would any other action within Redux.
By using redux-saga to handle the listening for and processing of push notification you can create an easy to maintain and easy to test codebase which is key in the ever evolving world of React and React Native.