How can I change a parent state through a react navigation component? - react-native

I'm new to React Native and having trouble figuring out how to accomplish this. Currently I have an app structure something like this:
App.js -> Authentication.js -> if(state.isAuthenticated) Homepage.js, else Login.js
I'm currently changing the isAuthenticated state on a logout button on the homepage. I'm now trying to add in a drawer navigator to the app, which would get returned to the authentication page in place of the homepage. So I'm not sure how to pass the state change through the drawernavigator component to the Authentication page.
Currently my Homepage has a button that has:
onPress={() => this.props.logout()}
And the authentication page has:
export default class Authentication extends Component {
constructor(props){
super(props);
this.state = {
isAuthenticated: false,
isLoading: false
}
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
}
login() {
AsyncStorage.setItem("user", JSON.stringify({email: this.state.email, password: this.state.password}))
.then(results => {
this.setState({isAuthenticated: true});
});
}
logout() {
AsyncStorage.clear()
.then(result => {
this.setState({isAuthenticated: false});
});
}
componentDidMount(){
this.setState({isLoading: true});
AsyncStorage.getItem("user")
.then(results => {
const data = JSON.parse(results);
if (data) {
this.setState({isAuthenticated: true});
}
this.setState({isLoading: false});
});
}
render() {
if (this.state.isLoading){
return(
<Splashpage />
);
}
if (!this.state.isAuthenticated){
return (
<Login login={this.login}/>
);
}
return (
<Homepage logout={this.logout}/>
);
}
}
So I made a Navigation.js page where I'm creating a drawernavigator and going to be returning this instead of the Homepage.
export default Navigation = createDrawerNavigator({
Home: {
screen: Homepage,
},
WebView: {
screen: WebView,
},
});
But I'm not sure how to pass along the state change from the homepage, through the Navigation component to the parent Authentication page. Any help would be much appreciated.

You could pass a callback through navigate:
this.props.navigation.navigate('yourTarget',{callback:function});
In yourTraget you can access it via:
this.props.navigation.state.params.callback(isAuthenticated)
Here the documentation: https://reactnavigation.org/docs/en/params.html
I hope this is what you were looking for! Oh now I see you asked that already a while ago. Maybe you already moved on...

Related

How to refresh data when navigating to the previous page in React Native?

What I'm Trying To Do
When navigating back from second page to first page, I want to refresh datas.
The problem is that when I navigate back from second page, onRefresh() in First doesn't work.
First Page
export default class First extends React.Component {
constructor(props) {
super(props);
this.state = {
refreshing: false,
items: [],
};
}
fetchData = async () => {
const querySnapshot = db.items();
const items = [];
querySnapshot.forEach((doc) => {
items.push(doc.data());
});
this.setState({ items });
}
componentDidMount() {
this.fetchData();
}
componentDidUpdate(prevProps) {
this.onRefresh;
}
onRefresh() {
this.setState({ refreshing: true });
this.fetchData().then(() => {
this.setState({ refreshing: false });
});
}
render() {
return (
<Container>
<Content
refreshControl={(
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.onRefresh.bind(this)}
/>
)}
>
</Content>
</Container>
);
}
}
Second Page
this.props.navigation.navigate('First', 'update');
I would appreciate it if you could give me any advices.
Suppose you have two pages PAGE-A & PAGE-B
create a JS-File named ForegroundBackground.js
import React from "react";
import { View } from "react-native";
const ForegroundBackground = ({ navigation, bgCallback, fgCallback }) => {
React.useEffect(() => navigation.addListener('focus', () => {
fgCallback && fgCallback()
}), []);
React.useEffect(() => navigation.addListener('blur', () => {
bgCallback && bgCallback()
}), []);
return (<View/>);
};
export default ForegroundBackground;
you can use it in render function of PAGE-A by providing navigation object to it from screen props.
<ForegroundBackground navigation = {this.props.navigation}
fgCallback = {()=>{alert("resume")}}/>
There are two solutions for the purpose.
To use store.Define a state in store and use that state in pageA and pageB.So if you change store state from pageB.It will auto reflect in entire app.
you can pass a function from pageA to pageB while navigation.The purpose of the function is to refresh state of PageA while moving back. For example:
this.props.navigation.navigate("pageB", {
resetData: () => {
this.setState({
mydata: ""
})
}
})
And while navigating from pageB you can do something like this:
this.props.navigation.state.params.resetData();
this.props.navigation.goBack();
I hope it helps. Leave a comment if you want to have more help/code/discussion etc

How Redirect to Login if page is protected and the user is not signed in?

In my App I have some public screens that are accessible even if the user is not logged in, and some screens are protected (you must be logged in to access them).
My solution to the problem is to check the component willFocus Listener and if not logged in, the user should be redirected to the loginPage.
export async function ProtectRoute(navigation){
//if page will enter and the user is not authenticated return to login
navigation.addListener(
'willFocus',
async () => {
let token = await getTokenAsync();
if(!token){
navigation.navigate('Login');
}
})
}
In my screen I Call this function in ComponentWillMount lifecycle.
The issue is that it takes like a second to verify the token and the page is displayed briefly.
How can I make it so that he goes directly to the Login Page without that lag ?
I wrote a quick example below. You can examine and use it.
import React, { Component } from "react";
import { Text, View } from "react-native";
const withAuth = WrappedComponent => {
class AuthenticationScreen extends Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false
};
props.navigation.addListener("willFocus", async () => {
await this.checkAuth();
});
}
remoteReuqest = async () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 2000);
});
};
checkAuth = async () => {
const result = await this.remoteReuqest();
if (result) {
this.setState({
isAuthenticated: true
});
} else {
this.props.navigation.navigate("Login");
}
};
render() {
if (!this.state.isAuthenticated) {
return <Text>Waiting...</Text>;
}
return <WrappedComponent {...this.props} />;
}
}
return AuthenticationScreen;
};
export default withAuth;
You can use it as follows.
import React, { Component } from "react";
import { Text, StyleSheet, View } from "react-native";
import withAuth from "./withAuth";
class ContactScreen extends Component {
render() {
return (
<View>
<Text> Contact Screen </Text>
</View>
);
}
}
const styles = StyleSheet.create({});
const extendedComponent = withAuth(ContactScreen);
extendedComponent.navigationOptions = {
title: "Contact"
};
export default extendedComponent;
The issue is that it takes like a second to verify the token and the page is displayed briefly.
The reason is because reading/writing from/to AsyncStorage is an asychronous operation.
In my screen I Call this function in ComponentWillMount lifecycle.
I suggest you to not use ComponentWillMount lifecycle because it's deprecated and it will be removed from React (https://reactjs.org/docs/react-component.html#unsafe_componentwillmount)
After this introduction, now i show you how I have achieved this in my app: CONTEXT API! (https://reactjs.org/docs/context.html)
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
How to implement context api:
the context will be the 'state' of your App.js file. You root App.js will be the provider of the context, while other views which will need the context are called the consumers of the context.
First of all, you need to create a 'skeleton' of your context into a separate file, something like this:
// AuthContext.js
import React from 'react'
const AuthContext = React.createContext({
isLogged: false,
login: () => {},
logout: () => {}
})
export default AuthContext
Your App.js will import, contain and initialize the context:
// App.js
// all necessary imports
import AuthContext from '....'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
isAuth: false,
login: this.login,
logout: this.logout
}
login = async userData => {
// do your login stuff and then
this.setState({ isAuth: true })
}
logout = async () => {
// do your logout stuff and then
this.setState({ isAuth: false })
}
async ComponentDidMount () {
// check the asyncStorage here and then, if logged:
this.setState({ isAuth: true })
}
render () {
return (
<AuthContext.Provider value={this.state}>
<AppContainer />
</AuthContext.Provider>
)
}
Then, into the View contained into AppContainer, you could access context like this:
import AuthContext from '.....'
// all necessary imports
export default class YourView extends React.Component<Props, State> {
constructor (props) {
super(props)
this.props = props
this.state = { ... }
}
// THIS IS IMPORTANT
static contextType = AuthContext
// with this, you can access the context through 'this.context'
ComponentDidMount () {
if (!this.context.isAuth) this.props.navigation.navigate('login')
}
Advantages of this approach:
Checking a boolean is so fast that you will not notice a blank screen.
Sharing an Authentication Context everywhere in you app
Making the access to asyncstorage only the first time that app mounts and not everytime you need to check if the user is logged
Sharing methods to login/logout everywhere in your app

React Native - How to update screen on an already mounted component

I have 2 screens:
Home Screen
Login Screen
My entry point is my Home screen, if I am logged in (fetching datas), log out and then log in to another account. Since my Home component is already mounted, my data will not be updated and this.getDatas() will not be called again. I can't find solutions, I think I need to use "ComponentWillUpdate" in my Home.js to call this.getDatas() or something like that but you can't find the solution.
Here's my code:
Home.js
componentDidMount = async () => {
const { navigation } = this.props;
if (!await AsyncStorage.getItem('auth')) {
navigation.navigate('Login');
} else {
this.getDatas();
}
}
getDatas() {
axios.get(`${apiUrl}/me`).then((res) => {
this.setState({
datas: res.data.datas,
});
}).catch((err) => {
console.warn(err);
});
}
Login.js
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: null,
password: null,
};
}
logIn() {
const { email, password } = this.state;
axios.post(`${apiUrl}/auth`, {
email,
password
}).then((res) => {
navigation.navigate('Home');
}).catch((err) => {
console.warn('Error:', err);
});
}
}
App.js
const MainNavigator = createStackNavigator({
Home,
CreateGame,
GamePreview,
CardSelection,
BonusCard,
Game,
GameResult
}, { initialRouteName: 'Home', headerMode: 'none', });
const LoginNavigator = createStackNavigator({
Login,
Subscribe,
SubscribeNext,
ForgottenPassword,
}, { initialRouteName: 'Login', headerMode: 'none', });
const RootNavigator = createSwitchNavigator({
AuthLoading: AuthLoadingScreen,
LoginNavigator,
MainNavigator
}, { initialRouteName: 'AuthLoading' });
export default class App extends Component {
render() {
return <RootNavigator />;
}
}
Any idea ?
You should consider using Switch Navigator from react-navigation for your authentication flows in your application. So once you get logged out of your app and lands on login, the application screen pops out of the memory and thus when you login back, the componentDidmount will again be triggered.

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

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