Making navigation available to all components in React Native app - react-native

I'm using React Navigation 5 in my React Native app and I'm not exactly clear about how to make navigation available to all components. I also want to mention that my app uses Redux for state management but I didn't integrate React Navigation with Redux -- maybe I should!
My App.js renders my Navigator.js component which looks like below. BTW, my app requires authentication and one of the key functions of the navigator function is to redirect unauthenticated users to login screen.
class Navigator extends Component {
render() {
return (
<NavigationContainer>
{
this.props.isAuthenticated
? <MainMenuDrawer.Navigator drawerContent={(navigation) => <DrawerContent member={this.props.member} navigation={navigation} drawerActions={DrawerActions} handleClickLogOut={this.handleClickLogOut} />}>
<MainMenuDrawer.Screen name="Home" component={HomeStack} />
<MainMenuDrawer.Screen name="ToDoList" component={ToDoListStack} />
</MainMenuDrawer.Navigator>
: <SignInStack />
}
</NavigationContainer>
);
}
}
Then in my HomeStack, I have my Dashboard component which happens to be a class component that needs to access navigation. Here's my HomeStack:
const HomeStackNav = new createStackNavigator();
const HomeStack = () => {
return(
<HomeStackNav.Navigator screenOptions={{ headerShown: false }}>
<HomeStackNav.Screen name="DashboardScreen" component={Dashboard} />
<HomeStackNav.Screen name="SomeOtherScreen" component={SomeOtherComponent} />
</HomeStackNav.Navigator>
);
}
export default HomeStack;
Say, my Dashboard component looks like below. How do I make navigation available to this component?
class Dashboard extends Component {
handleNav() {
// Need to use navigation here...
}
render() {
return (
<Text>Welcome to Dashboard</Text>
<Button onPress={() => this.handleNav()}>Go somewhere</Button>
);
}
}
function mapStateToProps(state) {
return {
member: state.app.member
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(appActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
Do I need to pass navigation manually to each component? That seems too repetitive and prone to error. How do I make navigation available throughout my app?

Solution 1:
you can use useNavigation hook for functional component
import {useNavigation} from '#react-navigation/native';
const navigation = useNavigation();
navigation.navigate("interests");
//or
navigation.push("interests");
Solution 2:
you can use HOC withNavigation to navigation in props in any component for class component Ref
you can install #react-navigation/compat by
yarn add #react-navigation/compat
You can import like below
import { withNavigation } from '#react-navigation/compat';
you can use withNavigation like below
export default withNavigation(Dashboard)
Note: then you can use this.props.navigation in Dashboard component

Related

How to navigate back to parent page from inside an custom component in React Native

Greeting everyone,
I'm writing a React Native application and have stumbled into something interesting.
This is the simplified version of my code:
//import statements
...
const Profile = (props) => {
return(
<Text>Hey {props.name}!</Text>
<TouchableOpacity onPress={() => navigation.goBack();}>Go back</TouchableOpacity>
);}
export function ScreenOne({ navigation }) {
<Profile name="HUFF">
}
export function ScreenTwo({ navigation }) {
//blah blah blah
}
export default function App(){
//rest of the code
...
}
So basically, I'm trying to use react-navigation to navigate between screens of my App. But the button inside of my Profile component doesn't work.
Consider that I navigate from ScreenTwo to ScreenOne
Thanks in Advance.
React navigation has useNavigation hook
import { useNavigation } from '#react-navigation/native'
// if you are not using react-navigation 5 or 6
// then there is package https://github.com/react-navigation/hooks
const Profile = (props) => {
const navigation = useNavigation();
return(
<Text>Hey {props.name}!</Text>
<TouchableOpacity onPress={() => navigation.goBack()}>Go back</TouchableOpacity>
);
}
For this, you might have to specify navigation via props on the Profile JSX tag, so it can access the parent's navigation inside the component. E.g.:
//import statements
...
const Profile = (props) => {
return(
<Text>Hey {props.name}!</Text>
<TouchableOpacity onPress={() => props.navigation.goBack()}>Go back</TouchableOpacity>
);
}
function ScreenOne({ navigation }) {
<Profile name="HUFF" navigation={navigation}>
}
function ScreenTwo({ navigation }) {
//blah blah blah
}
export default function App(){
//rest of the code
...
}

onStateChange doesn't get called when wrapped in React.Component

I am trying to wrap the NavigationContainer in React component for screen tracking, this approach worked well in V4 but fails in V5 unfortunately. The follow-up question is: will it be possible to wrap it in a function component and not in the react component to have the ability to use hooks? (must admit I am relatively new to react)
Will really appreciate any assistance
App.js
const Drawer = createDrawerNavigator();
function MyDrawer() {
return (
<Drawer.Navigator drawerType="front" drawerPosition="left">
<Drawer.Screen name="Properties" component={PropertiesTabs} />
<Drawer.Screen name="Profile" component={Profile} />
</Drawer.Navigator>
);
}
const AppContainer = function App() {
return <NavigationContainer>{MyDrawer()}</NavigationContainer>;
}
export default with(AppContainer);
Wrapper.tsx
export function with(Component: any) {
class PNDContainer extends React.Component {
debounce;
componentDidMount() {
console.log('PND Mounted - First Time Screen');
}
componentWillUnmount() { }
render() {
return (<Component onStateChange={() => {
console.log('Screen Changed Doesnt get Called !!!');
}} />);
}
}
return PNDContainer;
}
Expected Behavior
onStateChange should be called, in the V4 the same approach did trigger the onNavigationStateChange
Enviroment
#react-navigation/native 5.7.0
react-native 0.61.5
node 13.10.1
yarn 1.22.1
I can understand why it doesnt work, as I am passing a function element that has no such prop onStateChange, in V4 CreatAppContainer returned a component that had the prop onNavigationStateChange
So I would like to call the function get the element and "inject" my onStateChange implementation, but I think react doesnt work that way (its more like imperative way and react is a declarative framework) so what will be a better approach?
So I tried to debug in chrome to see the onStateChange, no luck...
I think that I misunderstand the concepts, I read the following React Function Components
Edit
For now the only solution that worked for me is to wrap my component in NavigationContainer and returned it
<NavigationContainer onStateChange={() => {console.log('ProbablynewScreen');}}>{Component()}
</NavigationContainer>);
In that case, I noticed that discovering drawer is not that simple there is no clue in the state for the drawer and if I use Class component (unfortunately currently I must) I have no hooks, so how would I discover a drawer open/close ?
App.js
const Drawer = createDrawerNavigator();
function MyDrawer(props) {
return (
<NavigationContainer onStateChange={props.onStateChange}>
<Drawer.Navigator drawerType="front" drawerPosition="left">
<Drawer.Screen name="Properties" component={PropertiesTabs} />
<Drawer.Screen name="Profile" component={Profile} />
</Drawer.Navigator>
</NavigationContainer>
)
};
export default with(MyDrawer)
With.tsx
export function with(Component: any) {
class PNDContainer extends React.Component {
child: any;
componentDidMount() {
//debugger;
console.log('PND Mounted - First Time Screen');
}
componentWillUnmount() { }
render() {
debugger;
return (<Component onStateChange={(state) => {
debugger;
console.log('Screen Changed');
}} />)
}
}
return PNDContainer;
}
Thanks to WhiteBear

Navigating to screen in another navigator in React Navigation 5

I'm using the new React Navigation 5 in my React Native app.
The main menu is handled by the RootNavigator which is a Drawer. The login/sign up are handled by AuthNavigation which is a Stack.
I'm trying to send user to Home screen under RootNavigator after a successful login. In other words, I'm trying to send user to a screen under another navigator by issuing navigation.navigate('RootNavigator', {screen: 'Home'}); but I'm getting an error that reads:
The action 'NAVIGATE' with payload {"name":"RootNavigator"} was not
handled by any navigator.
Do you have a screen named 'RootNavigator'?
This is what my App.js looks like:
const App = () => {
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticatedUser, setIsAuthenticatedUser] = useState(false);
useEffect( () => {
// Check for valid auth_token here
// If we have valid token, isLoading is set to FALSE and isAuthenticatedUser is set to TRUE
});
return (
<Provider store={store}>
<PaperProvider>
<NavigationContainer>
{
isLoading
? <SplashScreen />
: isAuthenticatedUser ? <RootNavigator /> : <AuthNavigator />
}
</NavigationContainer>
</PaperProvider>
</Provider>
);
};
And here's my AuthNavigator which is a Stack:
import React from 'react';
import { createStackNavigator } from '#react-navigation/stack';
// Components
import Login from '../../login/Login';
const AuthStack = new createStackNavigator();
export const AuthNavigator = () => {
return(
<AuthStack.Navigator>
<AuthStack.Screen name="LoginScreen" component={Login} />
</AuthStack.Navigator>
);
};
And here's the RootNavigator which is a Drawer:
import React from 'react';
import { createDrawerNavigator } from '#react-navigation/drawer';
// Components
import Dashboard from '../../home/Dashboard';
import DrawerContent from './DrawerContent';
import ToDoList from '../../active_lists/to_do_lists/ToDoList';
const MainMenuDrawer = createDrawerNavigator();
export const RootNavigator = () => {
return (
<MainMenuDrawer.Navigator drawerContent={(navigation) => <DrawerContent navigation={navigation} />}>
<MainMenuDrawer.Screen name="Home" component={Dashboard} />
<MainMenuDrawer.Screen name="To Do List" component={ToDoList} />
</MainMenuDrawer.Navigator>
);
};
Any idea what I'm doing wrong here? I want to use the cleanest approach for handling this.
There are 2 issues:
You're doing navigation.navigate('RootNavigator', {screen: 'Home'});, which means navigate to Home screen in a navigator nested inside RootNavigator screen. but there's no screen named RootNavigator, you have a component named RootNavigator, but navigate expects a screen name.
Looking at your code, you don't have nested navigators, both navigators are still at the root, just rendered conditionally. So you'd usually do navigation.navigate('Home') for navigation.
Examples of how nesting works and how the structure looks when you nest: https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator
When you are conditionally defining screens/navigators, React Navigation will take care of rendering correct screens automatically after you change the condition. You don't do navigate in this case.
From the auth flow docs:
It's important to note that when using such a setup, you don't need to navigate to the Home screen manually by calling navigation.navigate('Home'). React Navigation will automatically navigate to the Home screen when isSignedIn becomes true.
Auth flow docs: https://reactnavigation.org/docs/auth-flow#how-it-will-work
So what you need to do, is change the isAuthenticatedUser state in your component. You can follow the guide in the docs for an example using React context, but if you're using Redux, move this state to Redux, or any other state management library you use.
And lastly, remove the navigation.navigate after successful login, since it'll happen automatically when you update your condition.

React navigation. Render extra views on top of navigator

I'm trying to create a React Native app that renders an area with interchangeable screens and fixed view with some additional data/menu. I tried this solution form official React Navigation, but I can't make it work.
import React, { Component } from "react";
import {createStackNavigator} from 'react-navigation';
import { Text, View,} from 'react-native';
import Details from './DetailsScreen';
import MainScreen from './MainScreen';
const RootStack = createStackNavigator({
Main: {screen: MainScreen,
navigationOptions: {
header: null
}
},
Details: {
screen: Details,
},);
class App extends Component {
static router = {
...RootStack.router,
getStateForAction: (action, lastState) => {
return MyStack.router.getStateForAction(action, lastState);
}
};
render() {
const { navigation } = this.props;
return(
<View>
<Text>Foo</Text> //this is rendering
<RootStack navigation={navigation}/> //this is not
</View>);
}
}
export default App;
Article form link suggests that I can wrap object created with createStackNavigator() in a parent View, but it renders only when it is the only thing returned from render(), otherwise it seems to be ignored. Example from link claims that it is possible to "render additional things", but I can't make it work. Is it possible to do this way?
You can try this:
//wrapper.js
function wrapNavigator(Navigator, Wrapper, wrapperProps = {}) {
const WrappedComponent = props => (
<Wrapper { ...props, ...wrapperProps}>
<Navigator {...props} />
</Wrapper>
);
WrappedComponent.router = Navigator.router;
WrappedComponent.navigationOptions = Navigator.navigationOptions;
return WrappedComponent;
};
//app.js
const App = (props) => <View>
<Text>Some text...</Text>
{children}
</View>
export default wrapNavigator(Stack, App);
But this will not work if there are no parent navigator for App component.

Passing navigation props from parent to child component

I am developing an application which starts from App.js. If user is using app, first time then LoginScreen rendered and if user is logged in already then HomeScreen rendered.
Here's my App.js,
export default class App extends Component{
state = {
isFirstTime: true,
}
renderIf(condition, content) {
if (condition) {
return content;
} else {
return null;
}
}
render(){
const { navigate } = this.props;
return(
<View style = {{flex: 1}} >
{
this.renderIf(this.state.isFirstTime, <LoginScreen />)
}
{
this.renderIf(!this.state.isFirstTime, <HomeScreen />)
}
</View>
);
}
}
But now when I am trying to navigate from LoginScreen to HomeScreen using StackNavigator, I am getting an error that is,
Cannot read property 'navigate' of undefined
So, simply mine question is that how to pass props from parent component to child component. That is I want to pass, this.props.navigation from App.js to LoginScreen.js
when you render your component just pass it like this <LoginScreen navigate={this.props.navigate} />
on your Component LoginScreen you can get it by using const navigate = this.props.navigate;
You can pass your navigation props from a container screen to children components by doing this:
<LoginScreen navigation={this.props.navigation} />
Then in your child component, receive it and use it in an action like this:
this.props.navigation.navigate('placeToGo');