Custom header in nested TabNavigator - react-native

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

Related

How to use hook with SwitchNavigator

I'm trying to use https://github.com/expo/react-native-action-sheet with switch navigator.
I'm not sure how to do the basic setup as the example in the readme is different than my App.js. I'm using react 16.8 so I should be able to use hooks.
My App.js
import { useActionSheet } from '#expo/react-native-action-sheet'
const AuthStack = createStackNavigator(
{ Signup: SignupScreen, Login: LoginScreen }
);
const navigator = createBottomTabNavigator(
{
Feed: {
screen: FeedScreen,
navigationOptions: {
tabBarIcon: tabBarIcon('home'),
},
},
Profile: {
screen: ProfileScreen,
navigationOptions: {
tabBarIcon: tabBarIcon('home'),
},
},
},
);
const stackNavigator = createStackNavigator(
{
Main: {
screen: navigator,
// Set the title for our app when the tab bar screen is present
navigationOptions: { title: 'Test' },
},
// This screen will not have a tab bar
NewPost: NewPostScreen,
},
{
cardStyle: { backgroundColor: 'white' },
},
);
export default createAppContainer(
createSwitchNavigator(
{
AuthLoading: AuthLoadingScreen,
App: stackNavigator,
Auth: AuthStack,
},
{
initialRouteName: 'AuthLoading',
}
);
const { showActionSheetWithOptions } = useActionSheet();
);
Update, I'm getting this error when calling the showActionSheetWithOptions inside my component:
Hooks can only be called inside the body of a function component. invalid hook call
This is my code:
import React, { Component } from 'react';
import { useActionSheet } from '#expo/react-native-action-sheet'
export default class NewPostScreen extends Component {
_onOpenActionSheet = () => {
const options = ['Delete', 'Save', 'Cancel'];
const destructiveButtonIndex = 0;
const cancelButtonIndex = 2;
const { showActionSheetWithOptions } = useActionSheet();
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
destructiveButtonIndex,
},
buttonIndex => {
console.log(buttonIndex);
},
);
};
render () {
return (
<View>
<Button title="Test" onPress={this._onOpenActionSheet} />
</View>
)
}
}
update 2
I also tried using a functional component, but the actionsheet does not open (console does print "pressed")
// ActionSheet.js
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
import { useActionSheet } from '#expo/react-native-action-sheet'
export default function ActionSheet () {
const { showActionSheetWithOptions } = useActionSheet();
const _onOpenActionSheet = () => {
console.log("pressed");
const options = ['Delete', 'Save', 'Cancel'];
const destructiveButtonIndex = 0;
const cancelButtonIndex = 2;
showActionSheetWithOptions(
{
options,
cancelButtonIndex,
destructiveButtonIndex,
},
(buttonIndex) => {
console.log(buttonIndex);
},
);
};
return (
<TouchableOpacity onPress={_onOpenActionSheet} style={{height: 100,}}>
<Text>Click here</Text>
</TouchableOpacity>
);
};
Problem
As you can see here. You are not connecting your application root component.
Solution
import connectActionSheet from #expo/react-native-action-sheet and connect your application root component to the action sheet.
Simply modify your App.js to reflect the following:
// ... Other imports
import { connectActionSheet } from '#expo/react-native-action-sheet'
const AuthStack = createStackNavigator({
Signup: SignupScreen,
Login: LoginScreen
});
const navigator = createBottomTabNavigator({
Feed: {
screen: FeedScreen,
navigationOptions: {
tabBarIcon: tabBarIcon('home'),
},
},
Profile: {
screen: ProfileScreen,
navigationOptions: {
tabBarIcon: tabBarIcon('home'),
},
},
});
const stackNavigator = createStackNavigator({
Main: {
screen: navigator,
// Set the title for our app when the tab bar screen is present
navigationOptions: { title: 'Test' },
},
// This screen will not have a tab bar
NewPost: NewPostScreen,
}, {
cardStyle: { backgroundColor: 'white' },
});
const appContianer = createAppContainer(
createSwitchNavigator({
AuthLoading: AuthLoadingScreen,
App: stackNavigator,
Auth: AuthStack,
}, {
initialRouteName: 'AuthLoading',
})
);
const ConnectApp = connectActionSheet(appContianer);
export default ConnectApp;
Now on any of your application screens (i.e. Feed, Profile, Main, etc.) you can access the action sheet as follows:
If Stateless Component
// ... Other imports
import { useActionSheet } from '#expo/react-native-action-sheet'
export default function Profile () {
const { showActionSheetWithOptions } = useActionSheet();
/* ... */
}
If Statefull Component
// ... Other imports
import React from 'react'
import { useActionSheet } from '#expo/react-native-action-sheet'
export default Profile extends React.Component {
const { showActionSheetWithOptions } = useActionSheet();
/* ... */
}
Note: You can also access the action sheet as stated below from the docs
App component can access the actionSheet method as this.props.showActionSheetWithOptions

Set title in createStackNavigator onPress

I am making react native app. I have button and onPress i want to send headerTitle to createStackNavigator but i have some errors. I am sending title with setParams and in createStackNavigator i get it with getParam.
Why it is not working how is correct?
Code:
// page.js
<TouchableOpacity
onPress={() => navigate('Page2', {
this.props.navigation.setParams({Title: 'Title'});
})}>
// createStackNavigator
export default createStackNavigator(
{
Main: {
screen: Page1,
},
Page2: {
screen: Page2,
navigationOptions: ({ navigation }) => ({
headerTitle: navigation.state.getParam('Title', 'Default Title'),
}),
},
},
{
initialRouteName: 'Main',
}
);
The problem appears to be occurring in the operating order of the app.
Because the value is already obtained before the value is set, the value must be null or undefind, which is the value before the setting.
Try using it as a global function.
globalScreen
let TITLE = "defaultTitle"
function setT(data){
TITLE = data;
}
function getT() {
return TITLE;
}
export { setT, getT }
Usage
// page.js
import {setT } from "globalScreen"
<TouchableOpacity
onPress={() => {
setT("page2");
navigate('Page2'); }}>
// createStackNavigator
import {getT } from "globalScreen"
export default createStackNavigator(
{
Main: {
screen: Page1,
},
Page2: {
screen: Page2,
navigationOptions: ({ navigation }) => ({
headerTitle: getT(),
}),
},
},
{
initialRouteName: 'Main',
}
);

Set navigator parameters from child navigator?

I have a Stack navigator (RecipesTab) nested in a Tab navigator (NavBar) and I'm trying to hide the Tab Bar on RecipeSite. My current solution is to pass showTabBar up the tree but I'm having trouble setting the navigation parameters for NavBar from RecipesTab. Wondering if it's possible to somehow call this.props.navigation.setParams({...}) from the RecipesTab navigator or pass parameters to NavBar from RecipesTab in another way.
//class RecipeList...
//class IngredientsTab...
class RecipeSite extends Component {
render() {
this.props.navigation.setParams({showTabBar: false});
return;
}
}
const RecipesTab = createStackNavigator(
{
Main: {
screen: RecipeList,
},
Site: {
screen: RecipeSite,
}
},
{
initialRouteName: 'Main',
}
);
export default NavBar = createBottomTabNavigator(
{
Recipe: {
screen: RecipesTab,
navigationOptions: ({ navigation }) => ({
tabBarVisible: navigation.getParam('showTabBar', true)
}),
},
Ingredient: {
screen: IngredientsTab,
}
},
);`
Basically I'm just trying to send data from RecipesTab to NavBar.

React native TabNavigator navigation params

I want to send route params to Profile tab from button click action in Home screen. Below code is tab navigator
const Tabs = TabNavigator({
[routes.HOME]: {
screen: FeedMainScreen,
navigationOptions: {
tabBarLabel: tabBarLabels.HOME_TAB_LABEL,
},
},
[routes.SEARCH]: {
screen: SearchScreen,
navigationOptions: {
tabBarLabel: tabBarLabels.SEARCH_TAB_LABEL,
},
},
[routes.PROFILE]: {
screen: ProfileScreen,
navigationOptions: {
tabBarLabel: tabBarLabels.PROFILE_TAB_LABEL,
},
},
}
and button click navigation is like this
<Button onPress={() => this.props.navigation.navigate(routes.PROFILE, { id: this.props.user._id }, null)} />
and accsess param from Profile screen like this
constructor (props) {
super(props)
console.log('NavPrams', this.props.navigation.state)
}
alwaya undifined
You can use connect() method to successfully map the props and use them anywhere.
Add the below mentioned code in the class where is written -
const mapStateToProps = state => {
return {
user: this.props.user;
};
};
export default connect(mapStateToProps, {})(className);

I don't have headers on my screens but i want to have header only on one screen

I use reactnavigation for navigation. I don't have headers on my screens but i want to have header only on one screen (especially back button).
This is my root navigator:
...
class RootNavigation extends Component {
props: Props;
render() {
const { isProfileSelect } = this.props;
const initialRouteName = currentRoute(isProfileSelect);
const RootStackNav = StackNavigator(
{
AppHome: { screen: TabBarNavigator },
SwitchProfile: { screen: SwitchProfileScreen },
Pin: { screen: PinScreen },
},
{
initialRouteName,
headerMode: 'none',
},
);
return <RootStackNav />;
}
}
...
there is headerMode: 'none', it does mean i haven't any header navigator it is good for screens AppHome,SwitchProfile but not for Pin i need back button on header. Because I have to go on SwitchScreen and there i can continue on Pin screen and from Pin screen i sometime need go back to SwitchScreen And i have no back button on IOS. I need set header only in pin screen.
this is navigationOptions for pin screen:
...
static navigationOptions = {
title: I18n.t('screens.pinScreen.navigationOptions.title'),
};
...
but changes haven't any effect
Can you help me to add header only on Pin screen?
Thank you
update:
SOLUTION
const RootStackNav = StackNavigator(
{
AppHome: {
screen: TabBarNavigator,
navigationOptions: {
title: 'Header title',
headerMode: 'screen',
header: null,
},
},
SwitchProfile: {
screen: SwitchProfileScreen,
navigationOptions: {
title: 'Header title',
headerMode: 'screen',
header: null,
},
},
Pin: {
screen: PinScreen,
navigationOptions: {
title: 'Header title',
headerMode: 'screen',
// header: null,
},
},
},
{
initialRouteName,
// headerMode: 'none',
},
);
My case was reverse of yours...
I wanted to disable the header on one screen so I added the following code in that particular screen.
export default class NoHeaderView extends Component {
static navigationOptions = { header: null }
...
//your constructor, render and other stuff
...
}//
Let me know if it works.