I'm experiencing an issue with React Navigation 2.18.3. I cannot deep link to a route that belongs to a navigator that is nested. I have a top-level tab navigator, MainTabNavigator which has two routes, Home and Menu:
import React, { PureComponent } from 'react';
import { createMaterialTopTabNavigator } from 'react-navigation';
import Explore from '../Home/Home';
import Menu from '../Menu/Menu';
import { TabBarContainer as TabBar } from './TabBar';
class MainTabNavigator extends PureComponent {
getRoutes() {
return {
Explore: { screen: Explore },
Menu: { screen: Menu, path: 'menu' }
};
}
getOptions() {
return {
tabBarComponent: TabBar,
tabBarPosition: 'bottom',
swipeEnabled: false
};
}
render() {
const TabNavigator = createMaterialTopTabNavigator(
this.getRoutes(),
this.getOptions()
);
return (
<TabNavigator uriPrefix="myapp://" />
);
}
}
export default MainTabNavigator;
Menu itself is a stack navigator:
import React, { Component } from 'react';
import { createStackNavigator } from 'react-navigation';
import MenuPage from './MenuPage';
import Products from '../Products/Products';
import onNavigationStateChange from '../../util/onNavigationStateChange';
import Settings from './Settings/Settings';
class Menu extends Component {
getRoutes() {
return {
MenuPage: { screen: MenuPage, path: 'page' },
Products: { screen: Products, path: 'products' },
Settings: { screen: Settings }
};
}
getOptions() {
return {
initialRouteName: 'MenuPage',
headerMode: 'none',
cardStyle: { backgroundColor: '#fff' }
};
}
render() {
const Navigation = createStackNavigator(this.getRoutes(), this.getOptions());
const screenProps = { rootNavigation: this.props.navigation };
return (
<Navigation
uriPrefix="myapp://menu"
screenProps={screenProps}
onNavigationStateChange={onNavigationStateChange}
/>
);
}
}
export default Menu;
When I go to safari and navigate to myapp://menu/products, the Menu navigator navigates correctly, but the MainTabNavigator stays stuck on Home. To the user, it looks like no navigation took place. If the user then navigates to the Menu, they will find that the Menu is already navigated to the Products page.
How can I get MainTabNavigator to navigate correctly when responding to a deep link?
To anyone experiencing a similar problem:
The issue was that we were explicitly rendering our Navigators. As it says in the "Common Mistakes" section of the docs:
Most apps should only ever render one navigator inside of a React component, and this is usually somewhere near the root component of your app. This is a little bit counter-intuitive at first but it's important for the architecture of React Navigation.
Apparently, failing to do so prevents you from being able to deep link all the way through your navigators ¯\_(ツ)_/¯
Related
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
class Signout extends Component {
componentDidMount() {
this.props.signout();
}
render() {
return <div>
Sorry to see you go
</div>
}
};
export default connect(null, actions)(Signout);
This is not rendering the page to show sorry to see you go. When I click on the link to this component I just get an empty blank page. Several other of my components experience the same thing. Only components that don't need anything from the ComponenDidMount function work on the app.
The component is called from here:
import Dashboard from "../views/Dashboard/Dashboard.jsx";
import TagTable from "../views/TagTable/TagTable.jsx";
// #material-ui/icons
import DashboardIcon from "#material-ui/icons/Dashboard";
import Apps from "#material-ui/icons/Apps";
import SignOut from "../views/Auth/Signout";
var dashRoutes = [
{
path: "/dashboard",
name: "Dashboard",
icon: DashboardIcon,
component: Dashboard
},
{
path: "/tags",
name: "Tags",
icon: Apps,
component: TagTable
},
{
path: "/logout",
name: "Logout",
icon: Apps,
component: SignOut
},
{ redirect: true, path: "/", pathTo: "/dashboard", name: "Dashboard" }
];
export default dashRoutes;
The action call is as follows:
export const signout = () => {
localStorage.removeItem('token');
return {
type: AUTH_USER,
payload: ''
};
}
Try doing this.
return (
<div>
Sorry to see you go
</div>
)
Enclose your divs inside parenthesis.
Hum! Put the text inside h1 tags or p tags instead of divs.
I created a multiscreen app using React Navigator following this example:
import {
createStackNavigator,
} from 'react-navigation';
const App = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
export default App;
Now I'd like to add a global configuration state using the new builtin context api, so I can have some common data which can be manipulated and displayed from multiple screens.
The problem is context apparently requires components having a common parent component, so that context can be passed down to child components.
How can I implement this using screens which do not share a common parent as far as I know, because they are managed by react navigator?
You can make it like this.
Create new file: GlobalContext.js
import React from 'react';
const GlobalContext = React.createContext({});
export class GlobalContextProvider extends React.Component {
state = {
isOnline: true
}
switchToOnline = () => {
this.setState({ isOnline: true });
}
switchToOffline = () => {
this.setState({ isOnline: false });
}
render () {
return (
<GlobalContext.Provider
value={{
...this.state,
switchToOnline: this.switchToOnline,
switchToOffline: this.switchToOffline
}}
>
{this.props.children}
</GlobalContext.Provider>
)
}
}
// create the consumer as higher order component
export const withGlobalContext = ChildComponent => props => (
<GlobalContext.Consumer>
{
context => <ChildComponent {...props} global={context} />
}
</GlobalContext.Consumer>
);
On index.js wrap your root component with context provider component.
<GlobalContextProvider>
<App />
</GlobalContextProvider>
Then on your screen HomeScreen.js use the consumer component like this.
import React from 'react';
import { View, Text } from 'react-native';
import { withGlobalContext } from './GlobalContext';
class HomeScreen extends React.Component {
render () {
return (
<View>
<Text>Is online: {this.props.global.isOnline}</Text>
</View>
)
}
}
export default withGlobalContext(HomeScreen);
You can also create multiple context provider to separate your concerns, and use the HOC consumer on the screen you want.
This answer takes in consideration react-navigation package.
You have to wrap your App component with the ContextProvider in order to have access to your context on both screens.
import { createAppContainer } from 'react-navigation'
import { createStackNavigator } from 'react-navigation-stack'
import ProfileContextProvider from '../some/path/ProfileContextProvider'
const RootStack = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
const AppContainer = createAppContainer(RootStack)
const App = () => {
return (
<ProfileContextProvider>
<AppContainer />
</ProfileContextProvider>);
}
https://wix.github.io/react-native-navigation/docs/third-party-react-context/
As RNN screens are not part of the same component tree, updating the values in the shared context does not trigger a re-render across all screens. However you can still use the React.Context per RNN screen component tree.
If you need to trigger a re-render across all screens, there are many popular third party libraries such as MobX or Redux.
Im new to react native and navigation and wanna ask about how to show ads only at certain screen? For example i only want to show the ad on ScreenOne but not on ScreenTwo or ScreenThree and so on.
Right now im using react-native-admob to show the ads and write the code on the App.js
My code is Something like this:
import React, { Component } from 'react';
import {
View
} from 'react-native';
import { AdMobBanner } from 'react-native-admob';
import { createStackNavigator } from 'react-navigation';
import LoginScreen from './screens/LoginScreen';
import StrataScreen from './screens/StrataScreen';
import DrawerNavigator from './screens/DrawerNavigator';
class App extends Component {
render() {
return (
<View style={{ height: '100%', width: '100%' }}>
<AppStackNavigator />
<AdMobBanner
adSize='smartBannerLandscape'
adUnitID='ca-app-pub-3940256099942544/2934735716'
/>
</View>
);
}
}
const AppStackNavigator = createStackNavigator(
{
Login: LoginScreen,
Strata: StrataScreen,
Drawer: {
screen: DrawerNavigator,
navigationOptions: {
header: null
}
}
},
{
initialRouteName: 'Login',
navigationOptions: {
gestureEnabled: false
}
}
);
export default App;
You should take a look at High Order Components, write a component that returns the component itself, plus the adMobBanner, then use return the screen you want wrapped inside this HOC.
https://reactjs.org/docs/higher-order-components.html
Ok, lets suppose you want the ad on your login screen, you can import your HOC on app.js, and do
const AppStackNavigator = createStackNavigator(
{
Login: withAd(LoginScreen),
Strata: StrataScreen,
Drawer: {
screen: DrawerNavigator,
navigationOptions: {
header: null
}
}
},
{
initialRouteName: 'Login',
navigationOptions: {
gestureEnabled: false
}
}
);
where withAd is the HOC you created and imported here, or you can change you LoginComponent, on the file itself to return withAd(LoginComponent) and just keep using the way you are, the decision you have the make is if, you gonna need that screen with ad on every moment, or just with this navigator.
Edit: To have the same ad on all screens, I would recommend you to use some state store, either redux, mobx or even React New Context API to store if the screen should render the ad, where you can wrap you adBanner to a component, and put some logic to check if should render the ad, where when you change to a screen you should or should not show, you dispatch an action to change that variable, and the adBanner will re-render accordingly.
How do I combine StackNavigator and TabNavigator?
My following code works:
index.android.js :
import React, { Component } from 'react';
import { AppRegistry, Text, View } from 'react-native';
import { StackNavigator,TabNavigator } from 'react-navigation';
import TestComp1 from './src/components/TestComp1'
import TestComp2 from './src/components/TestComp2'
import TestComp3 from './src/components/TestComp3'
import TestComp4 from './src/components/TestComp4'
import TestComp5 from './src/components/TestComp5'
export default class myApp extends Component {
render() {
return (
<MyApp />
);
}
}
const MyApp = StackNavigator({
TestComp1: {screen:TestComp1},
TestComp2: {screen:TestComp2}
});
const Tabs = TabNavigator({
TestComp3: {screen:TestComp3},
TestComp4: {screen:TestComp4}
TestComp5: {screen:TestComp5}
});
AppRegistry.registerComponent('myApp', () => myApp);
This works only for StackNavigator. I want to keep the existing navigation and integrate TabNavigation. Now in TestComp1 if I do the following:
TestComp1 :
import { StackNavigator, TabNavigator } from 'react-navigation';
import { FooterTabs } from './routes/FooterTabs';
export default class HomePage extends Component {
static navigationOptions = {
header: null
};
render() {
return(
<View style={styles.container}>
<View style={styles.mainContent}>
<Button
onPress={() => this.props.navigation.navigate('TestComp1', {name: 'Lucy'})}
title="NewPage"
/>
<FooterTabs /> //Page shows all StackNavigator screens if I add this
</View>
</View>
)
}
}
And the FooterTabs:
import { StackNavigator, TabNavigator } from 'react-navigation';
import TestComp3 from '../TestComp3';
import TestComp4 from '../TestComp4';
import TestComp5 from '../TestComp5';
export const FooterTabs = TabNavigator({
Tab1: {
screen: TestComp3
},
Tab2: {
screen: TestComp4
},
Tab3: {
screen: TestComp5
},
})
The FooterTabs is shown but TestComp1 and TestComp2 are also shown everything below one another. How do I fix this? Thanks.
UPDATE 1:
UPDATE 2:
const Tabs = TabNavigator({
TestComp3: {screen:TestComp1},
TestComp4: {
screen:TestComp4,
navigationOptions: ({ navigation }) => ({
title: "TestComp4",
tabBarIcon: ({ tintColor }) => <MaterialIcons name="accessibility" size={20}/>
})
}
UPDATE 3
I added another const for DrawerNavigator and configured it like this:
const Drawer = DrawerNavigator({
First:{
screen: DrawerScreen1
},
Second:{
screen: DrawerScreen2
}
},{
initialRouteName:'First',
drawerPosition: 'left'
});
And included in the app:
const MyApp = StackNavigator({
TestComp1: {screen:TestComp1},
TestComp2: {screen:TestComp2},
Tabs: {
screen: Tabs
},
Drawer: {
screen: Drawer
},
}, {
initialRouteName: "Tabs"
});
I'm calling it :
<Button
onPress={() => this.props.navigation.navigate('DrawerOpen')}
title="Show Drawer"
/>
OnPress of this button the DrawerScreen1 is called but as a component, it does not show as a drawer from the left. What am I missing?
You can try this:
const Tabs = TabNavigator({
TestComp3: {screen:TestComp3},
TestComp4: {screen:TestComp4}
TestComp5: {screen:TestComp5}
});
const MyApp = StackNavigator({
TestComp1: {screen:TestComp1},
TestComp2: {screen:TestComp2},
Tabs: {
screen: Tabs
}
}, {
initialRouteName: "Tabs"
});
and remove <FooterTabs /> from TestComp1
Let's see now. What you need is first a StackNavigator then inside one of the screens of the StackNavigator you need a TabNavigator. Right?
The answer to this would be the following:
For your index.android.js:
import React, { Component } from 'react';
import { AppRegistry, Text, View } from 'react-native';
import { StackNavigator } from 'react-navigation';
import TestComp1 from './src/components/TestComp1'
import TestComp2 from './src/components/TestComp2'
// Don't need to export default here since this is the root component
// that is registered with AppRegistry and we don't need to import it anywhere.
class myApp extends Component {
render() {
return (
<MyApp />
);
}
}
// Notice that these two screens will consist the navigation stack.
// Assume, TestComp1 contains our Tabbed layout.
const MyApp = StackNavigator({
TestComp1: { screen:TestComp1 }, // This screen will have tabs.
TestComp2: { screen:TestComp2 }
});
AppRegistry.registerComponent('myApp', () => myApp);
Let's now move on to your TestComp1, which is the screen that has your tabs.
TestComp1:
// ... all imports here.
// This should be your first tab.
class Home extends Component {
static navigationOptions = {
// Label for your tab. Can also place a tab icon here.
tabBarLabel: 'Home',
};
render() {
return(
<View style={styles.container}>
<View style={styles.mainContent}>
// This will change your tab to 'Profile'.
<Button
onPress={() => this.props.navigation.navigate('Profile', {name: 'Lucy'})}
title="NewPage"
/>
</View>
</View>
)
}
}
// This can be your second tab and so on.
class Profile extends Component {
static navigationOptions = {
// Label for your tab.
tabBarLabel: 'Profile',
};
render() {
return (
<Text>This is the profile Tab.<Text>
);
}
}
export default TabNavigator({
Home: {
screen: Home,
},
Profile: {
screen: Profile,
},
}, {
// This will get the tabs to show their labels/icons at the bottom.
tabBarPosition: 'bottom',
animationEnabled: true,
tabBarOptions: {
activeTintColor: '#e91e63',
},
});
So essentially, your TestComp1 has two components (Home and Profile) inside it which are both parts of TabNavigator so they'll act as tabs. (You don't need to worry about a separate footer component as ReactNavigation places that automatically using your component's navigationOptions) We'll be exporting this TabNavigator instance that we'll use as an import to index.android.js and we'll place this import inside our StackNavigator as a screen of the app.
This way you've incorporated Tabs as well as StackNavigator inside your RN app. Also, tabBarPosition: 'bottom' places your tabs at the bottom.
You also no longer to maintain a separate FooterTabs component as you can see.
Read up the docs if you need more clarity or fine-tuning.
Hope I've helped. :)
I'm trying to create a simple navigation in the following page:
Page1:
import React, { Component } from 'react';
import { Text, View, TouchableOpacity, Alert } from 'react-native';
import { StackNavigator } from 'react-navigation';
import { connect } from 'react-redux';
var styles = require('./styles');
class Page1 extends Component {
static navigationOptions = {
header: null
};
componentDidMount() { // <== Edited
setTimeout(function() {
const { navigate } = this.props.navigation;
},4000);
}
render() {
const { navigate } = this.props.navigation; // <= Getting error here
const { fname, lname } = this.props.person;
return (
<View style={styles.container}>
<Text style={styles.welcome}>
From Page 1 - {lname} { fname}
</Text>
<TouchableOpacity
// onPress={() => this.props.navigation.navigate('Page2', { user: 'Lucy' }) }
>
<Text style={styles.welcome}> click for Page 2</Text>
</TouchableOpacity>
</View>
);
}
}
function mapStateToProps(state) {
return {
person:state.person
}
}
export default connect(mapStateToProps) (Page1);
Page 3:
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import Page1 from './Page1'
import { Provider, connect } from 'react-redux'
import configureStore from './store'
const store = configureStore()
export default class Page3 extends Component {
render() {
return (
<Provider store={store}>
<Page1 />
</Provider>
);
}
}
And in the index page I'm importing Page1, Page2 and Page3 and :
const SimpleApp = StackNavigator({
Page1: { screen: Page1 },
Page2: { screen: Page2 },
Page3: { screen: Page3 },
});
AppRegistry.registerComponent('my03', () => SimpleApp);
The app works fine unless I comment const { navigate } = this.props.navigation;. I get the following error:
Undefined is not an object (evaluating 'this.props.navigation.navigate')
I also tried:
onPress={() => this.props.navigation.navigate('Page2', { user: 'Lucy' }) } and
onPress={() => navigation.navigate('Page2', { user: 'Lucy' }) }
Not really sure why I tried it but was going to figure it out if it worked. It did not.
I'm trying to use ReactNavigation without creating the reducer for it, cause I did not understand this part even after trying for 2 days.
Why is this failing here? Please help.
Many thanks.
UPDATE1
import React, { Component } from 'react';
import { AppRegistry, Text, View } from 'react-native';
import { StackNavigator } from 'react-navigation';
import Page1 from './src/components/Page1'
import Page2 from './src/components/Page2'
import Page3 from './src/components/Page3'
const SimpleApp = StackNavigator({
Page3: { screen: Page3 },
Page2: { screen: Page2 },
Page1: { screen: Page1 },
});
AppRegistry.registerComponent('my03', () => SimpleApp);
Short answer. Just do this. <Page1 navigation={this.props.navigation} />
Explanation - The reason why this.props.navigation is coming out undefined is that you're essentially using another instance of the Page 1 component inside your Page 3 and not the one that you used to initialize the StackNavigator with. So it's an entirely new Page 1 that is not coupled with the StackNavigator at all. If Page 1 would have been the starting component of your StackNavigator. Then this.props.navigation would not have been undefined which brings me to another point of interest.
Why would you ever nest Page 1 inside Page 3 but want the same page as a sibling to Page 3 inside your react navigation stack? The idea is to add components inside our StackNavigator as screens without nesting them together and we move from one screen to another using this.props.navigation.navigate inside any one of them. So, therefore, our Page 3 would have our navigation prop (since I'm assuming it gets loaded first through the StackNavigator directly) we just pass that prop to our nested Page 1 component and viola! You would now have access to this.props.navigation inside your Page 1.
Also, since your Page 3 has the <Provider > tag I'm assuming it's something more of a root. In that case, you're better off using <SimpleApp > in place of <Page1 > and keeping Page 1 as the starting point of your Stack. You can then register your root component as AppRegistry.registerComponent('my03', () => Page3);
The last piece of info, you can keep your redux state and your navigation completely decoupled from each other so use redux integration only when you're absolutely sure that you need your navigation state inside your redux store. A project which has both Redux and ReactNavigation doesn't mean that you have to integrate them both. They can be completely separate.
Phew! Hope it helps! :)