How to Programmatically Navigate Between Tabs in React Native Tab View
TLDR: Update the index property within the navigationState prop passed to the TabView component to programmatically switch tabs, or invoke the onIndexChange callback with the target index to trigger a navigation change.
The react-native-tab-view library implements a controlled component pattern for React Native tab interfaces, meaning the active tab is determined by external state rather than internal component state. This architecture, defined in src/TabView.tsx and src/types.ts, requires you to manage the navigationState object—containing the index and routes arrays—giving you explicit control over programmatic navigation.
Understanding the Controlled Component Architecture
In src/TabView.tsx, the component accepts a navigationState prop that follows the NavigationState type defined in src/types.ts. This object structure determines which tab is currently visible:
navigationState={{
index: 0, // Zero-based position of active tab
routes: [ // Array of Route objects
{ key: 'first', title: 'First Tab' },
{ key: 'second', title: 'Second Tab' }
]
}}
The TabView renders content through the renderScene callback and reports user interactions via onIndexChange. When a user swipes between tabs, the gesture handler in src/TabViewPagerPan.tsx calculates the new index and invokes onIndexChange, allowing you to update your state. Conversely, updating the index in your state and passing it back to navigationState forces the component to animate to that tab programmatically.
Core Methods for Programmatic Navigation
Updating State Directly via onIndexChange
The simplest approach maintains the tab index in local state and updates it programmatically:
import React, { useState, useCallback } from 'react';
import { TabView, SceneMap } from 'react-native-tab-view';
const FirstRoute = () => <View style={{ flex: 1, backgroundColor: '#ff4081' }} />;
const SecondRoute = () => <View style={{ flex: 1, backgroundColor: '#673ab7' }} />;
export default function TabNavigator() {
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: 'first', title: 'First' },
{ key: 'second', title: 'Second' },
]);
// Programmatic navigation function
const jumpToTab = useCallback((targetIndex) => {
if (targetIndex >= 0 && targetIndex < routes.length) {
setIndex(targetIndex);
}
}, [routes.length]);
return (
<>
<TabView
navigationState={{ index, routes }}
renderScene={SceneMap({
first: FirstRoute,
second: SecondRoute,
})}
onIndexChange={setIndex}
/>
<Button title="Go to Second Tab" onPress={() => jumpToTab(1)} />
</>
);
}
Navigating by Route Key Instead of Index
Hardcoding numeric indices creates brittle code if tab order changes. Instead, navigate by route key by mapping the key to its current index:
const jumpTo = useCallback((key) => {
const routeIndex = routes.findIndex(route => route.key === key);
if (routeIndex !== -1) {
setIndex(routeIndex);
}
}, [routes]);
// Usage
jumpTo('settings'); // Navigates to the tab with key 'settings'
This approach is more maintainable when tabs are dynamically added, removed, or reordered.
Using Refs with useImperativeHandle
For scenarios where you need to trigger navigation from parent components or deep callbacks where state setters aren't accessible, expose navigation methods via a ref using useImperativeHandle:
import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react';
import { TabView, SceneMap } from 'react-native-tab-view';
const TabViewWrapper = forwardRef((props, ref) => {
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: 'home', title: 'Home' },
{ key: 'profile', title: 'Profile' },
]);
useImperativeHandle(ref, () => ({
navigateToKey: (key) => {
const targetIndex = routes.findIndex(r => r.key === key);
if (targetIndex !== -1) setIndex(targetIndex);
},
navigateToIndex: (idx) => {
if (idx >= 0 && idx < routes.length) setIndex(idx);
}
}));
return (
<TabView
navigationState={{ index, routes }}
renderScene={SceneMap({
home: () => <Text>Home Content</Text>,
profile: () => <Text>Profile Content</Text>,
})}
onIndexChange={setIndex}
/>
);
});
// Parent component usage
export default function Parent() {
const tabRef = useRef(null);
return (
<>
<TabViewWrapper ref={tabRef} />
<Button
title="Jump to Profile"
onPress={() => tabRef.current?.navigateToKey('profile')}
/>
</>
);
}
React Context for Deep Component Trees
When navigation triggers originate from deeply nested components, avoid prop drilling by providing navigation functions through React Context:
// TabNavigationContext.js
import { createContext, useContext, useState, useCallback } from 'react';
const TabContext = createContext(null);
export const useTabNavigation = () => useContext(TabContext);
export const TabProvider = ({ children, initialRoutes }) => {
const [index, setIndex] = useState(0);
const [routes] = useState(initialRoutes);
const jumpTo = useCallback((key) => {
const idx = routes.findIndex(r => r.key === key);
if (idx !== -1) setIndex(idx);
}, [routes]);
return (
<TabContext.Provider value={{ index, routes, setIndex, jumpTo }}>
{children}
</TabContext.Provider>
);
};
Any child component can now import useTabNavigation and call jumpTo('targetKey') without receiving props through intermediate components.
Key Implementation Files
According to the react-native-tab-view source code, these files define the core navigation behavior:
src/TabView.tsx: Implements the controlled component logic, acceptingnavigationStateandonIndexChangeprops to coordinate tab displays.src/TabViewPagerPan.tsx: Contains the gesture handling logic that translates swipe gestures intoonIndexChangecalls with calculated index values.src/types.ts: Defines the TypeScript interfaces forNavigationState,Route, and callback signatures includingonIndexChangeandrenderScene.
Summary
- Controlled Pattern: The
TabViewcomponent insrc/TabView.tsxis fully controlled via thenavigationState.indexprop—updating this value programmatically switches tabs. - State Management: Use
useStateto track the current index and pass the setter toonIndexChangefor both user gestures and programmatic navigation. - Key-Based Navigation: Map route keys to indices using
routes.findIndex()to create stable navigation logic that survives tab reordering. - Ref Exposure: Use
useImperativeHandleto exposejumpToorsetIndexmethods on component refs when direct state access is impractical. - Context Integration: Implement a Context Provider to distribute navigation functions throughout deep component trees without prop drilling.
Frequently Asked Questions
Can I call a jumpTo method directly on the TabView ref?
The react-native-tab-view library does not natively expose a jumpTo method on component refs. However, you can implement this functionality by wrapping TabView in a custom component that uses useImperativeHandle to expose navigation methods, as shown in the ref-based examples above.
How do I disable swipe gestures while keeping programmatic navigation?
Set the swipeEnabled prop to false on the TabView component. This disables the gesture handler in src/TabViewPagerPan.tsx while preserving the ability to navigate programmatically by updating the index in navigationState.
What is the difference between index-based and key-based navigation?
Index-based navigation uses numeric positions (0, 1, 2) to identify tabs, which is simple but fragile if tab order changes. Key-based navigation uses string identifiers defined in the key property of each route object, making your navigation logic resilient to changes in tab ordering or dynamic tab addition/removal.
How does programmatic navigation integrate with React Navigation?
When embedding a TabView inside a React Navigation screen, maintain the tab index in the screen's component state or in a global state manager. You can synchronize the tab index with React Navigation's navigation state using useFocusEffect or by dispatching navigation actions that update the parent route parameters, though the core mechanism remains updating the navigationState.index prop.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →