undefined is not an object (evaluating 'component.router.getStateForAction') - react-native

I'm seeing the following error when I load my react native app on my simulators. It appears to be related to my usage of react-navigation. But in googling the error, I'm unable to find what I'm missing.
It is calling out the "wrappedComponent" in this code snippet.
function WithAuth(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
ready: false,
session: null,
};
this.handleOnSignIn = this.handleOnSignIn.bind(this);
this.handleOnSignUp = this.handleOnSignUp.bind(this);
this.handleOnSignOut = this.handleOnSignOut.bind(this);
}
async componentDidMount() {
await LocalStorage.init();
let session;
try {
session = await Auth.currentSession();
} catch (err) {
console.log(err);
session = null;
}
this.setState({
session,
ready: true,
});
}
handleOnSignIn(session) {
this.setState({ session });
}
handleOnSignUp() { }
handleOnSignOut() {
Auth.signOut();
this.setState({ session: null });
}
render() {
const { ready, session } = this.state;
console.log('Rendering HOC', ready, !!session);
const {
onSignIn,
onSignUp,
doSignOut,
...otherProps
} = this.props;
return (
ready && (
<WrappedComponent
session={session}
onSignIn={onSignIn || this.handleOnSignIn}
onSignUp={onSignUp || this.handleOnSignUp}
doSignOut={doSignOut || this.handleOnSignOut}
auth={Auth}
{...otherProps}
/>
)
);
}
}
}
export default WithAuth;
which is utilizing the following app.js code:
Amplify.configure(awsmobile);
const App = createDrawerNavigator({
FirstScreen: {
screen: props => <First rootNavigator={props.navigation} screenProps={{ ...props.screenProps }} />,
navigationOptions: {
drawerLabel: ' ',
},
},
}, { initialRouteName: 'FirstScreen' });
const AppContainer = createAppContainer(props => <App screenProps={{ ...props }} />);
export default WithAuth(AppContainer);
(the export is what calls the WithAuth.)
This some of this code was pulled from an app using react-navigation 2.0 and I've updated to 3.0 and am thinking I'm missing something that is now required, however, I can't find it.
Any help is appreciated!

createAppContainer needs to directly wrap the navigator component. Updating your code, it looks like this:
Amplify.configure(awsmobile);
const App = createDrawerNavigator({
FirstScreen: {
screen: props => <First rootNavigator={props.navigation} screenProps={{ ...props.screenProps }} />,
navigationOptions: {
drawerLabel: ' ',
},
},
}, { initialRouteName: 'FirstScreen' });
const AppContainer = createAppContainer(App);
export default WithAuth(AppContainer);

Set AppContainer as your root component, like this:
const AppContainer = createAppContainer(props => WithAuth(_ => <App screenProps={{ ...props }} />));
export default AppContainer;

Related

How to use AsyncStorage in createBottomTabNavigator with React Navigation?

I work with react-navigation v3 and I want to use AsyncStorage in createBottomTabNavigator for checking if user logged.
I save key to Stoage in LoginScreen:
await AsyncStorage.setItem('#MyStorage:isLogged', isLogged);
And I want to use AsyncStorage in my stack (TabStack):
const TabStack = createBottomTabNavigator(
{
Home: { screen: HomeScreen, },
// I need isLogged key from AsyncStorage here!
...(false ? {
Account: { screen: AccountScreen, }
} : {
Login: { screen: LoginScreen, }
}),
},
{
initialRouteName: 'Home',
}
);
How I can do it?
My environment:
react-native: 0.58.5
react-navigation: 3.3.2
The solution is: create a new component AppTabBar and set this in tabBarComponent property
const TabStack = createBottomTabNavigator({
Home: { screen: HomeScreen, },
Account: { screen: AccountScreen, }
},
{
initialRouteName: 'Home',
tabBarComponent: AppTabBar, // Here
});
And AppTabBar component:
export default class AppTabBar extends Component {
constructor(props) {
super(props);
this.state = {
isLogged: '0',
};
}
componentDidMount() {
this._retrieveData();
}
_retrieveData = async () => {
try {
const value = await AsyncStorage.getItem('isLogged');
if (value !== null) {
this.setState({
isLogged: value,
});
}
} catch (error) {
// Error retrieving data
}
};
render() {
const { navigation, appState } = this.props;
const routes = navigation.state.routes;
const { isLogged } = this.state;
return (
<View style={styles.container}>
{routes.map((route, index) => {
if (isLogged === '1' && route.routeName === 'Login') {
return null;
}
if (isLogged === '0' && route.routeName === 'Account') {
return null;
}
return (
<View /> // here your tabbar component
);
})}
</View>
);
}
navigationHandler = name => {
const { navigation } = this.props;
navigation.navigate(name);
};
}
You don't need to do that, just may want to check for a valid session in the login screen.
You need to create 2 stacks, one for the auth screens and your TabStack for logged users:
const TabStack = createBottomTabNavigator({
Home: { screen: HomeScreen, },
Account: { screen: AccountScreen, }
},
{
initialRouteName: 'Home',
headerMode: 'none',
navigationOptions: {
headerVisible: false,
}
});
const stack = createStackNavigator({
Home: {screen: TabStack},
Login: { screen: LoginScreen, }
});
and then check for a valid session in LoginScreen in the method componentDidMount.
class LoginScreen extends Component {
componentDidMount(){
const session = await AsyncStorage.getItem('session');
if (session.isValid) {
this.props.navigate('home')
}
}
}
In your Loading screen, read your login state from AsyncStorage, and store it in your Redux store, ( or any sort of global data sharing mechanism of your choice ) - I'm using redux here, then read this piece of data in your Stack component like the following:
import React from "react";
import { View, Text } from "react-native";
import { connect } from "react-redux";
import { createStackNavigator } from "react-navigation";
class Stack extends React.Component {
render() {
const { isLoggedIn } = this.props.auth;
const RouteConfigs = {
Home: () => (
<View>
<Text>Home</Text>
</View>
),
Login: () => (
<View>
<Text>Login</Text>
</View>
)
};
const RouteConfigs_LoggedIn = {
Home: () => (
<View>
<Text>Home</Text>
</View>
),
Account: () => (
<View>
<Text>Account</Text>
</View>
)
};
const NavigatorConfig = { initialRouteName: "Login" };
const MyStack = createStackNavigator(
isLoggedIn ? RouteConfigs_LoggedIn : RouteConfigs,
NavigatorConfig
);
return <MyStack />;
}
}
const mapStateToProps = ({ auth }) => ({ auth });
export default connect(mapStateToProps)(Stack);

Combine React Navigation 3.0 and JWT to determine initialRouteName based on auth state

I am trying to make a normal App.js component integrated with JWT client to behave correctly when integrating React Navigator 3.0
Unfortunately I can only make work one or the other, not both. My issue is that React Navigator sort of hijacks App.js and determines the initial route instead of the usual App component render.
Here is my code so far:
App.js
import React, { Component } from 'react';
import { createAppContainer,
createBottomTabNavigator,
} from 'react-navigation';
import Auth from './src/screens/Auth';
import HomeScreen from './src/screens/HomeScreen';
class App extends Component {
constructor() {
super();
this.state = {
jwt: '',
};
}
render() {
if (!this.state.jwt) {
return (
<Auth />
);
} else if (this.state.jwt) {
return (
<HomeScreen />
);
}
}
}
const TabNavigator = createBottomTabNavigator({
Home: { screen: HomeScreen,
navigationOptions: {
tabBarLabel: 'Home',
}
},
Auth: { screen: Auth,
navigationOptions: {
tabBarLabel: 'Auth',
}
},
},
{ initialRouteName: App.state.jwt ? 'Home' : 'Auth' }
);
export default createAppContainer(TabNavigator);
As you can see, my issue is with this line:
{ initialRouteName: App.state.jwt ? 'Home' : 'Auth' }
How can I get the JWT state inside the TabNavigator component so I can define the correct initialRouteName?
this.state.jwt and App.state.jwt obviously do not work, and I tried (and failed) to pass the state to the TabNavigator object as a prop.
Any help is appreciated.
This is the correct method:
First you set get the Auth token and save its state in App.js. Then you pass the state as screenProps to the Navigation object:
export default class App extends React.Component {
constructor() {
super();
this.state = {
jwt: '',
loading: true
};
this.newJWT = this.newJWT.bind(this);
this.deleteJWT = deviceStorage.deleteJWT.bind(this);
this.loadJWT = deviceStorage.loadJWT.bind(this);
this.loadJWT();
}
state = {
isLoadingComplete: false,
};
newJWT(jwt) {
this.setState({
jwt: jwt
});
}
render() {
if (this.state.loading) {
return (
<Loading size={'large'} />
);
} else if (!this.state.jwt) {
return (
<View style={styles.container}>
{Platform.OS === 'ios' && <StatusBar barStyle="default" />}
<TabNavigator screenProps={{setToken: this.newJWT }} />
</View>
);
}
The navigation object passes it on to the screens that need it.
const TabNavigator = createMaterialTopTabNavigator(
{
Profile: {
screen: props => <ProfileScreen {...props.screenProps} />,
navigationOptions: {
//tabBarLabel: 'Perfil',
title: 'Header Title',
tabBarIcon: ({ tintColor, focused }) => (
<Ionicons
name={focused ? 'ios-person' : 'ios-person'} //TODO change to focused icon
size={26}
style={{ color: tintColor }}
/>
),
}
},
A you can see the props can passed on as ScreenProps and then read in the child component (screen) like this:
export default class ProfileScreen extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: true,
email: '',
username: '',
phone: '',
password: '',
name: '',
error: ''
};
}
componentDidMount() {
const headers = {
Authorization: this.props.jwt
};
api.get('/user')
.then((response) => {
this.setState({
email: response.data.data.attributes.email,
phone: response.data.data.attributes.phone,
username: response.data.data.attributes.username,
name: response.data.data.attributes.name,
loading: false
});
}).catch((error) => {
this.setState({
error: 'Error retrieving data',
loading: false
});
});
}
I suppose this would be far simpler with Redux but I'm still learning that.
May i ask that did you include login screen for authentication?

Can I loop componentWillMount until I get the user_key from API?

I am trying to use react navigation authentication flow to manage the login screen if the user is logged in or not. But now I got stuck in AsyncStorage. So while the user is not logged in I presume that componentWillMount will wait until the user will input the credentials, tap the login button, receive the user_id from API call and then try again. For me now it is calling what in the beginning which is fine but then I have to exit from app and go back to get the dashboard rendered. Any solution?
This is my code from App.js where I'm creating the routes as well. Also I am loading redux map on bottom.
export const createRootNavigator = (signedIn = false) => {
return SwitchNavigator(
{
SignedIn: {
screen: SignedIn
},
SignedOut: {
screen: SignedOut
}
},
{
initialRouteName: signedIn ? "SignedIn" : "SignedOut"
}
);
};
class App extends Component {
constructor(props) {
super(props);
this.state = {
signedIn: false,
checkedSignIn: false
};
}
async componentWillMount() {
await isSignedIn()
.then(res => this.setState({ signedIn: res, checkedSignIn: true }))
.catch(err => alert("An error occurred"));
}
render() {
const { checkedSignIn, signedIn } = this.state;
// If we haven't checked AsyncStorage yet, don't render anything (better ways to do this)
if (!checkedSignIn) {
return null;
}
const Layout = createRootNavigator(signedIn);
return (
<SafeAreaView style={styles.safeArea}>
<View style={{flex: 1, backgroundColor: '#ffffff'}}>
<StatusBar barStyle="light-content"/>
<Layout />
<AlertContainer/>
</View>
</SafeAreaView>
)
}
};
And here is the Auth.js where I am waiting for the user_key.
export let USER_KEY = 'myKey';
export const onSignIn = async () => { await AsyncStorage.setItem(USER_KEY, 'true') };
export const onSignOut = async () => { await AsyncStorage.removeItem(USER_KEY) };
export const isSignedIn = () => {
return new Promise((resolve, reject) => {
AsyncStorage.getItem(USER_KEY)
.then(res => {
if (res !== null) {
// console.log('true')
resolve(true);
} else {
resolve(false);
// console.log('false')
}
})
.catch(err => reject(err));
});
};
A solution would be to make use of Splashscreen. You can add a splashscreen to the App. While Splashscreen is being displayed, check if user exists in Asyncstorage, if they do, navigate user to the Dashboard/Homescreen and if asynstorage responds null, navigate user to the Login page. Once Navigation is complete, you can hide the splashscreen. Checkout this package in npmjs for Splashscreen setup react-native-splash-screen

React native navigation class function in header

I've got a problem with react native navigation and nested navigators.
Basically, the nested navigators (tab in a page) work pretty well. But when i add a button in the header with the _saveDetails function, it throw me an undefined function if i'm in the Players tab, and it works well when i'm on the Teams tab
Does anyone have an idea of what am i doing wrong? Thanks.
class HomeScreen extends React.Component {
static navigationOptions = ({ navigation }) => {
const { params = {} } = navigation.state;
return {
headerRight: <Button title="Save" onPress={() =>
params.handleSave()} />
};
};
_saveDetails() {
console.log('clicked save');
}
componentDidMount() {
this.props.navigation.setParams({ handleSave: this._saveDetails });
}
render() {
return (
<View />
);
}
}
const MainScreenNavigator = TabNavigator({
Players: { screen: HomeScreen},
Teams: { screen: HomeScreen},
});
const SimpleApp = StackNavigator({
Home: { screen: MainScreenNavigator },
Player: { screen: PlayerPage },
});
Please try this one and let me know if you fetch any other problem. Just convert your code according to below code.
static navigationOptions = {
header: (navigation, header) => ({
...header,
right: (
navigation.navigate('Settings')}>
Settings
)
})
}
Thanks
Just change your code become like this
class HomeScreen extends React.Component {
static navigationOptions = ({ navigation }) => {
// const { params = {} } = navigation.state; //delete this
return {
headerRight: <Button title="Save" onPress={() =>
navigation.state.params.handleSave()} /> // add navigation.state
};
};
.....

How to pass redux store in react native between pages?

In react native app I have 2 pages. If I upload a redux store with data on the 2 page, then return to the 1 page - how can I access the store with the uploaded data from the 2 page? So is there a way to access the store with data from all of the pages in react native?
Maybe simoke example or where to read?
Thanks
1page.js
class ScreenHome extends Component{
static navigationOptions = {
title: 'ScreenHome',
};
constructor(props){
super(props)
console.log("PROPS: ",props);
}
render() {
const { navigate } = this.props.navigation;
return (
<View>
<Button
title="Go to load data page"
onPress={() => navigate('New', { name: 'Jane' })}
/>
<Button
title="Get redux data"
onPress={() => {console.log(this.props)}}
/>
</View>
);
}
}
class ScreenRegister extends Component{
static navigationOptions = {
title: 'ScreenRegister',
};
render(){
return <Text>ScreenRegister</Text>
}
}
const MainScreenNavigator = DrawerNavigator({
Recent: {
screen: ScreenHome
},
All: {
screen: ScreenRegister
},
});
export default SimpleApp = StackNavigator({
Home: {
screen: MainScreenNavigator
},
Chat: {
screen: ScreenHome
},
New: {
screen: testScreen
}
});
const mapStateToProps = (state) => {
const {items, isFetching, done} = state.myTestData
return {testScreen:{items, isFetching, done}};
}
const mapDispatchToProps = (dispatch) => {
return {
getNewItems: () => {
dispatch(fetchData());
}
}
}
export default someTest = connect(
mapStateToProps,
mapDispatchToProps
)(SimpleApp)
2page.js
class testScreen extends Component{
static navigationOptions = {
title: 'testScreen.js',
};
_reduxStuff = () => {
this.props.getNewItems();
}
render() {
const { navigate } = this.props.navigation;
const {done, items, isFetching} = this.props.testScreen;
return (
<View>
<Text>Some new screen</Text>
<Button
title="Load Data"
onPress={() => this._reduxStuff()}
/>
</View>
);
}
}
const mapStateToProps = (state) => {
const {items, isFetching, done} = state.myTestData
return {testScreen:{items, isFetching, done}};
}
const mapDispatchToProps = (dispatch) => {
return {
getNewItems: () => {
dispatch(fetchData());
}
}
}
export default FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(testScreen)
There should be a container for each page, a store for data you want to access between pages and actions to changing this store. By using mapStateToProps you can pass this store to the container of the page. You can find good example in here.
On your first container you'll need to make your async calls to fill your store.
You can do a dispatch on your componentWillMount() and populate your store with the received data.