I have a tabbar in my app using React Native Router Flux. There are a couple use cases where hiding or showing specific tabs based on the current user would be very helpful. The main ones I have run into are:
AB testing new tabs to specific users
Showing a special admin tab to certain users with certain privileges
The react-native-router-flux library does not support any options to do this from what I can see. How can I achieve this functionality?
The default tabbar component in react-native-router-flux is just the component from the react-navigation-tabs library. You can import this component directly into your code, customize as needed, and then pass it to react-native-router-flux through the tabBarComponent prop (documented here).
I created a new component, which you should be able to copy directly and just change the logic for actually hiding the tabs based on your state:
import React from 'react'
import { BottomTabBar } from 'react-navigation-tabs'
import { View, TouchableWithoutFeedback } from 'react-native'
import { connect } from 'react-redux'
const HiddenView = () => <View style={{ display: 'none' }} />
const TouchableWithoutFeedbackWrapper = ({
onPress,
onLongPress,
testID,
accessibilityLabel,
...props
}) => (
<TouchableWithoutFeedback
onPress={onPress}
onLongPress={onLongPress}
testID={testID}
hitSlop={{
left: 15,
right: 15,
top: 5,
bottom: 5,
}}
accessibilityLabel={accessibilityLabel}
>
<View {...props} />
</TouchableWithoutFeedback>
)
const TabBarComponent = props => (
<BottomTabBar
{...props}
getButtonComponent={({ route }) => {
if (
(route.key === 'newTab' && !props.showNewTab) ||
(route.key === 'oldTab' && props.hideOldTab)
) {
return HiddenView
}
return TouchableWithoutFeedbackWrapper
}}
/>
)
export default connect(
state => ({ /* state that you need */ }),
{},
)(TabBarComponent)
And then simply imported and used that in my Tabs component:
<Tabs
key="main"
tabBarComponent={TabBarComponent} // the component defined above
...
Detailed look at where these things are getting passed to
Looking at the line of the source of react-native-router-flux, it is using createBottomTabNavigator from the react-navigation library, and passing no component if you do not pass a custom tabBarComponent. The createBottomTabNavigator method in react-navigation comes from this line of the library, and is actually defined in react-navigation-tabs. Now, we can here see in react-navigation-tabs that if no tabBarComponent has been passed, it simply uses BottomTabBar, also defined in react-navigation-tabs. This BottomTabBar, in turn, takes a custom tab button renderer through props, called getButtonComponent.
Related
I'm using react native navigation bottom tabs, and I want to create a custom "bottom sheet" popup component, but I want that component to come over the bottom tabs. Does anybody know how to position elements on top of the bottom tabs?
Yes! You have to use react-native-portalize. Just wrap the elements you want to be rendered on top in a <Portal></Portal>. This will place it above a Bottom Tab navigator.
import { Portal } from 'react-native-portalize';
const FooterButton = () => {
return(
<Portal>
<View>
<Text>I appear above the Tab Navigator!</Text>
</View>
</Portal>
);
export default FooterButton;
Don't forget to wrap the whole app in the the Host:
//In app.js
import { Host } from 'react-native-portalize';
const App = () => {
return (
<Host>
<NavigationContainer>
<AppNavigator />
</NavigationContainer>
</Host>
)
}
export default App;
NOTE: The elements inside the Portal, do not clear when the navigator navigates to another screen. So to get around this, you have to only display the Portal, when the screen is active. Thankfully React Navigation 5+ provides a useIsFocused hook that accomplishes this perfectly.
import { Portal } from 'react-native-portalize';
import { useIsFocused } from '#react-navigation/native';
const FooterButton = () => {
const isFocused = useIsFocused();
// Only display the button when screen is focused. Otherwise, it doesn't go away when you switch screens
return isFocused ? (
<Portal>
<View style={styles.buttonContainer}>
<View style={styles.footer}>{props.children}</View>
</View>
</Portal>
) : null;
};
export default FooterButton;
If you want a modal-style popup, you can wrap react-native-modalize and wrap it with react-native-modalize
Thanks to livin52 on Reddit for the solution
When I use props.navigation.navigate("example"), it works normally. But if I import the component on another page it doesn't work anymore, props returns an empty object.
Works Fine:
const Menu = props =>{
console.log(props)
return(
<View style={styles.menuStyle}>
<TouchableOpacity style={styles.topicDiv} onPress={() =>props.navigation.navigate("Ads")}>
<View>
<Image style={styles.topicStyle} source={require ("../assets/security-camera.png")}/>
<Text style={styles.textStyle}>Câmeras</Text>
<Text style={styles.subTextStyle}>Veja como está a praia ao vivo 📷</Text>
If i try import Menu, navigation does not work:
import React from "react";
import { View } from "react-native";
import Menu from "./menu";
const Supermenu = () =>{
return(
<View>
<Menu></Menu>
</View>
)
}
export default Supermenu
If you use <Menu> inside of another component like <Supermenu>, React Navigation has no way to pass its navigation property in there. It only happens automatically if a component is a direct child of a screen (or its component property).
To have navigation available in Menu regardless of its position in the hierarchy, as long as it's a child of <NavigationContainer>, the best way is to make use of the useNavigation hook:
import { useNavigation } from '#react-navigation/core';
const Menu = props =>{
const navigation = useNavigation();
return (
<View style={styles.menuStyle}>
<TouchableOpacity style={styles.topicDiv} onPress={() => navigation.navigate("Ads")}>
...
See documentation for more detail.
If you are on an older version, there was also a HOC withNavigation that you could use.
You could also do the same in Supermenu and then pass navigation down manually.
use import {userNavigation} from '#react-navigation/core' instead of props navigate or you can add navigation props to the Menu component.
I'm looking to create a Stack Navigator that can handle animating specific elements between 2 screens. Fluid Transitions looked like a library I could use, but it doesn't support react-navigation 5.X. If there's a package that has this functionality for react-navigation v5, that would be great.
However, if there is no current package for v5, I'd like to extend the StackNavigator to handle this kind of functionality. I've been able to remove the default animations for the StackNavigator with something similar to the following (where transition is a bool taken in the options prop for the Stack.Screen:
const CustomTransitionStackNavigator = ({
initialRouteName,
children,
screenOptions,
...rest
}) => {
if (descriptors[state.routes[state.index].key].options.transition) {
return (
<View style={{ flex: 1 }}>
{descriptors[state.routes[state.index].key].render()}
</View>
);
}
return (
<StackView
{...rest}
descriptors={descriptors}
navigation={navigation}
state={state}
/>
);
};
I'd like to be able to use a Context (or some other method) of passing the transition progress to the scene's descendants in order to handle the animations. Is there some way to get the transition progress in v5? Or would this CustomTransitionStackNavigator need to manage that state? Thanks!
You can use CardAnimationContext or useCardAnimation (which is just a convenience wrapper for the first one) to get transition progress in a stack navigator.
For example:
import { useCardAnimation } from '#react-navigation/stack';
import React from 'react';
import { Animated } from 'react-native';
export const SomeScreen = () => {
const { current } = useCardAnimation();
return (
<Animated.View
style={{
width: 200,
height: 200,
backgroundColor: 'red',
transform: [{ scale: current.progress }],
}}
/>
);
};
This feature seems to be undocumented at the moment, but you can check TypeScript definitions to get some more information.
I am trying to navigate to the Main screen when Login button is pressed in Login.js. LoginForm loads perfectly but when I press the button I start receiving the error "TypeError: Undefined is not an object (evaluating 'navigation.navigate'). Before this was not a problem but it changed when I moved Login to a different component. I was getting a different error and I went through this but it didn't fix it: Getting undefined is not an object evaluating _this.props.navigation
Let me know if you need the whole code. Thanks :)
LoginForm.js
...
import Login from "./Login";
const LoginForm = props => {
return <Login />
}
...
Login.js
const Login = ({ navigation }) => {
return (
<TouchableOpacity
style={{ marginTop: 20 }}
activeOpacity={1}
onPress={() => navigation.navigate("Main")}
>
<View style={styles.loginButtonContainer}>
<Text style={styles.loginButtonText}>Login</Text>
</View>
</TouchableOpacity>
)}
...
I fixed it thanks to withNavigation. https://reactnavigation.org/docs/en/with-navigation.html
withNavigation is a higher order component which passes the navigation
prop into a wrapped component. It's useful when you cannot pass the
navigation prop into the component directly, or don't want to pass it
in case of a deeply nested child.
Login.js
import { withNavigation } from 'react-navigation'
const Login = ({ navigation }) => {
...
}
export default withNavigation(Login)
Anyway, if its possible, I would love to know how to fix it without this.
I am trying to set a NavigationBar for my Navigator. When I display a static title, it is OK. However, when I try to set title asynchronously (for example when I go to user's profile route, I get user's display name from API and set this.props.navigation.title when API call promise resolves) the title gets jumpy.
What would be the proper approach for this issue?
Here is my component (which is connected to redux store) that handles NavigationBar:
import React from 'react-native';
let {
Component,
Navigator,
View
} = React;
import {connect} from 'react-redux';
let NavigationBarRouteMapper = {
Title: (route, navigator, index, navState) => {
return (
<Text style={{marginTop: 15, fontSize: 18, color: colors.white}}>{this.props.navigation.title}</Text>
);
}
};
class App extends Component {
render() {
return (
<View style={{flex: 1}}>
<Navigator
ref={'navigator'}
configureScene={...}
navigationBar={
<Navigator.NavigationBar routeMapper={ NavigationBarRouteMapper } />
}
renderScene={(route, navigator) => {...}}
/>
</View>
);
}
}
export default connect(state => state)(App);
Since routeMapper is a stateless object and route seems to be the only way (best practice) to keep the state of NavigationBar.
route can be set with this.props.navigator.replace(newRoute); in child components. However, this will re-renderScene and will sometimes cause dead loop of reRendering child components. What we want is to change the NavigationBar itself without side effects.
Luckily, there's a way to keep the context of current route. Like this:
var route = this.props.navigator.navigationContext.currentRoute;
route.headerItems = {title: "Message"};
this.props.navigator.replace(route);
Refs:
How to change the title of the NavigatorIOS without changing the route in React Native
[Navigator] Binding the navigation bar with the underlying scene