React-Navigation passing functions to screenProps - react-native

I am creating an app using React Native, Expo and React Navigation, and I don't know the correct method for passing props and functions around.
I have copied the Expo method for navigators that I will build on, but right now I just have App.js calling the following
AppNavigator -> MainSwitchNavigator -> HomeScreen
I have then wrapped the main exported App that Expo expects with the Amazon AWS Amplify HOC withAuthenticator. Now I can log in to my app and show hello world securely using Amazon's cognito service.
If I want to log out I can use the signout function bellow that I currently have on the props of App.
class App extends React.Component {
constructor(props) {
super(props);
this.signOut = this.signOut.bind(this);
}
signOut() {
Auth.signOut().then(() => {
this.props.onStateChange('signedOut', null);
console.log("signed out");
}).catch(e => {
console.log(e);
});
}
render () {
return (
<View style={styles.container}>
<AppNavigator screenProps={{
signOut: () => {this.signOut}
}}/>
</View>
);
}
}
export default withAuthenticator(App)
For now I just want to pass this function down to my Home Screen so I can add a button and log out.
AppNavigator
export default createAppContainer(
createSwitchNavigator({
// You could add another route here for authentication.
// Read more at https://reactnavigation.org/docs/en/auth-flow.html
Main: { screen: MainSwitchNavigator, params: { signOut: this.props.screenProps.signOut } },
},
{
initialRouteName: 'Main'
})
);
MainSwitchNavigator
export default createSwitchNavigator({
// Home: { screen: HomeScreen }
Home: { screen: HomeScreen, params: { signOut: this.props.navigation.params.signOut } }
},{
initialRouteName: 'Home'
});
HomeScreen
class HomeScreen extends Component {
render () {
return (
<View>
<Text>Main App Here</Text>
<Button
onPress={this.props.navigation.params.signOut()}
title="Sign Out"
/>
</View>
)};
}
export default HomeScreen;
At the moment I get the error
undefined is not an object (evaluating 'this.props.navigation')
<unknown>
MainSwitchNavigator.js
8:62
Which puts its at the point I'm trying to read the props passed in to MainSwitchNavigator.
So my question is, what is good practice on sharing functions and state with screens below the main App and how do I pass the signOut function down to the rest of my components?
=======================================================================
EDIT
I have since worked out how to use screenProps correctly for variables but not for functions. screenProps is passed down through navigators automatically to the screen. So I only have to pass it to the AppNavigator component once and I can access it in HomeScreen. However any functions are not passed down.
E.g for variables, if I modify App.js to pass text to the variable signOut and pass that to screenProps
class App extends React.Component {
constructor(props) {
super(props);
this.signOut = this.signOut.bind(this);
}
signOut() {
Auth.signOut().then(() => {
this.props.onStateChange('signedOut', null);
console.log("signed out");
}).catch(e => {
console.log(e);
});
}
render () {
return (
<View style={styles.container}>
<AppNavigator screenProps={{signOut: "some text"}}/>
</View>
);
}
}
HomeScreen will then show this as part of the this.props object.
class HomeScreen extends Component {
render () {
return (
<View>
<Text>Main App Here</Text>
<Button
onPress={console.log(JSON.stringify(this.props))}
// onPress={alert(this.props.screenProps.var3)}
title="Sign Out"
/>
</View>
)};
}
export default HomeScreen;
However if I pass a function to AppNavigator instead and do this
<AppNavigator screenProps={{signOut: () => {this.signOut}}}/>
screenProps does not get set and I can't access the function signOut.
The only thing I can get to work is this
<AppNavigator screenProps={this.signOut}/>
But what I need is to create a property of screenProps and pass this down. Any thoughts?

passing a function as a screenProp should be
<AppNavigator screenProps={{signOut: this.signOut}} />
then to use do
this.props.screenProps.signOut()

MainSwitchNavigator
export default createSwitchNavigator({
// Home: { screen: HomeScreen }
Home: { screen: HomeScreen }
},{
initialRouteName: 'Home'
});
to send data on navigate
this.props.navigation.navigate('Signout' , { name: 'John Doe', email: 'john#doe.com' })

Related

react navigation how to go back to different stack

const HomeStack = createStackNavigator({
Home: HomeScreen,
});
const CarStack = createStackNavigator({
Booking: BookingScreen,
Confirm: ConfirmScreen
});
export default createBottomTabNavigator({
HomeStack,
CarStack,
});
export default class HomeScreen extends React.Component {
goTo(){
this.props.navigation.navigate('Confirm')
}
render() {
return (
<View style={css.container}>
<button onPress={this.goTo.bind(this)}>go to</button>
</View>
);
}
}
export default class ConfirmScreen extends React.Component {
goBack(){
this.props.navigation.goBack()
}
render() {
return (
<View style={css.container}>
<button onPress={this.goBack.bind(this)}>goback</button>
</View>
);
}
}
In home screen, when I click goto, it take me to confirmscreen, which is good.
But when I press go back, it takes me to booking screen instead of home screen. Which is bad.
It suppose to take me to home screen. What am i missing here?
You have to define 2 separate navigators and check user/guest in another component. like this:
Provider.js:
export default class Provider extends Component {
render() {
let {key, signedIn, checkedSignIn} = this.state
return(
<View>
{
checkedSignIn
? signedIn
? <App />
: <SignedOut />
: null
}
</View>
)
}
}
App.js:
export default createStackNavigator({
home: Home // sample
})
SignedOut.js:
export default createStackNavigator({
auth: Auth // sample
})
If you are using react-navigation v2.x or greater, directly use the navigate method to go to your Home screen.
Customize the back button behaviour and call this.props.navigation.navigate('Home') on the back button press in Confirmation screen. This will cause the desired effect.
Since react-navigation v2.0, the navigate action is less pushy. If the route you specify is already in the stack, then that screen will be opened, instead of pushing a new screen in stack by default.
Reference
Try this:
this.props.navigtion.goBack('Home')
I hope it help you.

How to route my login screen in react native?

I want to add a login screen right after my splash screen, and then go to my home screen.
const LoginStack = createStackNavigator({
Login: LoginScreen,
Home: HomeStack,
});
The problem is that my LoginStack is never used even if I call the home screen in my login screen :
<Button onPress = {this.onPressHome}/>
onPressHome = () => {
this.props.navigation.navigate('Home');
};
My App.js
render() {
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={this._handleLoadingError}
onFinish={this._handleFinishLoading}
/>
);
I'm assuming that you're using React Navigation for the routing. You can pass a 2nd argument to the createStackNavigator which is the config for the Stack.
if you pass an object to the stack you can define which screen you want. So your code would look like:
const LoginStack = createStackNavigator({
Login: LoginScreen,
Home: HomeStack,
},
{
initialRouteName: 'Login',
});
And then if you use the button as you're using now it should work just fine.
So in your case you need to change your App.js to look something like this:
export default App extends Component {
componentDidMount = () => {
this.setState({
isLoadingComplete: true
})
}
render() {
// This is the loading screen?
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
startAsync={this._loadResourcesAsync}
onError={this._handleLoadingError}
onFinish={this._handleFinishLoading}
/>
) else {
return <LoginStack />
}
Once your LoadingIsComplete becomes true it would render the LoginStack

In a React-Native app, how to use SwitchNavigator (react-navigator) and react-redux?

I'm using react-navigation's SwitchNavigator to manage my navigation based on authentication state. When Authenticated, I want to use Redux to store data I'm fetching.
My SwitchNavigator looks like this
SwitchRouteConfig = {
AuthLoading: AuthLoadingScreen,
Authenticated: AppStackNavigator,
NotAuthenticated: AuthStackNavigator,
}
SwitchConfig = {
initialRouteName: 'AuthLoading',
}
export default createSwitchNavigator(SwitchRouteConfig, SwitchConfig);
My Authenticated navigation looks like this:
// App Bottom Tab Navigator
const AppTabRouteConfig = {
AddItem: { screen: AddItem },
Items: { screen: Items },
Map: { screen: Map },
Help: { screen: Help },
}
const AppTabConfig = { initialRouteName: 'Items',}
const AppTabNavigator = new createBottomTabNavigator(
AppTabRouteConfig,
AppTabConfig)
And in my Screen we have:
class Items extends React.Component {
constructor(props){
super(props);
this.state = {};
}
componentDidMount(){
this.props.getData(); //call our redux action
}
render() {
if(this.props.isLoading){
return(
<View>
<ActivityIndicator />
</View>
)
} else {
return (
<Provider store={store}>
<SafeAreaView>
<FlatList
data={this.props.dataSource.features}
renderItem={({ item }) =>
<TouchableWithoutFeedback>
<View style={styles.listContainer}>
<Text>{item.prop1}</Text>
<Text>{item.prop2}</Text>
</View>
</TouchableWithoutFeedback>
}
/>
</SafeAreaView>
</Provider>
)
}
}
}
function mapStateToProps(state, props) {
return {
isLoading: state.dataReducer.isLoading,
dataSource: state.dataReducer.dataSource
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(Actions, dispatch);
}
export default connect(mapStateToProps,
mapDispatchToProps)(Items)
When I'm not authenticated, that works fine. I can login. When I am authenticated, I get the following error:
Invariant Violation: Could not find "store"
in either the context or props of
Connect(Items)". Either wrap the root
component in a <Provider>, or explicitly pass
"store" as a prop to "Connect(Items)".
In the reading I've done today, all the samples have a single top-level component which they wrap with . So, I'm not understanding how you instantiate the store and manage Redux without that model.
I should mention two additional things:
The initial authenticated app screen worked fine before I started to implement Redux.
I'm not trying to manage the state with Redux, just application data.
Project started with Create-React-Native-App.
Thank you!
You have to wrap the root component (the switch navigator) inside Provider to make the store available to all container components.
const SwitchRouteConfig = {
AuthLoading: AuthLoadingScreen,
Authenticated: AppStackNavigator,
NotAuthenticated: AuthStackNavigator,
}
const SwitchConfig = {
initialRouteName: 'AuthLoading',
}
const Navigator = createSwitchNavigator(SwitchRouteConfig, SwitchConfig);
// You can use a stateless component here if you want
export default class Root extends React.PureComponent {
render() {
return (
<Provider store={store}>
<Navigator />
</Provider >
);
}
}

Use a method in another class and navigation

I am coding an application that is neccessary to login users and I am experiencing a problem.
I have in my App component a getUser () method that I would like to call from other components. I also have a Login () method that allows you to navigate to another page. Only the problem is that I can not export the StackNavigator and App at the same time. Would there be a way to export the StackNavigator and my function? Or an other solution that would solve my problem?
class App extends Component<{}> {
//get the user and return it
_getUser() {
return user
}
//authentify the user and go to is profil
_userLogin() {
navigate('Profil');
}
render() {
const { navigate } = this.props.navigation;
return (
<View>
<Button
title="Login"
onPress={() => this.Login() }
/>
</View>
);
};
}
export default StackNavigator({
Home: { screen: App },
Profil: { screen: secondScreen },
});
Thanks for your help.
Finally i found a solution, I ll store the user in an asynchron storage.

Where to initialize data loading with react-navigation

I'm using react-navigation and here is my structure :
The root stack navigator :
export const Root = StackNavigator({
Index: {
screen: Index,
navigationOptions: ({ navigation }) => ({
}),
},
Cart: {
screen: Cart,
navigationOptions: ({ navigation }) => ({
title: 'Votre panier',
drawerLabel: 'Cart',
drawerIcon: ({ tintColor }) => <Icon theme={{ iconFamily: 'FontAwesome' }} size={26} name="shopping-basket" color={tintColor} />
}),
},
...
My structure looks like this :
StackNavigator (Root)
DrawerNavigator (Index)
TabNavigator
MyPage
MyPage (same page formatted with different datas)
...
So my question is, where do I load my data, initialize my application ? I need somewhere called once, called before the others pages.
The first page displayed in my application is the MyPage page. But as you can see, because of the TabNavigator, if I put my functions inside, it will be called many times.
Some will says in the splashscreen, but I'm using the main splashscreen component and I don't have many controls over it.
I thought about my App.js where we create the provider, but I don't think this is a good idea ?
const MyApp = () => {
//TODO We're loading the data here, I don't know if it's the good decision
ApplicationManager.loadData(store);
SplashScreen.hide();
return (
<Provider store={store}>
<Root/>
</Provider>
);
};
What is the good way to do it ?
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return <SplashScreen />
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
TabNavigator by default renders/loads all its child components at the same time, but if you set property lazy: true components will render only if you navigate. Which means your functions will not be called many times.
const Tabs = TabNavigator(
{
MyPage : {
screen: MyPage
},
MyPage2 : {
screen: MyPage,
}
}
},
{
lazy: true
}
);
If you use this structure and call fetching data inside of MyPage you can add logic in componentWillReceiveProps that will check is data already in store and/or is it changed before fetching new data. Calling your fetch functions from MyPage gives you the ability to pull fresh data on every page/screen visit or do "pull to refresh" if you need one.
You could also pull initial data in splashscreen time, I would just not recommend pulling all your app data, data for all screens, at that time since you probably don't need it all at once. You can do something like:
class MyApp extends Component {
state = {
initialized: false
}
componentWillMount() {
// if this is a promise, otherwise pass a callback to call when it's done
ApplicationManager.loadData(store).then(() => {
this.setState({ initialized: true })
})
}
render() {
const { initialized } = this.state
if (!initialized) {
return null
}
return (
<Provider store={store} >
<Root />
</Provider>
);
}
}
class Root extends Component {
componentDidMount() {
SplashScreen.hide();
}
...
}
You should do it in App.js or where you initialize your StackNavigator. If I were you, I would put a loading screen, which would get replaced by the StackNavigator structure once the data is ready.
I wouldn't do it in the App because you lose control. Sadly I haven't used react-navigation or redux but I see that the TabNavigator has a tabBarOnPress method, which I would use to trigger the loading. You can load every page data on demand.
https://reactnavigation.org/docs/navigators/tab#tabBarOnPress