React Navigation v3 Modal does not work with createBottomTabNavigator - react-native

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.

Related

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

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

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.

Custom header in nested TabNavigator

I have a fairly complication navigation flow requirement for an app I'm working on.
I have a bottom tab bar, for each tab I'll be having a top tab bar for additional related views.
Which I have working, however on the videos tab in the nested "All" tab, I need to add a search bar to the header, and on the "Favourites" tab I'll be having yet another custom header with an "Edit" button at the top right.
How can I achieve this navigation whilst allowing React Navigation to co-ordinate everything. See images below:
What I don't want to do is disable the header at the MainNavigator level and enable it for particular routes. Or even worse embed the header and the tab bar on individual containers.
routes.js
import {
StackNavigator,
TabNavigator,
DrawerNavigator
} from "react-navigation";
import Feed from "./containers/Feed";
import Auth from "./containers/Auth";
import News from "./containers/News";
import Videos from "./containers/Videos";
import FavouriteVideos from "./containers/FavouriteVideos";
const DashboardNavigator = TabNavigator(
{
Feed: {
screen: Feed
},
News: {
screen: News
}
},
{
tabBarPosition: "top"
}
);
const VideoNavigator = TabNavigator(
{
Videos: {
screen: Videos,
navigationOptions: {
title: "All"
}
},
Favourites: {
screen: FavouriteVideos
}
},
{
tabBarPosition: "top"
}
);
const MainNavigator = TabNavigator(
{
Dashboard: {
screen: DashboardNavigator,
navigationOptions: ({}) => ({
title: "Dashboard"
})
},
Video: {
screen: VideoNavigator,
navigationOptions: ({}) => ({
title: "Videos"
})
}
},
{
swipeEnabled: false,
animationEnabled: false,
tabBarPosition: "bottom"
}
);
const AuthenticatedNavigator = DrawerNavigator({
App: {
screen: MainNavigator
}
});
const RootNavigator = StackNavigator({
LoggedOut: {
screen: Auth
},
Authenticated: {
screen: AuthenticatedNavigator
}
});
export default RootNavigator;
Snack
https://snack.expo.io/H1qeJrLiM
Images
You can use react-navigation addListener function with combination setParams to achieve desired behavior.
You can listen for focus and blur events and then change a parameter. Then in your route config you can look for this parameter and decide what to render for header. I changed your snack to show a working example of what I am suggesting.
Example
const MainNavigator = TabNavigator(
{
Dashboard: {
screen: DashboardNavigator,
navigationOptions: ({}) => ({
title: "Dashboard"
})
},
Video: {
screen: VideoNavigator,
navigationOptions: ({navigation}) => {
let title = 'Videos';
navigation.state.routes.forEach((route) => {
if(route.routeName === 'Videos' && route.params) {
title = route.params.title;
}
});
// I set title here but you can set a custom Header component
return {
tabBarLabel: 'Video',
title
}
}
}
},
{
swipeEnabled: false,
animationEnabled: false,
tabBarPosition: "bottom"
}
);
export default class Videos extends Component {
constructor(props) {
super(props);
this.willFocusSubscription = props.navigation.addListener(
'willFocus',
payload => {
this.props.navigation.setParams({title: 'All Videos'});
}
);
this.willBlurSubscription = props.navigation.addListener(
'willBlur',
payload => {
this.props.navigation.setParams({title: 'Just Videos'});
}
);
}
componentWillUnmount() {
this.willFocusSubscription.remove();
this.willBlurSubscription.remove();
}
render() {
return (
<View>
<Text> Videos </Text>
</View>
);
}
}

navigating user on notification click app in background/foreground

I'm using REACT-NATIVE-FCM.
My app has drawer navigator inside stack navigator in App.js.
On notification click from notification tray I want user to directly go to certain Screen.
Registering FCM events in Home page only work if app is closed, while in app in foreground the events are not called.It just takes you to the last open Screen.
I have to register FCM events on every Screen, which doesn’t look ok to me. So I have the solution to register FCM events in App.js since App.js is called from index.js while initializing the App but I’m unable to use this.props.navigation.navigate in App.js.
Using react navigation how can we redirect user just after stack is declared.
APP.JS
const Drawer = DrawerNavigator(
{
Dashboard: { screen: Dashboard },
Screen1: { screen: Screen1 },
Screen2: { screen: Screen2},
Screen3: { screen: Screen3},
},
{
navigationOptions: {
gesturesEnabled: false,
},
initialRouteName: "Dashboard",
contentOptions: {
activeTintColor: "#e91e63"
},
drawerPosition: 'right',
contentComponent: props => <SideBar {...props} />
}
);
const AppNavigator = StackNavigator(
{
Home: { screen: Home },
Drawer: { screen: Drawer },
ScreenABC: { screen: ScreenABC },
ScreenXYZ: { screen: ScreenXYZ }
},
{
headerMode: "none",
}
);
export default () =>
<Root>
<AppNavigator />
</Root>;
//CALLED WHEN APP IN FOREGROUND
this.notificationListener = FCM.on(FCMEvent.Notification, async (notif) => {
//alert('FCM.on: ' + JSON.stringify(notif));
if(notif.routeValue == 1)
{
this.props.navigation.navigate("Screen1")}
});
//CALLED WHEN APP IN BACKGROUND
FCM.getInitialNotification().then(notif => {
});
If you want to really go to a certain screen no matter what. You should at least reset your navigator state before you call navigate.
Or you can also do is creating a custom action, dispatch this action and return a correct navigator state.
Here is how I reset my navigator state to home:
export const homeNav = (state, action) => {
let index;
let routes;
switch (action.type) {
case types.RESET_HOME_NAV:
routes = [...state.routes];
for (let index = state.index; index > 0; --index) {
routes.pop();
}
return {
...state,
routes,
index: 0,
};
default:
return NavigatorHome.router.getStateForAction(action, state);
}};

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',
});