Passing navigation to BottomNavigation - react-native

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 />
}

Related

Implement StackRouter in react navigation v5 for nested routes

I've got a react native app with the below screen hierarchy
RootNavigator
|
|---AppNavigatorStack
| |--Screen1
| |--Screen2
|
|---LoginNavigatorStack
|--loginScreen
|--forgotPasswordScreen
I've been trying to create a StackRouter for this workflow
const options = {
initialRouteName: 'LoginNavigatorStack',
}
const routerConfigOptions = {
routeGetIdList: {},
routeNames: ["AppNavigatorStack", "LoginNavigatorStack"],
type: 'stack',
routeParamList: {}
}
let router = StackRouter(options)
How can I configure routerConfigOptions to use function like getStateForAction, getInitialState in StackRouter
Sample code to make a new state
const action = CommonActions.reset({
index: 0,
routes: [{name: "Screen2"}],
})
let newState = router.getStateForAction(state, action, routerConfigOptions)
state - current state object of the application which we could get from onStateChange of NavigationContainer
The result of newState is null because I haven't added Screen1, Screen2, loginScreen, forgotPasswordScreen in routerConfigOptions
I'm actually expecting a new state which should have the details to navigate to loginScreen.
How can I add all nested screens and create StackRouter for this?
Your syntax looks more like of react-navigation < 4. I am going to post here, how I do it using react-navigation v5.
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
const LoginStack = createStackNavigator();
const AppStack = createStackNavigator();
const RootStack = createStackNavigator();
const LoginStackNavigator = () => {
return (
<LoginStack.Navigator>
<LoginStack.Screen name="LoginScreen" component={LoginScreen}/>
<LoginStack.Screen name="ForgotPasswordScreen" component={ForgotPasswordScreen}/>
</LoginStack.Navigator>
)
}
const AppStackNavigator = () => {
return (
<AppStack.Navigator>
<AppStack.Screen name="Screen1" component={Screen1}/>
<AppStack.Screen name="Screen2" component={Screen2}/>
</AppStack.Navigator>
)
}
const RootStackNavigator = () => {
return (
<RootStack.Navigator>
<RootStack.Screen name="LoginStackNavigator" component={LoginStackNavigator}/>
<RootStack.Screen name="AppStackNavigator" component={AppStackNavigator} />
</RootStack.Navigator>
)
}
const App: () => React$Node = () => {
return (
<NavigationContainer>
<RootStackNavigator/>
</NavigationContainer>
);
};

Pointing to StackNavigator from TabNavigator throws Maximum update depth exceeded error

In my React Native app, I'm using react-navigation version 5 along with react-native-paper.
I'm trying to have one of the items in my bottom tab to point to a stack navigator. When I do that, I get the following error:
Maximum update depth exceeded. This can happen when a component calls
setState inside useEffect, but useEffect either doesn't have a
dependency array, or one of the dependencies changes on every render.
If I point the tab item to a regular screen, everything works fine. As per the react-navigation documentation, I use the BottomNavigation component from react-paper as opposed to using their tab solution -- https://reactnavigation.org/docs/material-bottom-tab-navigator/#using-with-react-native-paper-optional
Here's my tabs component:
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';
// Stylesheet
import { styles } from './style-home-tabs';
const HomeTabs = (props) => {
const [index, setIndex] = React.useState(0);
// Routes
const homeRoute = () => <Home />;
const vendorsRoute = () => <Vendors />;
const scanRoute = () => <CodeScanner />;
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;
And here's the stack navigator that I'm pointing to which causes the error:
import React from 'react';
import { createStackNavigator } from '#react-navigation/stack';
// Components
import VendorsScreen from '../../../screens/vendors/VendorsScreen';
// Navigator
const VendorStack = new createStackNavigator();
const VendorsStack = (props) => {
return(
<VendorStack.Navigator screenOptions={{ headerShown: false }}>
<VendorStack.Screen name="Vendors" component={VendorsScreen} />
</VendorStack.Navigator>
);
}
export default VendorsStack;
As I said, if I were to point to a regular screen, everything works fine. Any idea what's going on here and how to fix it?
Moving the route declarations outside the component fixed the issue. Every time the component got mounted, it was calling these functions and that was causing the issue.
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';
// Stylesheet
import { styles } from './style-home-tabs';
// 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;

React navigation undefined params

I'm trying to pass params into a new screen, and implemented it like mentioned here.
I have the following TouchableOpacity button.
<TouchableOpacity
onPress={() => {
this.props.navigation.navigate('SomeScreen', {
title: 'Title',
subTitle: 'Subtitle',
});
}}
>
On the other page (let's call it Somescreen), I have the following:
render() {
const { navigation } = this.props;
const title = navigation.getParam('title');
}
But title above is undefined:
{ params: undefined, routeName: "Somescreen", key: "id-xx" }
My rootStack:
const RootStack = createStackNavigator({
SomescreenA: { screen: SomescreenA },
SomescreenB: { screen: SomescreenB },
}, { headerMode: 'none' });
Why are my params undefined in a new screen?
If you face a situation where your target screen get undefined params, probably you have a nested navigation stack.
Here you have to pass params to the navigate method in this way:
navigation.navigate('Root', {
screen: 'Settings',
params: { user: 'jane' },
});
For more information read this page in the official docs:
https://reactnavigation.org/docs/nesting-navigators/#navigating-to-a-screen-in-a-nested-navigator
In my specific case, I was calling a nested navigator, so I had to manage how send those params to their specific screen, so I did this:
Send params this way...the regular way:
navigation.navigate(
'OrderNavigator',
{itemSelected},
);
Then, from navigator stack I did this:
const OrderNavigator = ({route: {params}}) => {
return (
<Stack.Navigator initialRouteName="Order">
<Stack.Screen name="Order" component={Order} options={{headerShown: false}} initialParams={params} />
</Stack.Navigator>
);
};
And that's it. Then from the screen I got them like this:
const Order = ({route}) => {
const {itemSelected} = route.params;
const {first_name, last_name} = itemSelected;
return (...)
}
I've, unfortunately, encountered cases where navigate(route, params, ...) wouldn't pass the params object, just like you did.
As a workaround, I use the other variant - navigate({routeName, params, action, key}) that you can find here. It always works.
The accepted answer workaround did not work for me, so apparently if you use children to render your component (in screen options) and pass route as a prop, it works
if you are on react navigation v6^ use the useRoute hook to access the params object
const route = useRoute();
useRoute is a hook that gives access to the route object. It's useful when you cannot pass the route prop into the component directly, or don't want to pass it in case of a deeply nested child.
below is an implementation of this
import { useNavigation, useRoute } from '#react-navigation/native';
import { Pressable, Text } from 'react-native';
function Screen1() {
const navigation = useNavigation();
return (
<Pressable
onPress={() => {
navigation.navigate('Screen2', { caption: 'hey' });
}}
>
<Text> Go to Screen 2 </Text>
</Pressable>
);
}
function Screen2() {
const route = useRoute();
return <Text>{route.params.caption}</Text>;
}

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.

Should I call multiple addNavigationHelpers in nested react navigation?

I'm following https://github.com/parkerdan/SampleNavigation to integrate redux with react-navigation.
I have a question: should I call addNavigationHelpers multiple times for nested navigators?
In the sample:
const routeConfiguration = {
TabOneNavigation: { screen: TabOneNavigation },
TabTwoNavigation: { screen: TabTwoNavigation },
TabThreeNavigation: { screen: TabThreeNavigation },
}
const tabBarConfiguration = {
tabBarOptions:{
activeTintColor: 'white',
inactiveTintColor: 'blue',
activeBackgroundColor: 'blue',
inactiveBackgroundColor: 'white',
}
}
export const TabBar = TabNavigator(routeConfiguration,tabBarConfiguration);
<TabBar
navigation={
addNavigationHelpers({
dispatch: dispatch,
state: navigationState,
})
}
/>
<NavigatorTabOne
navigation={
addNavigationHelpers({
dispatch: dispatch,
state: navigationState
})
}
/>
<NavigatorTabTwo
navigation={
addNavigationHelpers({
dispatch: dispatch,
state: navigationState
})
}
/>
<NavigatorTabThree
navigation={addNavigationHelpers({
dispatch: dispatch,
state: navigationState
})}
/>
addNavigationHelpers is called 4 times, one for the TabNavigator and 3 others for the tabs.
Is this the recommended way by the document?
Navigation state is automatically passed down from one navigator to another when you nest them.
What actually are TabBar, NavigatorTabOne, NavigatorTabTwo... ?
I suppose you have some code like this:
export const TabBar = TabNavigator({
NavigatorTabOne: { screen: NavigatorTabOne},
NavigatorTabTwo: { screen: NavigatorTabTwo},
NavigatorTabThree: { screen: NavigatorTabThree}
}, {
initialRouteName: 'NavigatorTabOne',
})
To create your root container you need this:
import { createReduxBoundAddListener, createReactNavigationReduxMiddleware } from 'react-navigation-redux-helpers';
export const navigationMiddleware = createReactNavigationReduxMiddleware(
"root",
state => state.nav,
);
const addListener = createReduxBoundAddListener("root");
class ReduxNavigation extends React.Component {
render() {
const { dispatch, nav } = this.props
const navigation = ReactNavigation.addNavigationHelpers({
dispatch,
state: nav,
addListener,
});
return <TabBar navigation={navigation} />
}
}
This could be your reducer
const firstAction = TabBar.router.getActionForPathAndParams('NavigatorTabOne')
const initialState = TabBar.router.getStateForAction(firstAction)
export const reducer = (state = initialState, action) => {
const nextState = TabBar.router.getStateForAction(action, state);
return nextState || state;
};
Anyway, don't use that repository
The official react navigation doc does exist
If you want to use something for scaffolding your react native + redux + react navigation, you can have a look a this project, that will create app base in a command