Skip to main content

Back Navigation

Handle the Android back button and iOS swipe gesture correctly with the channel's internal navigation.

Quick Start: Automatic

The simplest approach — let the SDK handle everything:

<BBChannel
channelUrl="https://demo.bbvms.com/ch/channel_name.json"
handleBackButton={true}
onMediaRequest={(mediaInfo) => setMedia(mediaInfo)}
/>

With handleBackButton={true}, the SDK automatically:

  1. Closes the fullscreen player if open
  2. Navigates back within the channel (detail page -> main)
  3. Falls through to the system default (exit app) when on the main page

This is sufficient for most full-screen channel setups. For anything more complex, use manual handling.

Manual: Using Refs

For custom back logic — e.g., integration with React Navigation, confirmation dialogs, or multi-step flows — handle back navigation manually.

import { BackHandler } from 'react-native';

function ChannelScreen() {
const channelRef = useRef<BBChannelRef>(null);
const [media, setMedia] = useState<BBMediaInfo | null>(null);
const canGoBackRef = useRef(false);

useEffect(() => {
const handler = BackHandler.addEventListener('hardwareBackPress', () => {
// Priority 1: Close fullscreen player
if (media) {
setMedia(null);
return true;
}
// Priority 2: Channel internal navigation
if (canGoBackRef.current) {
channelRef.current?.goBack();
return true;
}
// Priority 3: Let system handle it
return false;
});
return () => handler.remove();
}, [media]);

return (
<>
<BBChannel
ref={channelRef}
channelUrl="https://demo.bbvms.com/ch/channel_name.json"
onCanGoBackChange={(canGoBack) => {
canGoBackRef.current = canGoBack;
}}
onMediaRequest={(mediaInfo) => setMedia(mediaInfo)}
/>
<BBFullscreenPlayer
media={media}
visible={media !== null}
onClose={() => setMedia(null)}
/>
</>
);
}

Why Refs Instead of State?

This is a common pitfall. Consider this broken example:

// BAD: canGoBack will be stale in the BackHandler callback
const [canGoBack, setCanGoBack] = useState(false);

useEffect(() => {
const handler = BackHandler.addEventListener('hardwareBackPress', () => {
// canGoBack is captured at the time this effect ran.
// Rapid back presses may read an outdated value!
if (canGoBack) {
channelRef.current?.goBack();
return true;
}
return false;
});
return () => handler.remove();
}, []); // Empty deps = canGoBack is always false here

The BackHandler listener captures the canGoBack value from when the effect ran. Even if you add canGoBack to the dependency array, there's a brief window during re-registration where a back press could be missed.

Refs solve this because canGoBackRef.current always returns the latest value:

// GOOD: ref always has the current value
const canGoBackRef = useRef(false);

useEffect(() => {
const handler = BackHandler.addEventListener('hardwareBackPress', () => {
if (canGoBackRef.current) { // Always current
channelRef.current?.goBack();
return true;
}
return false;
});
return () => handler.remove();
}, []); // No dependency needed

Priority Order

When the back button is pressed, handle events in this order:

  1. Close fullscreen player — If a player is open in fullscreen/modal
  2. Channel back navigation — If the channel is on a sub-page (detail, overview, search)
  3. App navigation — Pop the screen, switch tabs, or exit
const handler = BackHandler.addEventListener('hardwareBackPress', () => {
// 1. Player
if (media) {
setMedia(null);
return true;
}
// 2. Channel
if (canGoBackRef.current) {
channelRef.current?.goBack();
return true;
}
// 3. App (return false to let React Navigation or system handle it)
return false;
});

React Navigation Coordination

When the channel is inside a React Navigation stack or tab navigator, use useFocusEffect to register the back handler only when the screen is focused:

import { useFocusEffect } from '@react-navigation/native';

function VideoScreen({ navigation }) {
const channelRef = useRef<BBChannelRef>(null);
const canGoBackRef = useRef(false);

useFocusEffect(
React.useCallback(() => {
const handler = BackHandler.addEventListener('hardwareBackPress', () => {
if (canGoBackRef.current) {
channelRef.current?.goBack();
return true;
}
// Let React Navigation handle it (pop screen, switch tab, etc.)
return false;
});

return () => handler.remove();
}, [])
);

return (
<BBChannel
ref={channelRef}
channelUrl="https://demo.bbvms.com/ch/channel_name.json"
onCanGoBackChange={(val) => {
canGoBackRef.current = val;
}}
/>
);
}
warning

Do not use handleBackButton={true} together with your own BackHandler. They will conflict. Choose one approach.

iOS Swipe Gesture

On iOS, users expect to swipe from the left edge to go back. When the channel has internal navigation, disable the native swipe gesture to prevent conflicts:

import { useFocusEffect } from '@react-navigation/native';

function VideoScreen({ navigation }) {
const canGoBackRef = useRef(false);

// Disable iOS swipe-back when channel can go back
useFocusEffect(
React.useCallback(() => {
navigation.setOptions({
gestureEnabled: !canGoBackRef.current,
});
}, [])
);

return (
<BBChannel
channelUrl="https://demo.bbvms.com/ch/channel_name.json"
onCanGoBackChange={(canGoBack) => {
canGoBackRef.current = canGoBack;
navigation.setOptions({ gestureEnabled: !canGoBack });
}}
/>
);
}

When canGoBack is true, the iOS swipe gesture is disabled so swiping doesn't pop the screen while the user is on a detail page. When they return to the main page, the gesture is re-enabled.

Custom Header Back Button

If you have a custom header with a back button:

function VideoScreen() {
const channelRef = useRef<BBChannelRef>(null);
const [canGoBack, setCanGoBack] = useState(false);

const handleBackPress = () => {
if (canGoBack) {
channelRef.current?.goBack();
} else {
navigation.goBack();
}
};

return (
<View style={{ flex: 1 }}>
<View style={styles.header}>
<TouchableOpacity onPress={handleBackPress}>
<Text>{canGoBack ? '← Back' : '← Close'}</Text>
</TouchableOpacity>
</View>
<BBChannel
ref={channelRef}
channelUrl="https://demo.bbvms.com/ch/channel_name.json"
safeAreaEdges={[]}
onCanGoBackChange={setCanGoBack}
/>
</View>
);
}

Internal Implementation

The SDK maintains a custom history stack for channel navigation rather than relying on WebView.goBack(). This is because the standard WebView history API has known issues with single-page applications (react-native-webview#3229, #2608) where history.back() may navigate to an unexpected URL or fail silently.

The custom stack tracks navigation state changes and replays them via the channel's JavaScript API, ensuring reliable back navigation regardless of WebView quirks.