Change Bottom Tab Bar based on state in React Navigation with navigationOptions - react-native

I want to change the bottom tabs on the screen based on what features are enabled. This feature list is populated via a login API call.
Currently I have the following:
const TabRouteConfig = {
Home: DashboardScreen,
FeatureA: FeatureA,
FeatureZ: FeatureZ,
};
const TabNavigatorConfig = {
initialRouteName: 'Home',
order: [
'Home',
'Feature A',
],
tabBarPosition: 'bottom',
lazy: true,
};
const TabNavigator1 = createBottomTabNavigator(
TabRouteConfig,
TabNavigatorConfig,
);
// Set the tab header title from selected route
// https://reactnavigation.org/docs/en/navigation-options-resolution.html#a-stack-contains-a-tab-navigator-and-you-want-to-set-the-title-on-the-stack-header
TabNavigator1.navigationOptions = ({ navigation }) => {
const { index, routes } = navigation.state;
const { routeName } = routes[index];
return {
headerTitle: routeName,
};
};
const TabNavigator2 = createBottomTabNavigator(
TabRouteConfig,
{
...TabNavigatorConfig,
order: [
'Home',
'Feature Z',
]
);
TabNavigator2.navigationOptions = TabNavigator1.navigationOptions;
const Stack = createStackNavigator({
Main: {
screen: props => (props.screenProps.hasFeature ?
<TabNavigator1 /> : <TabNavigator2 />
)
},
})
const WrappedStack = props => (
<View style={styles.container}>
<Stack
screenProps={{ hasFeature: props.hasFeature }}
/>
</View>
);
const mapStateToProps = (state, props) => {
return {
...props,
hasFeature: state.hasFeature,
};
};
export default connect(mapStateToProps, null)(WrappedStack);
This mostly works - it dynamically switches between TabNavigator1 and TabNavigator2 based on hasFeature, but it no longer honors the navigationOptions placed on the TabNavigators and the headerTitle is not set.
Is there a better way to do this?

It's an antipattern to render more than one navigator simultaneously as the navigation states of those navigators will be completely separated, and you will not be able to navigate to one from another.
You can use tabBarComponent option to achieve what you want. Hope you can get the idea from below example:
import { createBottomTabNavigator, BottomTabBar } from 'react-navigation-tabs';
const TabNavigator = createBottomTabNavigator(
TabRouteConfig,
{
tabBarComponent: ({ navigation, ...rest }) => {
const filteredTabNavigatorRoutes = navigation.state.routes.filter(route => isRouteAllowed(route));
return (
<BottomTabBar
{...rest}
navigation={{
...navigation,
state: { ...navigation.state, routes: filteredTabNavigatorRoutes },
}}
/>
);
},
},
);
NOTES:
You don't have to install react-navigation-tabs separately. It is automatically installed with react-navigation 2.0.0+.
isRouteAllowed is the function which returns true or false based on whether to show that route or not. Make sure to only check the top level routes in that object.
TabRouteConfig should contain all possible tabs, and this logic only hides the route from the TabBar visually. So, you can still programmatically navigate to all routes. Therefore, you might need additional logic in each of those screens to decide whether to render them based on hasFeature.

Related

Passing navigation to BottomNavigation

In my React Native app, I use react-navigation version 5.
How do I pass the navigation object to the scenes in BottomNavigation?
Here's the component where I create the BottomNavigation:
import React from 'react';
import { BottomNavigation } from 'react-native-paper';
// Components
import CodeScanner from '../../../screens/vendors/CodeScannerScreen';
import Home from '../../../screens/home/HomeScreen';
import Vendors from '../vendors/VendorsStack';
// Routes
const homeRoute = () => <Home />;
const vendorsRoute = () => <Vendors />;
const scanRoute = () => <CodeScanner />;
const HomeTabs = (props) => {
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{ key: 'home', title: 'Home', icon: 'newspaper-variant-multiple' },
{ key: 'vendors', title: 'Vendors', icon: 'storefront' },
{ key: 'codescanner', title: 'Scan', icon: 'qrcode-scan' }
]);
const renderScene = BottomNavigation.SceneMap({
home: homeRoute,
vendors: vendorsRoute,
codescanner: scanRoute
});
return (
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
labeled={false} />
);
}
export default HomeTabs;
In this code, I do have the navigation in the props but haven't been able to figure out a way to pass navigation to the screens. Please also note that the vendor one is actually a stack navigator. In particular, that's where I need access to navigation so that I can open up other screens.
You should pass it from tabBar prop like below (I use TypeScript) and BottomTabBarProps:
export declare type BottomTabBarProps = BottomTabBarOptions & {
state: TabNavigationState
descriptors: BottomTabDescriptorMap
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>
}
So you pass BottomTabBarProps to your custom tab component
<BottomTab.Navigator
screenOptions={TabBarVisibleOnRootScreenOptions}
initialRouteName={'Explore'}
tabBar={(props: BottomTabBarProps) => <HomeTabs {...props} />}
>
<BottomTab.Screen name="Explore" component={ExploreScreen} />
...
</BottomTab.Navigator>
So inner HomeTabs you got props.navigation
I think you don't need to pass navigation to BottomNavigation. Because in react-navigation v5 navigation can access anywhere with hook
Read this document https://reactnavigation.org/docs/connecting-navigation-prop/
You may pass data to the vendors screen through the route like so:
const HomeTabs = ( props ) => {
...
const [routes] = React.useState([
...
{ key: 'vendors', title: 'Vendors', icon: 'storefront', props: props },
...
]);
...
}
On your vendor screen you can then retrieve and use the data like this:
const VendorsRoute = ({ route }) => {
const props = route.props;
<Vendors />
}

How do I create an embedded Stack navigator inside a React Native formSheet modal?

Like so:
I'm running react-navigation v4.
First you have to follow the tutorial on how to set up a react-navigation modal, the one that has a jump animation and doesn't look like the native formSheet. You have to set up a stack navigator with your root navigator as one child and the modal as another:
And it scales, because you can have more than one of these modals as children.
The code for this is the following:
const RootNavigator = createStackNavigator(
{
index: { screen: AppNavigator },
[NC.SCREEN_ROOM_INFO]: { screen: RoomInfoNavigator },
[NC.SCREEN_CHAT_CREATE]: { screen: RoomCreateNavigator },
[NC.SCREEN_CHAT_SEARCH]: { screen: ChatSearchNavigator },
[NC.SCREEN_CHAT_GIF_PICKER]: { screen: GifPickerNavigator }
},
{
mode: 'modal',
headerMode: 'none',
transitionConfig: () => ({
transitionSpec: {
duration: 0
}
}),
transparentCard: true
}
)
Then you need to implement these, from my example, 4 navigators that will be displayed as modals each like so:
// Here you'll specify the screens you'll navigate in this modal, starting from index.
const RoomInfoStack = createStackNavigator({
index: { screen: NavigableRoomInfo },
[NC.SCREEN_ROOM_ROSTER]: { screen: NavigableRoomRoster },
[NC.SCREEN_ROOM_NOTIFICATION_PREFERENCES]: { screen: NavigableRoomNotificationPreferences },
[NC.SCREEN_ROOM_EDIT]: { screen: NavigableRoomEdit }
})
type NavigationComponent<T = any> = {
navigation?: NavigationScreenProp<NavigationState, T>
}
type Props = NavigationComponent
// THIS code is from the react-navigation tutorial on how to make a react-navigation modal:
// https://reactnavigation.org/docs/4.x/custom-navigators/#extending-navigators
class RoomInfoNavigator extends React.Component<Props> {
static router = {
...RoomInfoStack.router,
getStateForAction: (action, lastState) => {
// check for custom actions and return a different navigation state.
return RoomInfoStack.router.getStateForAction(action, lastState)
}
}
constructor(props) {
super(props)
this.onClose = this.onClose.bind(this)
}
onClose() {
this.props.navigation?.goBack()
}
// And here is the trick, you'll render an always open RN formSheet
// and link its dismiss callbacks to the goBack action in react-navigation
// and render your stack as its children, redirecting the navigator var to it in props.
render() {
return (
<Modal
visible={true}
animationType={'slide'}
supportedOrientations={['portrait', 'landscape']}
presentationStyle={'formSheet'}
onRequestClose={() => this.onClose()}
onDismiss={() => this.onClose()}
>
<RoomInfoStack {...this.props} />
</Modal>
)
}
}
export { RoomInfoNavigator }
This export is what our root stack imported before. Then you just need to render the screens, I have a pattern that I do to extract the navigation params to props in case this screen is ever displayed without navigation:
const NavigableRoomInfo = (props: NavigationComponent<RoomInfoProps>) => {
const roomInfo = props.navigation!.state!.params!.roomInfo
const roomInfoFromStore = useRoomFromStore(roomInfo.id)
// Here I update the navigation params so the navigation bar also updates.
useEffect(() => {
props.navigation?.setParams({
roomInfo: roomInfoFromStore
})
}, [roomInfoFromStore])
// You can even specify a different Status bar color in case it's seen through modal view:
return (
<>
<StatusBar barStyle="default" />
<RoomInfo roomInfo={roomInfoFromStore} navigation={props.navigation} />
</>
)
}
Sources:
https://reactnavigation.org/docs/4.x/modal
https://reactnavigation.org/docs/4.x/custom-navigators/#extending-navigators

React Navigation v3 Modal does not work with createBottomTabNavigator

React Navigation v3 Modal does not work with createBottomTabNavigator and not sure why. However, headerMode: 'none' seems to be working but mode: 'modal' is not showing up as modal.
const Addpicture = createStackNavigator(
{
Addpicture: {
screen: Addpicture
}
},
{
mode: 'modal', // This does NOT work
headerMode: 'none' // But this works
}
);
const Navigator = createBottomTabNavigator(
{
'My Interviews': {
screen: MyDatesStack
},
'Add Interview': {
screen: AddDateStack
},
'Companies': {
screen: CompaniesStack
}
}
);
export default createAppContainer(Navigator);
Indeed it doesn't work no matter what I tried.
I solved this problem by following steps below. Let's say you want to open modal when NewModal tab is pressed.
Set up your app container by including tabs stack & to be opened modal navigation stack:
const FinalTabsStack = createStackNavigator(
{
Home: TabNavigator,
NewModal: NewModalNavigator
}, {
mode: 'modal',
}
)
Create app container with that tabs stack per this guide
Inside the TabNavigator in the createBottomTabNavigator return null component for specific tab (NewModal) (to turn off navigation by react-navigator)
const TabNavigator = createBottomTabNavigator({
Feed: FeedNavigator,
Search: SearchNavigator,
NewModal: () => null,
Chat: ChatNavigator,
MyAccount: MyAccountNavigator,
}
defaultNavigationOptions: ({ navigation }) => ({
mode: 'modal',
header: null,
tabBarIcon: ({ focused }) => {
const { routeName } = navigation.state;
if (routeName === 'NewModal') {
return <NewModalTab isFocused={focused} />;
}
},
}),
Handle click manually inside a custom tab component NewModalTab with TouchableWithoutFeedback & onPress. Inside NewModalTab component:
<TouchableWithoutFeedback onPress={this.onPress}>
<Your custom tab component here />
</TouchableWithoutFeedback>
Once you catch onPress dispatch redux event
onPress = () => {
this.props.dispatch({ type: 'NAVIGATION_NAVIGATE', payload: {
key: 'NewModalNavigator',
routeName: 'NewSelfieNavigator',
}})
}
Handle dispatched event using Navigation Service. I'm using redux-saga for it
NavigationService.navigate(action.payload);
A bit complicated but works.

cant send params via navigation navigate

i want to send parameter name from login screen to home screen via router but data object not defined
TypeError: undefined is not object (evaluating
'navigation.state.params.name'
login screen
<TouchableOpacity
style={styles.buttonContainer}
onPress={() => this.props.navigation.navigate('Home', { name: 'Erry' })}>
<Text style={styles.buttonText}>MASUK</Text>
</TouchableOpacity>
my router
export const LoginStack = SwitchNavigator({
Login: {
screen: Login,
},
Home: {
screen: HomeStack
}
}, {
headerMode: 'none',
navigationOptions: {
headerVisible: false,
}
});
export const HomeStack = StackNavigator({
Home: {
screen: Home
}
});
Home Screen
static navigationOptions = ({navigation}) => ({
title: `${navigation.state.params.name}`,
headerStyle : {
backgroundColor: '#f39c12',
},
headerTitleStyle :{
color: '#353b48',
},
});
any idea ?
The purpose of SwitchNavigator is to only ever show one screen at a time. By default, it does not handle back actions and it resets routes to their default state when you switch away.
You'd better not use SwitchNavigator, you can see the correct example.
And you'd better judge the params is empty or not:
const params = navigation.state.params || {};
const title = params.name || 'default title';

How to handle click event on tab item of TabNavigator in React Native App using react-navigation?

I am programming React Native App. I am using react-navigation for navigating screens.
I have 2 Stack Navigators, they are stayed in a Tab Navigator. I want handle click event when I press tab item on TabBar to refresh its view.
export const HomeStack = StackNavigator({
Home: {screen: ScreenHome},
Detail: {screen: ScreenDetail}
})
export const UserStack = StackNavigator({
User: {screen: ScreenUser}
})
export const Tabbar = TabNavigator({
HomeTab: {
screen: HomeStack,
},
UserTab: {
screen: UserStack,
}
}, {
tabBarComponent: ({ jumpToIndex, ...props}) => (
<TabBarBottom
{...props}
jumpToIndex={(index) => {
if(props.navigation.state.index === index) {
props.navigation.clickButton(); //----> pass props params (code processes)
}
else {
jumpToIndex(index);
}
}
}
/>
),
tabBarPosition: 'bottom'
});
class TabClass extends Component{
constructor(props){
super(props)
this.clickButton = this.clickButton.bind(this)
}
clickButton(){
console.log('click button')
}
render (){
return (
<Tabbar clickButton={()=> this.clickButton()}/>
)
}
}
I want to pass clickButton() function from TabClass Component to the code which processes event click tab bar. When if(props.navigation.state.index === index), I want it will call clickButton(). I try it but it doesn't work.
Is there any way to solve my matter?
I tried onNavigationStateChange(prevState, currentState).
class TabClass extends Component{
constructor(props){
super(props)
this.clickButton = this.clickButton.bind(this)
}
clickButton(){
console.log('click button')
}
_handleNavagationStateChange(prevState, currentState){
console.log('handle navigation state change')
}
render (){
return (
<Tabbar onNavigationStateChange={(prevState, currentState) => {
this._handleNavagationStateChange(prevState, currentState)
}}
clickButton={()=> this.clickButton()}
/>
)
}
}
However, _handleNavagationStateChange(prevState, currentState) only run when navigation state changes (for examples, if I stay at HomeTab, I click User Tab Item, this function will run; if I stay at HomeTab, I click Home Tab Item, this function will not run).
Is there any way to handle click event on tab item.
according to the newest documentation here, you can use navigationOptions: {navigationOptions: () => doWhatever()} to handle tab bar taps.
From the react-navigation 4.x docs, you can override tabBarOnPress within navigationOptions:
tabBarOnPress: ({ navigation, defaultHandler }) => {
defaultHandler(); // Call the default handler to actually switch tabs
// do extra stuff here
},
Please try the code following when customize the event touch of TabBar:
import { TabBarBottom, TabNavigator, StackNavigator } from 'react-navigation';
export const MainScreenNavigator = TabNavigator({
Home: { screen: HomeViewContainer },
Search: { screen: SearchViewContainer },
}, {
tabBarComponent: ({ jumpToIndex, ...props, navigation }) => (
<TabBarBottom
{...props}
jumpToIndex = { tabIndex => {
const currentIndex = navigation.state.index;
if (tabIndex == 1) {
// do some thing. Call Native Live Stream Record
} else {
jumpToIndex(tabIndex);
}
console.log('Select tab: ', tabIndex);
}}
/>),
tabBarPosition: 'bottom',
});