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

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

Related

Making navigation available to all components in React Native app

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

React navigation 5 access navigation with class components

I am currently migrating to react-navigation 5 (from react-navigation 4). My problem is that I am using class components and cannot access this.props.navigation, how do I pass the navigation to my class components and access it in my components.
My App.tsx looks like this:
export default function App(props:any) {
const Stack = createStackNavigator();
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="LoginScreen" component={LoginScreen} />
<Stack.Screen name="CreateUserScreen" component={CreateUserScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
And my pages looks like this:
interface ILoginScreenProps {
navigation:any
}
interface ILoginScreenState {
email: string
password: string
rememberlogin: boolean
//navigation: any
}
class LoginScreen extends React.Component<ILoginScreenProps,ILoginScreenState> {
gotoCreateUser = () => {
const { navigation } = this.props;
navigation.navigate('Createuser', {});
}
render() {
return ( rendering stuff.. );
}
}
I know it can be done functional, but I would currently like to keep it as class components.
I get the error:
TypeError: undefined is not an object (evaluating
'navigation.navigate')

Is there a way to pass a state to a component that was passed as a prop?

I'm trying to pass term from the HomeStackScreen into the HomeScreen function that I've used to create a .Screen in my HomeStack. I've tried passing it as a prop and an initial parameter in HomeStack.Screen. I'm able to change the state and I can confirm that with console.log() below. Seems to be that my Home component is not re-rendering/updating after created but I'm not sure how to force that update. I'm not sure what logic I'm missing and would be very grateful if someone was able to point me in the right direction.
I've included my code, actual output at the console, and what I thought I was writing to the console below.
Any tips or pointers would be great. Thanks in advance.
import React, {useState}from 'react';
import {createStackNavigator} from '#react-navigation/stack';
import Home from '../screens/home';
import HeaderLeft from '../components/headerLeft';
import HeaderRight from '../components/headerRight';
function HomeScreen({term}) {
console.log('Made it here!'.concat(term))
return (
<Home />
);
}
const HomeStack = createStackNavigator();
function HomeStackScreen() {
const [term, setTerm] = useState('First State');
console.log(term);
return (
<HomeStack.Navigator>
<HomeStack.Screen name=" "
component= {HomeScreen} initialParams = {{term}}
options = {{
headerStyle:{
backgroundColor: "#121212",
borderBottomColor:'#121212',
height:105,
shadowColor:'transparent'
},
headerLeft: () => <HeaderLeft/>,
headerRight: () => <HeaderRight setTerm = {setTerm}/>,
}}/>
</HomeStack.Navigator>
)
}
export default HomeStackScreen;
Console:
First State
Made it here!undefined
Second State
What I thought I was writing to the Console:
First State
Made it here!First State
Second State
Made it here!Second State
Assuming you're using react-navigation v5 there a change on pass params you can checkout here
On
`function HomeScreen({term}) {
console.log('Made it here!'.concat(term))
return (
<Home />
);
}`
Could you change it to
`function HomeScreen({route: {
params: { term },
},}) {
console.log('Made it here!'.concat(term))
return (
<Home />
);
}`
The initial Params property can be access inside route.params object
route.params
Example:
function HomeScreen({ navigation, route }) {
console.log('Made it here!'.concat(route.params.term))
return (
<Home />
);
}
Example 2:
https://snack.expo.io/BRK9_n_Dj

how to set initialRouteName dynamically in the react-native

how to give dynamic initial route name in the react-navigation? if the unit exists we have to redirect to another route or else we have to take user another route.
Note: I'm creating a bottom tab navigator in which I have to set an initial route to that particular bottom tab navigator.
(Not the authentication flow)
import React, {Component} from 'react';
import {createAppContainer} from 'react-navigation';
import {createMaterialBottomTabNavigator} from 'react-navigation-material-bottom-tabs';
... imports
function getInitialScreen() {
AsyncStorage.getItem('unit')
.then(unit => {
return unit ? 'Home' : 'secondTab';
})
.catch(err => {
});
}
const TabNavigator = createMaterialBottomTabNavigator(
{
Home: {
screen: HomeScreen,
navigationOptions: {
.....navigation options
},
},
secondTab: {
screen: secondTab,
},
},
{
initialRouteName: getInitialScreen(),
},
);
export default createAppContainer(TabNavigator);
See according to the docs, initialRoute name should not be a async func .
So ideally what you should do is , anyways you need a splashscreen for your app right, where you display the logo and name of app. Make that page the initialRoute and in its componentDidMount, check for the async function and navigate to ddesired page.
Like what ive done :
createSwitchNavigator(
{
App: TabNavigator,
Auth: AuthStack,
SplashScreen: SplashScreen,
},
{
initialRouteName: 'SplashScreen',
},
),
And inside SplashScreen im doing :
componentDidMount(){
if (token) {
this.props.navigation.navigate('App');
} else {
this.props.navigation.navigate('Auth');
}
}
Hope its clear. Feel free for doubts
As you can see here:
If you need to set the initialRouteName as a prop it is because there
is some data that you need to fetch asynchronously before you render
the app navigation. another way to handle this is to use a
switchnavigator and have a screen that you show when you are fetching
the async data, then navigate to the appropriate initial route with
params when necessary. see docs for a full example of this.
Take a look at here.
You'll find more description!
Also quick fix for this situation is check your condition inside SplashScreen componentDidMount() function
Example of SplashScreen :
componentDidMount(){
AsyncStorage.getItem('unit')
.then(unit => {
if(unit){
this.props.navigation.navigate('Home')
}else{
this.props.navigation.navigate('secondTab')
}
})
.catch(err => {
});
}
You can check the condition on the main page or App.js
render() {
const status = get AsyncStorage.getItem('unit');
if(status != null)
{ return <Home/> }
else
{ return <AnotherScreen/> }
}
But we can switch between pages if we use stacknavigator or switchnavigator..
We cant goto the particular tabs directly according to my knowledge. (Correct me if I am wrong).
The official documentation gives you this example to achieve pretty much what you want:
isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
</>
)
Ok So in your navigation page create state and change its value accordingly AsyncStorage like
render() {
const status = get AsyncStorage.getItem('unit');
if(status != null)
{ setState({ name : 'Home'}) }
else
{ setState({ name : 'anotherTab'}) }
}
then pass that state to tabNavigation
initialRouteName: this.state.name,
and this state , setState functions are classed based so you have to use useState instead to initial state on your page

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