Handling Android Back Button Events in React Native with Custom Components

Scott Luptowski
Made by Many
Published in
5 min readOct 28, 2016

As you build a React Native app across multiple platforms, one of the key differences between your iPhone and Android apps is that Android phones have a separate system back button that users can touch at any time.

As an Android user moves through your app and goes deeper and deeper into new scenes, the expected behavior is that a press on the back button returns the user to the previous screen, unless they are on the first screen, where a press on the back button means they exit the app.

Take this from the Android Developer Guide:

The system Back button is used to navigate, in reverse chronological order, through the history of screens the user has recently worked with. It is generally based on the temporal relationships between screens, rather than the app’s hierarchy.

React Native and Android’s Back Button

The default React Native behavior for the back button is that pressing the button will exit the app.

React Native provides a BackAndroid module that gives developers control of back button behavior. This module is in charge of registering callbacks to decide what to do when the back button is pressed. When a user taps on the back button, the app runs through every registered event listener and only stops if one callback returns true. If no callbacks return true, the button press quits your app.

To implement the Back Button’s reverse chronological screen navigation as discussed by the Android Developer Guide, for example, a developer could write a callback that checks the current number of routes before deciding whether or not to exit the app:

This works well enough if this function is the only registered callback. But what happens when you need to add more and more event listeners to cover the business logic for different use cases? After all, the same Android developer guide writes that the back button can be used to dismiss dialogs and popups or to dismiss contextual action bars.

Imagine a screen that performs an expensive calculation, or multiple network calls, or a payment page where the user has submitted credit card information and you are waiting for a reply from the server. If someone presses the back button here, you may want to handle this press differently.

React Native’s out of the box experience means that each of these situations requires adding a new event listener when it is necessary and manually removing it when it is not. It becomes mentally taxing to keep a list in your head of which listeners are mounted and whether the functions return true values or false values.

On top of that, forgetting to remove a listener could lead to strange bugs! Without enough defensive coding, you could end up running your back-button-press on credit-card-submission function even if that view is no longer visible.

An easier way to think about this problem and avoid these bugs is by reframing the problem: instead of adding new listeners when your business logic changes, only ever mount one listener, and change that function when your business logic changes.

React Side Effect

Handling back button behavior is a perfect use case for Dan Abramov’s React Side Effect library. React Side Effect allows programmers to create special higher-order components that are designed to perform side effects when they are rendered and updated by your app.

One popular package that is implemented with React Side Effect is React Document Title, which allows programmers to change browser window’s title by passing a property to a <DocumentTitle> component, like so:

React Side Effect fits with React’s component-based architecture because it allows wrapping of imperative APIs (like React Native’s BackAndroid or changing a document’s title) into components that better fit React’s programming model and abstractions.

The basic idea with React Side Effect is to create a React component that accepts any arbitrary props you define. This component is then wrapped with the withSideEffect function and two or three additional functions you implement. These functions are called, one after the other, every time your app re-renders and then emit a side effect based on the logic you provide. Here are their conventional names and behavior:

  • reducePropsToState — The argument passed to this function is an array of the properties to all of the mounted instances of your component
  • handleStateChangeOnClient — This function is called with the return value of reducePropsToState and is used to emit a side effect
  • mapStateOnServer (optional)This function behaves like handleStateChangeOnClient but is invoked on the server if your React app uses server rendering

A simple implementation of DocumentTitle may look like this:

Programmers can implement these functions in any manner they choose: some use cases could lend themselves to aggregating properties from many components together into one value. Other use cases, like this one, are only concerned with the value provided to the last component.

In either case, the return value of reducePropsToState is passed as the argument to handleStateChangeOnClient, where we use this argument to change the title of the document.

Implementing our Desired Android Behavior

Let us take a look at a more complicated example. Here are the basic rules: no matter how many AndroidBackButton components are rendered by our app at once, only the function provided as a property to the innermost rendered component should be executed. The return value of this function determines whether or not an Android button press quits the app.

This is implemented by ensuring that there is only ever one callback registered with React Native’s BackAndroid library. This callback will in turn call the function provided to the innermost AndroidBackButton instance. We will use React Side Effect to update the function that is called inside the event listener.

If you try this code as-is in your app, you may be surprised to find that it does not fully work. Namely, the reducePropsToState function executes while handleStateChangeOnClient does not. Why not?

The answer has to do with React Native’s execution environment and the way that React Side Effect determines how to invoke functions. Because React Side Effect can run on both the client and server, the library checks to see which environment it is in by asking if it has access to the DOM when it is time to emit a change.

React Native, as we know, does not render a DOM! Because React Native’s environment does not satisfy the conditions of this check, we cannot place our function as the second handleStateClientOnClient argument and must instead position it as the optional third argument, the rather confusingly (for us) named mapStateOnServer.

Despite the name of the function and the implication in a request-response world that this function is only called once, this function is called whenever a component is mounted, unmounted, or changed. We can rewrite the bottom of our above example as this:

With a working version of our component, we can now control back button behavior by rendering the component anywhere in our app’s view hierarchy. We can define even define a base behavior (for instance, navigating back unless the user is on the first screen of your app) and then override that behavior in certain contexts. All without manually juggling the addition and subtraction of event listeners!

In this example, the default behavior is to go back to the prior screen. If the view is loading, a click on the button will show a warning and prevent quitting the app.

In conclusion

I hope you learned something about React Side Effect and how higher order components can let you take advantage of React’s component abstraction to wrap imperative APIs and deal with changing data in your programs.

I’m open sourcing this component as “react-native-android-back-button.” View the source here, install it with npm install react-native-android-back-button, and let me know what you think!

--

--