React Native Context, how to share context beetwing multiple nested files and components - react-native

I'm quiete new to react native, and im stuck on passing context between components in different files
basically im building login flow following the react navigation auth-flow https://reactnavigation.org/docs/auth-flow/
my scenario looks as follow:
in App.js
a stack screen with Login/Register/Home, showing Login/Register or Home based on login status
the Home Screen is made by a component which is a drawer, using a custom drawer and two component (Home and About)
//VARIOUS IMPORT
const Drawer = createDrawerNavigator();
const HeaderOption = () => ({
headerShown: false,
// animationTypeForReplace: state.isSignout ? 'pop' : 'push',
});
const AppStack = createStackNavigator();
const AuthContext = createContext();
//THE DRAWER FOR HOME
function DrawerNavigator(props) {
return (
<Drawer.Navigator
initialRouteName="Home"
drawerContent={(props) => MyDrawer(props)}
>
<Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="About" component={About} />
</Drawer.Navigator>
);
}
//MAIN APP
export default function App({ navigation }) {
const [state, dispatch] = useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
userToken = await AsyncStorage.getItem('userToken');
} catch (e) {
}
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = useMemo(
() => ({
signIn: async (data) => {
// LOGIN PROCEDURE
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
// SUBSCRIBE PROCEDURE
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
if (state.isLoading) {
// We haven't finished checking for the token yet
return (
<View>
<Text>Loading</Text>
</View>
);
}
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<AppStack.Navigator initialRouteName="Login">
{state.userToken == null ? (
<>
<AppStack.Screen
name="Login"
component={LoginScreen}
options={HeaderOption}
/>
<AppStack.Screen
name="Register"
component={RegisterScreen}
options={HeaderOption}
/>
</>
) : (
<AppStack.Screen
name="HomeApp"
component={DrawerNavigator}
options={HeaderOption}
/>
)}
</AppStack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
in LoginScreen.js
the effective login screen (which is showed at app startup if not logged in)
//import
export default function LoginScreen(props) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { signIn } = useContext(AuthContext);
return (
<View
style={{
flex: 1,
backgroundColor: Constants.MAIN_GREEN,
}}
>
<View style={{ ...styles.container }}>
<StatusBar hidden={true} />
<View style={{ ...styles.logoContainer }}>
<Image
style={styles.logoIcon}
source={require('../assets/logo_popeating_amp.png')}
/>
</View>
<View style={{ ...styles.inputContainer }}>
<Image
style={styles.inputIcon}
source={require('../assets/mail.png')}
/>
<TextInput
autoFocus={true}
placeholder="Email address"
onChangeText={(email) => setEmail(email)}
value={email}
label="Email"
style={styles.inputs}
keyboardType={'email-address'}
/>
</View>
<View style={{ ...styles.inputContainer }}>
<Image
style={styles.inputIcon}
source={require('../assets/password.png')}
/>
<TextInput
placeholder="Password"
onChangeText={(password) => setPassword(password)}
value={password}
secureTextEntry={true}
label="Password"
style={styles.inputs}
/>
</View>
<TouchableHighlight
style={[styles.buttonContainer, styles.loginButton]}
onPress={() => signIn({ email, password })}
underlayColor={Constants.HI_COLOR}
>
<Text style={styles.loginText}>LOGIN</Text>
</TouchableHighlight>
<TouchableHighlight
style={styles.buttonContainer}
onPress={() => props.navigation.navigate('HomeApp')}
underlayColor={Constants.HI_COLOR}
>
<Text>Forgot your password?</Text>
</TouchableHighlight>
<TouchableHighlight
style={styles.buttonContainer}
onPress={() => props.navigation.navigate('Register')}
underlayColor={Constants.HI_COLOR}
>
<Text>Register</Text>
</TouchableHighlight>
</View>
</View>
);
}
const styles = StyleSheet.create({
//styles
});
in DrawerContent.js
the drawer for the home which contain a link to Home, a link to About, a link to Logout
in Home.js
the main page which is the initialroute of the Drawer
every time i try to start the app
the error is
Unhandled promise rejection: ReferenceError: Can't find variable: AuthContext
it seems LoginScreen cant access AuthContext, how can i have AuthContext available to other components between files?

You can place the context creation in a separate file
//AuthContext.js
const AuthContext = createContext();
export default AuthContext;
In app.js you can simply import this and use this
import AuthContext from './AuthContext.js';
You can do the same for login.js as well
Then it will work as expected.

Related

Warning: Cannot update a component (`ForwardRef(BaseNavigationContainer)`) while rendering a different component (`Context.consumer`)

here my context file
import React from 'react';
import storage from '#helper/storage/user-storage';
export const AuthContext = React.createContext();
class AuthContextProvider extends React.Component{
state = {
user: null,
nigth_mode: false,
token: null
}
login = async ({user, token}) => {
await storage.storeUser(user)
await storage.storeToken(token)
this.setState({
user: user,
token: token
})
}
logout = async () => {
storage.removeAll().then(() => {
this.setState({
user: null,
token: null
})
});
}
setUser = async (data) => {
this.setState({
user: data
})
}
nigth_mode = async () => {
this.setState({
nigth_mode: !nigth_mode
})
}
render(){
return(
<AuthContext.Provider
value={{
...this.state,
login: this.login,
logout: this.logout,
setUser: this.setUser,
nigth_mode: this.nigth_mode,
token: this.state.token
}}
>
{ this.props.children }
</AuthContext.Provider>
);
}
}
export default AuthContextProvider
in my login file i have this render
render(){
return(
<AuthContext.Consumer>
{context => {
console.log('token', context)
if(context.token){
// this.props.navigation.reset({
// index: 0,
// routes: [{ name: 'Home' }],
// });
this.props.navigation.navigate('View')
}else{
return(
<LinearGradient
colors={['#232526', '#182848']}
style={styles.mainContainerLogin}>
<KeyboardAvoidingView
style={styles.mainContainerLogin}
behavior={'height'}
>
{/* <View style={styles.loginLogo}>
<Image
source={require('../Images/ic-logo.jpg')}
style={styles.logo}
/>
</View> */}
<View style={styles.inputContainer}>
<Ionicon
name="md-person"
size={20}
color="rgb(110,182,245)"
style={styles.inputIcon}
/>
<TextInput
placeholder={'Email'}
placeholderTextColor={'rgba(255,255,255,0.5)'}
underlineColorAndroid="transparent"
style={styles.loginInput}
onChangeText={value => (this.userref = value)}
/>
</View>
<View style={styles.inputContainer}>
<Ionicon
name="md-lock-closed"
size={20}
color="rgb(110,182,245)"
style={styles.inputIcon}
/>
<TextInput
placeholder="Password"
placeholderTextColor={'rgba(255,255,255,0.5)'}
underlineColorAndroid="transparent"
secureTextEntry={this.state.keyHide}
style={styles.loginInput}
onChangeText={value => (this.password = value)}
/>
<TouchableOpacity
style={styles.btnEye}
onPress={this._showPassword.bind(this)}>
<Ionicon
name={
this.state.press == false ? 'md-eye' : 'md-eye-off'
}
size={20}
color="rgb(110,182,245)"
/>
</TouchableOpacity>
</View>
{this._displayLoading()}
{_errorTextLogin(this.state.error)}
</KeyboardAvoidingView>
</LinearGradient>
);
}
}}
</AuthContext.Consumer>
);
so on clicking login button, the context.login is called, so it's set the state of token, so in my condition if my context.token exist i navigate to another page; if i console log on context.token, i got eh token when login is success,then the navigation is processed successfuly but i got a error popup on the new page
Warning: Cannot update a component (`ForwardRef(BaseNavigationContainer)`) while rendering a different component (`Context.consumer`)

Getting LogOut everytime React Native Expo App refreshes?

I am new to React Native and using Expo and trying to login using AuthContext and AsyncStorage, after login it takes me to HomeScreen . But when I make changes in components, it refreshes the app and LogOut me from the app. And everytime I make some changes, I need to login again.
Can anyone help me where I am going wrong. I am using AsyncStorage for now for offline support and reducers.
App.js
<AuthContext.Provider value={authContext}>
<NavigationContainer>
{ loginState.userToken != null ? (
<Drawer.Navigator>
<Drawer.Screen name="Home" component={PreNav}/>
<Drawer.Screen name="Support" component={SupportScreen}/>
<Drawer.Screen name="Settings" component={SettingsScreen}></Drawer.Screen>
</Drawer.Navigator>
): <RootStackScreen />
}
</NavigationContainer>
</AuthContext.Provider>
const initialLoginState = {
isLoading: true,
userName: null,
userToken: null,
}
const loginReducer = (prevState, action) => {
switch(action.type) {
case 'RETRIEVE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false
};
case 'LOGIN':
return {
...prevState,
userName: action.id,
userToken: action.token,
isLoading: false
};
case 'LOGOUT':
return {
...prevState,
userName: null,
userToken: null,
isLoading: false
};
case 'REGISTER':
return {
...prevState,
userName: action.id,
userToken: action.token,
isLoading: false
};
}
}
const [loginState, dispatch] = useReducer(loginReducer, initialLoginState)
const authContext = useMemo(() => ({
userLogin: async (userName, password) => {
// setUserToken("abc");
// setLoading(false);
let userToken;
userToken = 'abc';
if (userName == 'user' && password == 'pass') {
try {
await AsyncStorage.setItem('userToken', userToken)
}
catch(e) {
console.log(e);
}
}
dispatch({type: 'LOGIN', id: userName, token: userToken});
},
userSignup: () => {
},
}));
useEffect(() => {
setTimeout(async () => {
// setLoading(false);
let userToken;
try {
await AsyncStorage.getItem('userToken')
}
catch(e) {
console.log(e);
}
dispatch({type: 'RETRIEVE_TOKEN', token: userToken});
}, 1000);
}, []);
LoginScreen.js
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [pwd, resetPwd] = useState(false);
const { userLogin } = useContext(AuthContext);
return(
<KeyboardAvoidingView behavior='position'>
<View style={styles.logoView}>
<Image style={styles.loginImage} source={require('../assets/login.png')}/>
<View>
<Text style={styles.logoText}> Welcome </Text>
<Text style={styles.logoSubText}> Please Login to continue, If account already created. </Text>
</View>
</View>
<View style={styles.loginInput}>
<Input placeholder='Enter your email address' value={email} onChangeText={(email) => setEmail(email)}
leftIcon={<IconI name='email' size={24} color='black' />}/>
<Input placeholder='Enter your password' secureTextEntry={true} value={password} onChangeText={(password) => {setPassword(password)}}
leftIcon={<Icon name='key' size={24} color='black' />}/>
</View>
<View style={styles.loginButtonBox}>
<TouchableOpacity onPress={() => userLogin(email, password)}>
<Text style={styles.loginButton}> LOGIN </Text>
</TouchableOpacity>
</View>
<View style={styles.noAccount}>
<Text>{"\n"} Don't have an account yet ? </Text>
<TouchableOpacity onPress={() => navigation.navigate('SignupScreen')}>
<Text style={styles.linkText}>{"\n"} Create New One </Text>
</TouchableOpacity>
</View>
<View style={styles.forgotPassword}>
<Text>{"\n"} Don't remember the Password ? </Text>
<TouchableOpacity style={styles.linkText} onPress={() => {
resetPwd(true);
}}>
<Text style={styles.linkText}>{"\n"} Forgot Password </Text>
</TouchableOpacity>
</View>
context.js
export const AuthContext = createContext();
As per the observation of the code provided by you,
here is the issue, occurring due to the saving of userToken using RETRIVE_TOKEN action not working properly as the code behavior hasn't initialized the variable of userToken to the value It getting from async storage so, you must have to initialize the userToken variable with the value getting from asyncStorage.
you can try with the below code changes in your App.js useEffect hook
useEffect(() => {
setTimeout(async () => {
let userToken;
try {
userToken = await AsyncStorage.getItem('userToken')
}
catch(e) {
console.log(e);
}
dispatch({type: 'RETRIEVE_TOKEN', token: userToken});
}, 1000);
}, []);

How can i pass the token to my child component react native

I was having trouble with how to get and save my token in my login component to my app component that is done now, I can't log out in because I don't know how to get the token from the local storage and I don't know how to pass it to the other screen and catch it there. this is my app component :
const App = () => {
const [userToken, setUserToken] = useState(0);
useEffect(() => {
AsyncStorage.getItem('token').then((value) => {
if (value) {
setUserToken(value);
}
});
}, []);
return (
<NavigationContainer>
<Stack.Navigator>
{userToken == null ? (
<>
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Restablecer" component={RestablecerPasswd} />
<Stack.Screen name="Registrar" component={RegistrarNewUsr} />
</>
) : (
<>
<Stack.Screen name="Perfil" component={Perfil} />
<Stack.Screen name="Configuraciones" component={CambiarPsswd} />
<Stack.Screen name="Dietas" component={Dietas} />
<Stack.Screen name="Datos" component={Data} />
</>
)}
</Stack.Navigator>
</NavigationContainer>
);
};
this is my profile component , this is where i want to catch the data that i get from a http request including the token that i use to enter this screen :
const Perfil = ({navigation}) => {
return (
<SafeAreaView>
<ScrollView>
<Text>Datos del Usuario</Text>
<Button title="Configuraciones" onPress={() => navigation.navigate('Configuraciones')}/>
<Button title="Mis Dietas" onPress={() => navigation.navigate('Dietas')}/>
<Button title="Datos Semanales" onPress={() => navigation.navigate('Datos')}/>
</ScrollView>
</SafeAreaView>
)
}
this is the way i get the token before enter the Perfil component screen:
const Login = ({navigation}) => {
const INITIAL_TOKEN = 0;
const token = useState(INITIAL_TOKEN);
const [email,setEmail] = useState('');
const [password,setPassword] = useState('');
const onEndGetDatos = (payload) => {
AsyncStorage.setItem('token', payload.data.token);
};
const sendDataL = () => {
Axios.post('http://3.90.64.114/api/v1/web/login',{
email,
password
}
).then(response => {
onEndGetDatos(response);
}).catch(err => {
alert(err);
})
}
useEffect(() => {
if (token !== INITIAL_TOKEN) {
AsyncStorage.setItem('token', `${token}`);
}
}, [token]);
return (
<SafeAreaView>
<ScrollView>
<TextInput placeholder="Usuario(email)"
onChangeText={email => setEmail(email)}
keyboardType = 'email-address'
/>
<TextInput placeholder="ContraseƱa"
secureTextEntry={true}
onChangeText={password => setPassword(password)}/>
<Button title="Entrar" onPress={sendDataL}/>
<Button title="Olvide mi ContraseƱa" onPress={() => navigation.navigate('Restablecer')}/>
<Button title="Soy Nuevo" onPress={() => navigation.navigate('Registrar')}/>
</ScrollView>
</SafeAreaView>
)
}
Your app is already getting the token from local storage when the app first renders here:
useEffect(() => {
AsyncStorage.getItem('token').then((value) => {
if (value) {
setUserToken(value);
}
});
}, []);
You can set a state value such as userLoggedIn to true and you can use that state value throughout your app.
When you want to logout, just clear the token value in local storage and set userLoggedIn state to false.
To pass this value to child components, you can use props or useContext() hook.

React navigation 5 ( navigate to another stack Screen)

this is my appNavigator: I divide into 3 stackScreen include HomeStack, SettingsStack and ProfileStack. AuthStack contain SignUp and Signin Screen, and i create bottom tab contain 3 StackScreen above
const HomeStack = createStackNavigator();
function HomeStackScreen() {
return (
<HomeStack.Navigator headerMode="none">
<HomeStack.Screen name="Home" component={HomeScreen} />
<HomeStack.Screen name="Details" component={DetailsScreen} />
</HomeStack.Navigator>
);
}
const SettingsStack = createStackNavigator();
function SettingsStackScreen() {
return (
<SettingsStack.Navigator>
<SettingsStack.Screen name="Settings" component={SettingsScreen} />
<SettingsStack.Screen name="Details" component={DetailsScreen} />
</SettingsStack.Navigator>
);
}
const ProfileStack = createStackNavigator();
function ProfileStackScreen({navigation}) {
return (
<ProfileStack.Navigator>
<ProfileStack.Screen name="Profile" component={ProfileScreen} />
</ProfileStack.Navigator>
);
}
// ** AUTH ** //
const AuthStack = createStackNavigator();
function AuthStackScreen() {
return (
<AuthStack.Navigator headerMode="none">
<AuthStack.Screen name="SignIn" component={LoginScreen} />
<AuthStack.Screen name="SignUp" component={SignUpScreen} />
</AuthStack.Navigator>
);
}
// ** APP ** //
const AppStack = createBottomTabNavigator();
function AppStackScreen() {
return (
<AppStack.Navigator name="mainApp">
<AppStack.Screen name="Dashboard" component={HomeStackScreen} />
<AppStack.Screen name="Favorite" component={SettingsStackScreen} />
<AppStack.Screen name="Profile" component={ProfileStackScreen} />
</AppStack.Navigator>
);
}
// ** ROOT ** //
const RootStack = createStackNavigator();
const RootStackScreen = ({userToken}) => {
return (
<RootStack.Navigator headerMode="none">
<RootStack.Screen name="Auth" component={AuthStackScreen} />
<RootStack.Screen name="App" component={AppStackScreen} />
</RootStack.Navigator>
);
};
export default function AppNavigator() {
const [loading, setloading] = React.useState(true);
const [userToken, setUserToken] = React.useState();
React.useEffect(() => {
setTimeout(() => {
setloading(false);
}, 1000);
});
if (loading) {
return <SplashScreen />;
}
// })
return (
<NavigationContainer>
<RootStackScreen />
</NavigationContainer>
);
}
and this is my Login Screen :
const LoginScreen = ({ navigation, props }) => {
console.tron.log("debug: LoginScreen -> props", props, navigation)
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const [error, setError] = React.useState('');
const [loading, setLoading] =React.useState(false)
const handleLogin = () => {
if (email === '' && password === '') {
Alert.alert('Enter details to signin!')
} else {
setLoading(true)
firebase
.auth()
.signInWithEmailAndPassword(email,password)
.then((res) => {
console.log(res)
console.log('User logged-in successfully!')
setLoading(false)
setEmail('')
setPassword('')
navigation.navigate("AppStack", {screen: "Dashboard"})
})
.catch(error => setError(error.message))
}
}
return (
<ImageBackground source={require('../images/background.jpg')} style={styles.imageBack}>
<View style={styles.area1}>
<Image
source={require('../images/vaccines.png')}
style={styles.logo} />
<View style={styles.box}>
<TextInput
placeholder='Email'
onChangeText={email => setEmail(email)}
value={email}
maxLength={15}
/>
</View>
<View style={styles.box}>
<TextInput
placeholder='Password'
secureTextEntry={true}
onChangeText={password => setPassword(password)}
value={password}
maxLength={15}
/>
</View>
<BlockIcon />
<TouchableOpacity style={styles.buttonLogin} onPress={handleLogin}>
<Text style={styles.text1}>Sign In</Text>
</TouchableOpacity>
<View style={{ flexDirection: 'row', justifyContent: 'center', marginTop: 50 }}>
<Text style={styles.text2}> If you don't have an account ?</Text>
<TouchableOpacity onPress={() => navigation.push('SignUp')}>
<Text style={styles.text3}> Sign Up </Text>
</TouchableOpacity>
</View>
</View>
</ImageBackground>
)
}
I navigate at navigation.navigate("AppStack", {screen: "Dashboard"})
when I login successful. I want to navigate to AppStack (bottomtab with initial Screen name is Dasboard) I try to use nesting navigator but unsuccessful. I also try to use navigation.navigate('HomeScreen'), but it not work. Anyone can help me =((. Thanks
I know this is a month old but maybe this will help someone else.
Looks like you're close. In your RootStackScreen, you have two screens: "Auth" and "App". And it sounds like you want to go to your "App" screen from your "Auth" screen. You just need to use the name of the screen.
navigation.navigate("App", {screen: "Dashboard"})

Cannot call Element Props inside React Navigation Stack.Navigator

I am using React-Navigation v5 with Redux. I like to call my logOut action creator to trigger my log out function from my headerRight.
However, I can only access logOut from inside Home element and not inside HomeStack.
One idea I have is to also wrap my HomeStack with connect. I haven't tried it yet to know whether it can work or not. Even should it work, this isn't my preferred solution because i feel it makes my code very verbose
Anyone has a solution on how to access my logOut function from within my HomeStack? Thanks in advance
const Home = props => {
const { loading, error, data, subscribeToMore } = useQuery(FIND_BOOKS_QUERY);
console.log("home props", props);
return (
<View>
<Text> Welcome {props.user && props.user.name} </Text>
{loading && <Text>Loading...</Text>}
{error && <Text>Error: {error.message}</Text>}
{data &&
data.findBooks.map((x, index) => {
return (
<View key={index}>
<Text>
{x.title} - {x.author}
</Text>
</View>
);
})}
</View>
);
};
const HomeContainer = connect(
state => {
// console.log("home state", state);
return {
user: state.auth.user
};
},
{ logOut }
)(Home);
export const HomeStack = props => {
console.log("home stack props", props);
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeContainer}
options={{
headerRight: () => {
return (
<TouchableOpacity
onPress={() => {
// props.logOut // - cannot access logOut here as HomeStack props does not have logOut
console.log("exit");
}}
>
<Text>Exit</Text>
</TouchableOpacity>
);
}
}}
/>
</Stack.Navigator>
);
};
Wrapping my HomeStack with connect works. Its able to make available logOut inside H which allows me to call logOut inside headerRight.
However, such a method will be verbose because I will need to connect both H and Home to redux. Is there a more elegant way? Thanks
const Home = props => {
const { loading, error, data, subscribeToMore } = useQuery(FIND_BOOKS_QUERY);
console.log("home props", props);
return (
<View>
<Text> Welcome {props.user && props.user.name} </Text>
{loading && <Text>Loading...</Text>}
{error && <Text>Error: {error.message}</Text>}
{data &&
data.findBooks.map((x, index) => {
return (
<View key={index}>
<Text>
{x.title} - {x.author}
</Text>
</View>
);
})}
</View>
);
};
const HomeContainer = connect(
state => {
// console.log("home state", state);
return {
user: state.auth.user
};
},
{ logOut }
)(Home);
export const H = props => {
console.log("home stack props", props);
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeContainer}
options={{
headerRight: () => {
return (
<TouchableOpacity
onPress={() => {
props.logOut(); // now works OK
console.log("exit");
}}
>
<Text>Exit</Text>
</TouchableOpacity>
);
}
}}
/>
</Stack.Navigator>
);
};
export const HomeStack = connect(
state => {
// console.log("home state", state);
return {
user: state.auth.user
};
},
{ logOut }
)(H);