MaterialTopTabNavigator dynamic route configs - react-native

I want to create via createBottomTabNavigator. It has 5 tabs. Each tab is a StackNavigator.
One of these tabs has a top tab bar. I create the top tab bar via createMaterialTopTabNavigator
But I know tab count after http request. How can I add tab dynamically? The doc says that
There are workarounds if you absolutely need dynamic routes but you can expect some additional complexity
I am confused about this task.
How can I do that?
Related react-navigation issue: https://react-navigation.canny.io/feature-requests/p/dynamic-routes-for-navigators

I think you can create a component that returns a tabNavigator. You can then access props or do whatever you want to dynamically add or remove tabs. Here I am using the latest version of react-navigation.
import React, { Component } from 'react-native';
import { createAppContainer, createMaterialTopTabNavigator } from 'react-navigation';
class DynamicTabs extends Component {
render() {
// I am using a prop here to update the Tabs but you can use state to update
// when the network request has succeeded or failed
const { shouldRenderTab } = this.props;
const TabNavigator = createMaterialTopTabNavigator({
Tab1: Tab1Component,
Tab2: Tab2Component,
// Create a tab here that will display conditionally
...(shouldRenderTab ? { Tab3: Tab3Component } : {}),
});
const ContainedTabNavigator = createAppContainer(TabNavigator);
return <ContainedTabNavigator />;
}
}
export default DynamicTabs;
This is the current solution I am using adapted from the original solution posted on github

Related

Type error in getting route params within nested navigator using Typescript [react-navigation v6]

Having a navigation type definition as bellow, when I navigate from e.g AOne to BTwo with id:99 the console log of props.route.params shows correct info. But props.route.params.id throws type error
TypeError: undefined is not an object (evaluating 'props.route.params.id')
// navigation related imports in all components
import {BottomTabScreenProps} from '#react-navigation/bottom-tabs';
import {CompositeScreenProps, NavigatorScreenParams} from '#react-navigation/core';
import {StackScreenProps} from '#react-navigation/stack';
// type defenitions
export type StackOneParams = {
AOne:undefined,
BOne: undefined,
// some other screens
};
export type StackTwoParams = {
ATwo: undefined;
BTwo:{id:number};
// some other screens
};
export type TabParams = {
StackOne: NavigatorScreenParams<StackOneParams>;
StackTwo: NavigatorScreenParams<StackTwoParams>;
// ... some other stacks each represent a tab
};
export type RootParamList = {
ROne: undefined; // these screens should stand alone and not blong to any tab
RTwo: undefined;
Tabs: NavigatorScreenParams<TabParams>
}
// navigation from AOne to BTwo
props.navigation.navigate('Tabs', {
screen: 'StackTwo',
params: {screen: 'BTwo', params: {id: 99}}
}); // this part give correct auto complete hints in VSCode and no compilation error
// BTwo component (screen)
//--------------------------------
type Props = CompositeScreenProps<
StackScreenProps<RootParamList, 'Tabs'>,
CompositeScreenProps<
BottomTabScreenProps<TabPrams, 'StackTwo'>,
StackScreenProps<StackTwoParams, 'BTwo'>
>
>;// using CompositeScreenProps to be able to navigate to screens in another tabs
// otherwise just `type Props=StackScreenProps<StackTwoParams, 'BTwo'>` works fine but cannot navigate to other tabs
const BTwo:React.FC<Props> = (props) =>{
console.log(props.route.params)// the log shows {id:23}
// but props.route.params.id gives error and also no auto completion hint
return(...)
}
is this the correct way to define screen props for a specific screen, like what I have in BTwo screen? or the sequence of the composition should be different?
most of the examples (and the official documentation) show the most simple composition where the target screen is not even in second level of nesting (in the official doc profile is not really in nested bottom tabs)
How should I solve the type error in this case?
the image shows the VSCode auto-complete suggestions
Solution using CompositeScreenProps
My other explanations were not quite accurate. The way you have defined the CompositeScreenProp is not correct. Here is the correct way to implement this.
type ScreenProps = CompositeScreenProps<
StackScreenProps<StackTwoParams, "BTwo">,
CompositeScreenProps<
BottomTabScreenProps<TabParams, "StackTwo">,
StackScreenProps<RootParamList>
>
>
The first parameter of CompositeScreenProps contains the type of the navigator that owns the screen. In this case BTwo is owned by StackTwo and this determines the primary navigator, which is a Stack.
The above yields to the correct types as well.
const BTwo = (props: ScreenProps) => {
return <></>
}
Solution using separate types for navigation and route
We can type the navigation object and the route object separately as follows.
type NavigationProps = CompositeNavigationProp<
StackNavigationProp<StackTwoParams, "BTwo">,
CompositeNavigationProp<
BottomTabNavigationProp<TabParams, "StackTwo">,
StackNavigationProp<RootParamList>
>
>
type ScreenPropsA = {
navigation: NavigationProps
route: RouteProp<StackTwoParams, "BTwo">
}
Notice the usage of CompositeNavigationProp and RouteProp here.
Then, use it as follows.
const BTwo = ({ route, navigation }: ScreenProps) => {
return <></>
}
Both, route and navigation are now correctly typed.

i18n translation for "Back"-text in React Navigation on iOS

I'm using React Navigation and a StackNavigator in a React Native app. I'm able to translate the navigationOptions.title, however, on iOS if there is too much text in the header it defaults to showing the text "Back" next to the back button. I don't mind this, but I want it to say "Back" in my current language. How can I achieve this? The text I'm talking about:
An example of how I use the navigationOptions now:
class Example extends Component<Props> {
static navigationOptions = () => ({
title: i18n.t('example_title'),
});
// ...
}
In case it is relevant we're using react-native-localize with i18n-js for the i18n functionality. I don't want it to always say the previous screens name, or always back, I want it dynamically as it is now, just with i18n.
The navigationOptions object has an key for this called headerTruncatedBackTitle.
Title string used by the back button when headerBackTitle doesn't fit on the screen. "Back" by default. headerTruncatedBackTitle has to be defined in the origin screen, not in the destination screen.
You can for example utilize this with i18n simply by doing:
class Example extends Component<Props> {
static navigationOptions = () => ({
title: i18n.t('example_title'),
headerTruncatedBackTitle: i18n.t('example_back'), // "Back", "Zurück", etc.
});
// ...
}
In my application in Main AppStack createStackNavigator where you combine all of your screens, I have added a second parameter an object in which we give it a key
createStackNavigator({
...screens
},
{
defaultNavigationOptions: {
headerBackTitle: i18n.t('example_title')
}
})
This will set the Back button to the current language of the entire application.

How to create different top tabs in multiple screens in React Native

I am looking to create different tabs in different screens. This is a little hard to explain so i will post a couple photos to illustrate my desire output.
I've already created a tab navigator using createMaterialTopTabNavigator, but it seems like i can't apply the same logic twice in a whole separate js file. My javascript is fairly weak.
This is my code for the first tab navigation(newsfeed + services). I am looking to do the exact same thing except with different tab titles.
My question is, how would i go about achieving my desire output?
import {createMaterialTopTabNavigator} from 'react-navigation';
import NewsfeedActivity from './NewsfeedActivity';
import ServiceActivity from './ServiceActivity';
export default createMaterialTopTabNavigator({
Newsfeed:{screen: NewsfeedActivity},
Services:{screen:ServiceActivity}
},
{
initialRouteName:'Services',
swipeEnabled:true,
navigationOptions:({navigation})=>({
header:null
}),
tabBarOptions:{
activeTintColor:'#65FAE9',
inactiveTintColor:'white',
allowFontScaling:true,
indicatorStyle:{borderBottomColor:'#65FAE9', borderBottomWidth:4,},
style:{backgroundColor:'#515276',paddingBottom:5},
labelStyle:{fontWeight:'bold',marginTop:'40%'},
},
},
);
What i have:
What Im looking to create:
You can nest multiple navigators.
If your desired output is to have the bottom navigation different in "Newsfeed" and different in "Services" , then instead of passing a page as the screen , you can pass a bottomNavigator
import {createMaterialTopTabNavigator,createBottomTabNavigator} from 'react-navigation';
import NewsfeedActivity from './NewsfeedActivity';
import ServiceActivity from './ServiceActivity';
export default createMaterialTopTabNavigator({
Newsfeed:{screen: firstBottomNavigation},
Services:{screen: secondBottomNavigation }
},
{
initialRouteName:'Services',
swipeEnabled:true,
navigationOptions:({navigation})=>({
header:null
}),
tabBarOptions:{
activeTintColor:'#65FAE9',
inactiveTintColor:'white',
allowFontScaling:true,
indicatorStyle:{borderBottomColor:'#65FAE9', borderBottomWidth:4,},
style:{backgroundColor:'#515276',paddingBottom:5},
labelStyle:{fontWeight:'bold',marginTop:'40%'},
},
},
);
const firstBottomNavigation = createBottomTabNavigator({
FirstTab:{screen FirstTab},
SecondTab: {screen:SecondTab}
})
const secondBottomNavigation = createBottomTabNavigator({
ThirdTab:{screen ThirdTab},
SecondTab: {screen:SecondTab} //You can recycle screens
})
You can get creative , you can try nesting a toptabnavigator in a bottomnavigator ,etc . But don't make things too complicated because if it's complicated for you, imagine how hard is gonna be for the user

Localization of React Native navigators

I am building an app where the users are prompted to choose their language the first time they launch the app. The language is then stored locally using AsyncStorage.
Every time the app launches the language is retrieved from storage and saved into the global.lang variable to be used by all components:
AsyncStorage.getItem('settings', (err, item) => {
global.lang = item.lang;
});
When I use the global.lang variable in the render() method in any component everything seems to be ok. However I run into trouble when trying to use the same variable when initializing my navigators:
const TabNavigator = createBottomTabNavigator(
{
Home: {
screen: HomeScreenNavigator,
navigationOptions:{
title: strings['en'].linkHome, --> this works
}
},
News: {
screen: NewsScreen,
navigationOptions:{
title: strings[global.lang].linkNews, --> this fails
}
}
});
I believe that this because the value is not retrieved from AsyncStorage by the time that the navigators are constructed. If I set the global.lang manually (eg. global.lang = 'en';) it seems to be OK, but not when I try to retrieve it from the storage.
Is there something that I am missing? Could I initialize the navigator with a default language and change the title later based on the value retrived?
Any help would be greatly appreciated.
The navigators are constructed in the app launch. So you would need to use some placeholder text and use the method described here where you change all screen titles based on the screen key...
Or... this sounds insane and i have never tried it. But you can use a loading screen where you retrieve the languaje settings. then... via conditional rendering you "render" a navigator component . Idk if it would work the same way , but you can try it. below some code that i just created for this purpose
export default class MainComponent extends React.Component {
constructor(props) {
super(props);
this.state = { hasLanguage:false};}
componentDidMount(){
this.retrieveLanguage()
}
async retrieveLanguage(){
//await AsyncStorage bla bla bla
//then
this.setState({hasLanguage:true})
}
render() {
return (
{
this.state.hasLanguage?
<View>
//this is a view that is rendered as a loading screen
</View>:
<Navigator/>//this will be rendered, and hence, created, when there is a language retrieved
}
);
}
}
Again. I don't know if react navigation creates the navigator at render . If so. When it creates a navigator , there should be the languaje to be used there

How to add Android Back Handler for a particular screen?

I have three screens MyVault, Add Doc and Add Repo. From Myvault there is one button add new doc by clicking on that Add Doc will open. Now in Add doc if user presses Back button then I want a confirmation pop up. I have one button inside add doc screen which opens Add repo screen where user can select one repo and when they click add that screen will be popped and add doc screen will be refreshed with the repo data. If I add a listener in ComponentDidMount and then remove it in ComponentWillUnmount then the issue is that even when I press back on Add repo then also the popup comes. I don't want popup on any other screens, I just want it on Add doc.
Note: I am using react native router flux for routing
I have posted this issue on this link also : https://github.com/facebook/react-native/issues/15248
As per react-native-router-flux documentation, event handlers can be added to buttons. You can use onExit, onLeft or onRight. Something like:
<Scene
key="AddDoc"
component={AddDoc}
onExit={() => console.log('your modal pop up logic')}
/>
I was able to do it with the help of onEnter and onExit props of react-native-router-flux
Try the below method
import React from 'react';
import {View, Text, AlertPlatform,BackAndroid} from 'react-native';
class App extends React.Component {
constructor(props) {
super(props);
}
onBackAndroid = () => {
backButtonPressedOnceToExit ? BackAndroid.exitApp() : "";
backButtonPressedOnceToExit = true;
setTimeout(() => {backButtonPressedOnceToExit = false;}, 2000);
return true;
}
componentWillMount = () => BackAndroid.addEventListener('hardwareBackPress', this.onBackAndroid.bind(this));
componentWillUnmount = () => {
BackAndroid.removeEventListener('hardwareBackPress', this.onBackAndroid.bind(this));
}
render() {
return (
// Your code
);
}
}
export default App;