Integrating React Navigation Back Button with a WebView

Author

Spencer Carli

Developer, cat dad, and devout pizza lover. Teaching at React Native School and building apps with Handlebar Labs.

Last Updated: July 17, 2019

Let's say that you have an app that embeds a webview for an entire screen and the user can interact with it/navigate to subsequent navigation pages within it.

By default, if the user presses a back button they'll be taken back to the previous screen in your app - not the previous web page. That's not really intuitive for a user. Here's an example:

Starter Code

index.js

import React from 'react';
import { createAppContainer, createStackNavigator } from 'react-navigation';

import Index from './IndexScreen';
import Details from './DetailsScreen';

const App = createStackNavigator({
  Index: {
    screen: Index,
    navigationOptions: {
      headerTitle: 'Index',
    },
  },
  Details: {
    screen: Details,
    navigationOptions: ({ navigation }) => ({
      headerTitle: 'Details',
    }),
  },
});

export default createAppContainer(App);

IndexScreen.js

import React from 'react';
import { View, Button } from 'react-native';

export default ({ navigation }) => (
  <View>
    <Button
      title="Go To Details Screen"
      onPress={() => navigation.navigate('Details')}
    />
  </View>
);

DetailsScreen.js

import React, { useRef } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { WebView } from 'react-native-webview';

export default ({ navigation }) => {
  return (
    <WebView
      source={{ url: 'https://www.google.com/search?q=react+native+school' }}
      startInLoadingState
      renderLoading={() => (
        <View style={{ flex: 1, alignItems: 'center' }}>
          <ActivityIndicator size="large" />
        </View>
      )}
      allowsBackForwardNavigationGestures
    />
  );
};

Strategy

What we're going to do is hijack the "back" press in the navigator by passing our own press handler via params. We'll know what data we need to pass by leveraging the onNavigationStateChange handler in a WebView.

Navigator Setup

From our component we're going to pass our data on the headerLeftInfo param. If that param exists we'll pass along that title/onPress otherwise we'll use the default.

We can get the default React Navigation back button by import it from react-navigation.

index.js

import React from 'react';
import {
  createAppContainer,
  createStackNavigator,
  HeaderBackButton, // new import
} from 'react-navigation';

import Index from './IndexScreen';
import Details from './DetailsScreen';

const App = createStackNavigator({
  Index: {
    screen: Index,
    navigationOptions: {
      headerTitle: 'Index',
    },
  },
  Details: {
    screen: Details,
    navigationOptions: ({ navigation }) => ({
      headerTitle: 'Details',
      headerLeft: (props) => {
        const headerLeftInfo = navigation.getParam('headerLeftInfo');

        if (headerLeftInfo) {
          return (
            <HeaderBackButton
              {...props}
              title={headerLeftInfo.title}
              onPress={headerLeftInfo.onPress}
            />
          );
        }
        return <HeaderBackButton {...props} />;
      },
    }),
  },
});

export default createAppContainer(App);

In our navigationOptions we grab the param for headerLeftInfo. If it exists we render the HeaderBackButton with all the default props (so it behaves as normal as possible) and we override the title and the onPress.

If headerLeftInfo doesn't exist then we just render the default button with the default props.

Passing Data to the Navigator from the Details Screen

To programmatically go back in our WebView we'll need to assign a ref on which we can call goBack.

DetailsScreen.js

import React, { useRef } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { WebView } from 'react-native-webview';

export default ({ navigation }) => {
  const ref = useRef(null);

  return (
    <WebView
      ref={ref}
      source={{ url: 'https://www.google.com/search?q=react+native+school' }}
      startInLoadingState
      renderLoading={() => (
        <View style={{ flex: 1, alignItems: 'center' }}>
          <ActivityIndicator size="large" />
        </View>
      )}
      allowsBackForwardNavigationGestures
    />
  );
};

Then we have to listen to onNavigationStateChange on our WebView. It passes navState to the callback function. In that we care about the canGoBack property which will let us know if there is a previous screen to go back to.

If so, use our custom button handler. If not, use the default button handler.

DetailsScreen.js

export default ({ navigation }) => {
  const ref = useRef(null);

  return (
    <WebView
      ref={ref}
      source={{ url: 'https://www.google.com/search?q=react+native+school' }}
      startInLoadingState
      renderLoading={() => (
        <View style={{ flex: 1, alignItems: 'center' }}>
          <ActivityIndicator size="large" />
        </View>
      )}
      allowsBackForwardNavigationGestures
      onNavigationStateChange={(navState) => {
        if (navState.canGoBack) {
          navigation.setParams({
            headerLeftInfo: {
              title: '',
              onPress: () => ref.current.goBack(),
            },
          });
        } else {
          navigation.setParams({
            headerLeftInfo: null,
          });
        }
      }}
    />
  );
};

And there you have it!

Final Demo

Finished Code

index.js

import React from 'react';
import {
  createAppContainer,
  createStackNavigator,
  HeaderBackButton,
} from 'react-navigation';

import Index from './IndexScreen';
import Details from './DetailsScreen';

const App = createStackNavigator({
  Index: {
    screen: Index,
    navigationOptions: {
      headerTitle: 'Index',
    },
  },
  Details: {
    screen: Details,
    navigationOptions: ({ navigation }) => ({
      headerTitle: 'Details',
      headerLeft: (props) => {
        const headerLeftInfo = navigation.getParam('headerLeftInfo');

        if (headerLeftInfo) {
          return (
            <HeaderBackButton
              {...props}
              title={headerLeftInfo.title}
              onPress={headerLeftInfo.onPress}
            />
          );
        }
        return <HeaderBackButton {...props} />;
      },
    }),
  },
});

export default createAppContainer(App);

IndexScreen.js

import React from 'react';
import { View, Button } from 'react-native';

export default ({ navigation }) => (
  <View>
    <Button
      title="Go To Details Screen"
      onPress={() => navigation.navigate('Details')}
    />
  </View>
);

DetailsScreen.js

import React, { useRef } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { WebView } from 'react-native-webview';

export default ({ navigation }) => {
  const ref = useRef(null);

  return (
    <WebView
      ref={ref}
      source={{ url: 'https://www.google.com/search?q=react+native+school' }}
      startInLoadingState
      renderLoading={() => (
        <View style={{ flex: 1, alignItems: 'center' }}>
          <ActivityIndicator size="large" />
        </View>
      )}
      allowsBackForwardNavigationGestures
      onNavigationStateChange={(navState) => {
        if (navState.canGoBack) {
          navigation.setParams({
            headerLeftInfo: {
              title: '',
              onPress: () => ref.current.goBack(),
            },
          });
        } else {
          navigation.setParams({
            headerLeftInfo: null,
          });
        }
      }}
    />
  );
};
React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our classes and our private Slack community.