Navigation does not change to MainScreen even after redux state changes. I have verified that authState changes from {"isAuthenticated": false, "isLoading": true, "token": null} to {"isAuthenticated": true, "isLoading": false, "token": "some_token"}, but the navigation page stays at login page (inside LandingNavigator) instead of going to MainNavigator.
AppNavigation.js
const Navigator = () => {
var [authStat, setAuthStat] = useState({})
useEffect(()=> {
var authState = store.getState().auth
setAuthStat(authState)
}, [authStat])
console.log(authStat);
if(authStat.isLoading){
return(
<Stack.Navigator>
<Stack.Screen
options={{headerShown: false}}
name="Splash"
component={ShowSplash}
/>
</Stack.Navigator>
)
}
else{
return(
<Stack.Navigator>
{ authStat.isAuthenticated ?
(<Stack.Screen
options={{headerShown: false}}
name="Main"
component={MainNavigator}
/>)
:
(<Stack.Screen options={{headerShown: false}} name="Landing" component={LandingNavigator} />)
}
</Stack.Navigator>
)
}
};
AuthAction.js
export function loginRequest() {
return {
type: "LOGIN_REQUEST",
};
}
export function loginSuccess(data) {
return {
type: "LOGIN_SUCCESS",
payload: data
};
}
export function loginFailure(data) {
return {
type: "LOGIN_FAILURE",
payload: data
};
}
export function restoreToken(data) {
return {
type: "RESTORE_TOKEN",
payload: data
};
}
export function logOut() {
return {
type: "LOGOUT",
};
}
AuthReducer.js
/* eslint-disable comma-dangle */
const authState = {
isLoading: true,
isAuthenticated: false,
token: null
};
export const authReducer = (state = authState, action) => {
const newState = JSON.parse(JSON.stringify(state));
switch (action.type) {
case 'LOGIN_REQUEST': {
return {
isLoading: true, // Show a loading indicator.
isAuthenticated: false
}
}
case 'RESTORE_TOKEN': {
return {
isLoading: false, // Show a loading indicator.
isAuthenticated: true,
token: action.payload
}
}
case 'LOGIN_FAILURE':
return {
isLoading: false,
isAuthenticated: false,
error: action.error
}
case 'LOGIN_SUCCESS':
return {
isLoading: false,
isAuthenticated: true, // Dismiss the login view.
token: action.payload
}
case 'LOGOUT': {
return {
isLoading: false, // Show a loading indicator.
isAuthenticated: false,
token: null
}
}
default:
return newState;
}
return newState;
};
Auth.js
import AsyncStorage from '#react-native-async-storage/async-storage';
import { useDispatch } from 'react-redux';
import {loginRequest, loginSuccess, loginFailure, logOut} from '../redux/actions/authAction';
export const storeToken = async (value) => {
try {
await AsyncStorage.setItem('token', value)
} catch (e) {
// saving error
}
}
export const getToken = async () => {
try {
const value = await AsyncStorage.getItem('token')
if(value !== null) {
return value
} else{
return null
}
} catch(e) {
// error reading value
console.log(e);
}
}
export const removeToken = async () => {
try {
await AsyncStorage.removeItem('token')
} catch(e) {
// remove error
}
console.log('token removed.')
}
export const isLoggedIn = async () => {
if(await getToken() != null){
return true
}
return false
}
export const signOut = () => {
removeToken()
}
export default {storeToken, getToken, removeToken, isLoggedIn, signOut }
LoginScreen.js
/* eslint-disable comma-dangle */
import React, { useEffect, useState, useCallback } from 'react';
import {
View,
TouchableHighlight,
Text,
TextInput,
TouchableWithoutFeedback,
Keyboard,
ScrollView
} from 'react-native';
import {login} from '../../api/apiQueries'
import {storeToken} from '../../auth/auth'
import store from '../../redux/store';
import styles from './styles';
import { useDispatch, useSelector } from 'react-redux';
const authState = store.getState().auth;
const LogInScreen = ({route, navigation}) => {
const [userName, setUserName] = useState("")
const [password, setPassword] = useState("")
const dispatch = useDispatch()
onPressLogButton = () => {
dispatch(login(userName, password))
}
return (
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<ScrollView style={styles.container}>
<View>
<Text style={styles.title}>Sign in</Text>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="User Name"
onChangeText={text => setUserName(text)}
value={userName}
/>
</View>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="Password"
onChangeText={text => setPassword(text)}
value={password}
/>
</View>
<View style={styles.logContainer}>
<TouchableHighlight
style={styles.loginContainer}
onPress={() => onPressLogButton()}
>
<Text style={styles.logTxt}>Log in</Text>
</TouchableHighlight>
{/* <Text style={styles.orTxt}>OR</Text> */}
{/* <TouchableHighlight
style={styles.facebookContainer}
onPress={() => this.onPressFacebookButton()}
>
<Text style={styles.facebookTxt}>Facebook Login</Text>
</TouchableHighlight> */}
</View>
</View>
</ScrollView>
</TouchableWithoutFeedback>
);
}
export default LogInScreen
As discussed in the comments, the solution is to either make use of the useSelector hook, or to subscribe your component to store updates using the mapStateToProps parameter of the connect method. That way, it will run whenever the store updates through a dispatched action.
From the docs:
useSelector() will also subscribe to the Redux store, and run your
selector whenever an action is dispatched. Link
This means, for your AppNavigation.js, for example, you can change the code to:
import { useSelector } from 'react-redux';
const Navigator = () => {
const authStat = useSelector((state) => state.auth);
if(authStat.isLoading){
return(
...
Reading from the store by direct access will do just that, but it does not imply a subscription for future changes.
Related
I'm trying to access the state values from the redux store which returns the correct values for the initial state of the reducer. However when I try to dispatch an action (authenticate) and update the state values then when I try to access the state again it returns undefined values for the state properties (jwtToken & isAuthenticated)
Here's my app component where all the action is happening
import "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { createStackNavigator } from "#react-navigation/stack";
import { createBottomTabNavigator } from "#react-navigation/bottom-tabs";
import { NavigationContainer } from "#react-navigation/native";
import { useFonts } from "expo-font";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Ionicons } from "#expo/vector-icons";
import { Provider } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { authenticate } from "./store/slices/userSlice";
import { Store } from "./store/store";
import AsyncStorage from "#react-native-async-storage/async-storage";
import * as SplashScreen from "expo-splash-screen";
import OnboardingScreen from "./screens/OnboardingScreen";
import LoginScreen from "./screens/LoginScreen";
import SignupScreen from "./screens/SignupScreen";
import HomeScreen from "./screens/HomeScreen";
import Colors from "./constant/colors";
import FavoriteScreen from "./screens/FavoriteScreen";
import NotificationScreen from "./screens/NotificationScreen";
import ProfileScreen from "./screens/ProfileScreen";
import { StorageKeys } from "./constant/storageKeys";
const Stack = createStackNavigator();
const BottomTabStack = createBottomTabNavigator();
function AuthStack() {
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="Onboarding" component={OnboardingScreen} />
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Signup" component={SignupScreen} />
</Stack.Navigator>
);
}
function AuthenticatedStack() {
return (
<BottomTabStack.Navigator
initialRouteName="Home"
screenOptions={{
tabBarActiveTintColor: Colors.raisin_black,
tabBarLabelStyle: { fontSize: 14 },
}}
>
<BottomTabStack.Screen
name="Home"
component={HomeScreen}
options={{
tabBarLabel: ({ focused }) => {
return focused ? <Text>Home</Text> : null;
},
tabBarIcon: ({ focused, color, size }) => (
<Ionicons
name={focused ? "home" : "home-outline"}
color={color}
size={size}
/>
),
}}
/>
<BottomTabStack.Screen
name="Favorites"
component={FavoriteScreen}
options={{
// tabBarLabel: "Favorites",
tabBarLabel: ({ focused }) => {
return focused ? <Text>Favorites</Text> : null;
},
tabBarIcon: ({ focused, color, size }) => (
<Ionicons
name={focused ? "bookmark" : "bookmark-outline"}
color={color}
size={size}
/>
),
}}
/>
<BottomTabStack.Screen
name="Notifications"
component={NotificationScreen}
options={{
// tabBarLabel: "Notifications",
tabBarLabel: ({ focused }) => {
return focused ? <Text>Notifications</Text> : null;
},
tabBarIcon: ({ focused, color, size }) => (
<Ionicons
name={focused ? "notifications" : "notifications-outline"}
color={color}
size={size}
/>
),
}}
/>
<BottomTabStack.Screen
name="Profile"
component={ProfileScreen}
options={{
// tabBarLabel: "Profile",
tabBarLabel: ({ focused }) => {
return focused ? <Text>Profile</Text> : null;
},
tabBarIcon: ({ focused, color, size }) => (
<Ionicons
name={focused ? "person" : "person-outline"}
color={color}
size={size}
/>
),
}}
/>
</BottomTabStack.Navigator>
);
}
function setupNavigation(isAuthenticated) {
console.log("setupnavigation rendered");
//TODO handle authentication state if user is already logged in
console.log("isAuthenticated", isAuthenticated);
return (
<NavigationContainer>
{!isAuthenticated && <AuthStack />}
{isAuthenticated && <AuthenticatedStack />}
</NavigationContainer>
);
}
// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();
function RootView() {
console.log("root view rendered");
const [fetchingToken, setFetchingToken] = useState(true);
// const [appIsReady, setAppIsReady] = useState(false);
let jwtToken = useSelector((state) => {
return state.userReducer.jwtToken;
});
let isAuthenticated = useSelector((state) => {
return state.userReducer.isAuthenticated;
});
console.log("isAuthenticated=> ", isAuthenticated);
console.log("jwtToken=> ", jwtToken);
const dispatch = useDispatch();
useEffect(() => {
async function fetchToken() {
let jwtToken = await AsyncStorage.getItem(StorageKeys.JWT_TOKEN);
if (jwtToken !== null) {
dispatch(authenticate({ jwt: jwtToken }));
}
setFetchingToken(false);
}
fetchToken();
}, []);
const onLayoutRootView = useCallback(async () => {
if (!fetchingToken) {
// This tells the splash screen to hide immediately! If we call this after
// `setAppIsReady`, then we may see a blank screen while the app is
// loading its initial state and rendering its first pixels. So instead,
// we hide the splash screen once we know the root view has already
// performed layout.
await SplashScreen.hideAsync();
}
}, [fetchingToken]);
if (fetchingToken) {
return null;
}
return (
<View style={styles.container} onLayout={onLayoutRootView}>
{setupNavigation(isAuthenticated)}
<StatusBar style="auto" />
</View>
);
}
export default function App() {
console.log("app rendered");
const [isFontsLoaded] = useFonts({
poppins_light: require("./assets/fonts/Poppins-Light.ttf"),
poppins_reg: require("./assets/fonts/Poppins-Regular.ttf"),
poppins_med: require("./assets/fonts/Poppins-Medium.ttf"),
poppins_semi_bold: require("./assets/fonts/Poppins-SemiBold.ttf"),
poppins_bold: require("./assets/fonts/Poppins-Bold.ttf"),
});
if (!isFontsLoaded) {
return null;
}
return (
<Provider store={Store}>
<RootView />
</Provider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
This the store file that I have:-
import { configureStore } from "#reduxjs/toolkit";
import usersReducer from "./slices/userSlice";
export const Store = configureStore({
reducer: {
userReducer: usersReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
And this is my user slice :-
import { createSlice } from "#reduxjs/toolkit";
import AsyncStorage from "#react-native-async-storage/async-storage";
import { StorageKeys } from "../../constant/storageKeys";
const initialState = {
jwtToken: "",
isAuthenticated: false,
};
const userSlice = createSlice({
name: "UserSlice",
initialState: initialState,
reducers: {
authenticate: async (state, action) => {
let jwt = action.payload.jwt;
console.log("jwt in payload=> ", jwt);
state.jwtToken = jwt;
state.isAuthenticated = true;
console.log(
"isAuthenticated state in store after authentication=>",
state.isAuthenticated
);
await AsyncStorage.setItem(StorageKeys.JWT_TOKEN, jwt);
return state;
},
logout: async (state) => {
await AsyncStorage.removeItem(StorageKeys.JWT_TOKEN);
state.jwtToken = "";
state.isAuthenticated = false;
return state;
},
},
});
export const { authenticate, logout } = userSlice.actions;
console.log("user slice reducer", userSlice.reducer);
export default userSlice.reducer;
Update: Well it turns out the usage of async await in the reducer functions were causing the issue. As per the official docs of redux, reducer functions are supposed to be pure and synchronous.
We need to use redux-thunk for asynchronous functionality.
After removing the async await from these functions I got to access proper updated values from the store
So im making an app with authentication flow and followed the docs
now even though there was no user in the state the navigation didn't move to login screen so i changed the condition for rendering and now the i'm on Login page except there's no output and following error
Got a component with the name 's' for the screen 'Settings'.Got a component with the name 'l' for the screen 'Login'.Got a component with the name 'p' for the screen 'Home'. React Components must start with an uppercase letter. If you're passing a regular function and not a component, pass it as children to 'Screen' instead. Otherwise capitalize your component's name.
App.js
import React, {createContext, useContext, useEffect, useState} from 'react';
import {NativeBaseProvider, Box} from 'native-base';
import AsyncStorage from '#react-native-async-storage/async-storage';
import {createNativeStackNavigator} from '#react-navigation/native-stack';
import {theme} from './theme';
import {NavigationContainer} from '#react-navigation/native';
import {
signInWithEmailPassword,
signOutFunc,
signUpWithEmailPassword,
signInWithGoogle,
} from './components/auth/helper';
import Login from './components/auth/Login';
import Settings from './components/core/Settings';
import Home from './components/core/Home';
import {createMaterialBottomTabNavigator} from '#react-navigation/material-bottom-tabs';
const Tab = createMaterialBottomTabNavigator();
const Stack = createNativeStackNavigator();
export const AuthContext = createContext();
export default function App({navigation}) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_USER':
return {...prevState, user: action.user, isLoading: false};
case 'SIGN_IN':
return {...prevState, isSignout: false, user: action.user};
case 'SIGN_OUT':
return {...prevState, isSignout: true, user: null};
}
},
{
isLoading: true,
isSignout: false,
user: null,
},
);
const authContext = React.useMemo(
() => ({
signIn: async (data, type) => {
let user;
if (type === 'email') {
user = await signInWithEmailPassword(data.email, data.password);
} else {
user = await signInWithGoogle();
}
if (user) {
try {
await AsyncStorage.setItem('user', JSON.stringify(user));
dispatch({type: 'SIGN_IN', user: user});
} catch (e) {
console.log(e);
}
}
},
signOut: async type => {
try {
signOutFunc('google');
} catch (e) {
console.log(e);
}
try {
signOutFunc('email');
} catch (e) {
console.log(e);
}
dispatch({type: 'SIGN_OUT'});
},
signUp: async (data, type) => {
let user;
if (type === 'email') {
user = await signUpWithEmailPassword(data.email, data.password);
} else {
user = await signInWithGoogle();
}
if (user) {
try {
const user = await AsyncStorage.setItem(
'user',
JSON.stringify(data),
);
dispatch({type: 'SIGN_IN', user: user});
} catch (e) {
console.log(e);
}
}
},
}),
[],
);
useEffect(() => {
const bootstrapAsync = async () => {
let user;
try {
user = await AsyncStorage.getItem('user');
console.log(user);
} catch (e) {
console.log(e);
}
dispatch({type: 'RESTORE_USER', user: user});
};
bootstrapAsync();
}, [state.user]);
return (
<NativeBaseProvider theme={theme}>
<AuthContext.Provider value={authContext}>
<NavigationContainer
screenOptions={{
headerShown: false,
}}>
{state.user !== null ? (
<Tab.Navigator>
<Tab.Screen name="Login" component={Login} />
</Tab.Navigator>
) : (
<Tab.Navigator screenOptions={{headerShown: false}}>
<Tab.Screen name="Home" component={Home} />
<Tab.Screen name="Settings" component={Settings} />
</Tab.Navigator>
)}
</NavigationContainer>
</AuthContext.Provider>
</NativeBaseProvider>
);
}
Login.js
import {View, Text} from 'react-native';
import React from 'react';
import EmailSignUp from './EmailSingUp';
import GoogleSignin from './GoogleSignIn';
const Login = () => {
return (
<View>
<GoogleSignin />
<EmailSignUp />
</View>
);
};
export default Login;
Setting.js
import {View, Text} from 'react-native';
import React from 'react';
import {AuthContext} from '../../App';
import {Button} from 'react-native-paper';
const Settings = () => {
const {signOut} = React.useContext(AuthContext);
return (
<View>
<Text>Settings</Text>
<Button onPress={() => signOut()}>Sign Out </Button>
</View>
);
};
export default Settings;
I had the same issue on Android. I realised that my "JS Minify" setting was checked which loads the JavaScript bundle with minify=true.
It might be the same issue in your case.
In order to disable this settings, open the Developer menu, select "Settings" and then unselect "JS Minify" option.
Close your app/uninstall it first to ensure that you no longer have this issue.
I have managed to get my login authentification working, but I am having an issue with the conditional navigator in App.js. If I log in using the LoginStackNavigator, I then need to refresh the app to be able to use the DrawerNavigator. I would like it so that as soon as I log in, App.js realises this, and takes me to the drawer navigator.
I've tried to use the Context API but it isn't working. What am I doing wrong?
Here is my AppContext.js
import { createContext } from "react";
const AppContext = createContext({
isloggedIn: {},
setLoggedIn: () => {},
});
export default AppContext;
Here is my App.js:
import AppContext from "./src/Component/AppContext";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
isloggedIn: false,
};
this.loginStatusCheck();
}
loginStatusCheck = async () => {
const userToken = await AsyncStorage.getItem("#storage_Key");
if (userToken) {
this.setState({ isloggedIn: true });
} else {
this.setState({ isloggedIn: false });
}
};
render() {
return (
<AppContext.Provider
value={{
isloggedIn: this.state.isloggedIn,
setLoggedIn: this.setLoggedIn,
}}
>
<NavigationContainer>
{this.state.isloggedIn ? (
<DrawerNavigator />
) : (
<LoginStackNavigator />
)}
</NavigationContainer>
</AppContext.Provider>
);
}
}
And here is my LoginScreen.js:
import AppContext from "../../Component/AppContext";
const LoginCall = () => {
const { setLoggedIn } = useContext(AppContext);
return setLoggedIn(true);
};
export default class LoginScreen extends Component {
login = async () => {
const { email, password, confirmpassword } = this.state;
console.log(email);
axios
.post("http://127.0.0.1:8002/rest-auth/login/", {
username: "username",
email: "default#email.com",
password: "password",
})
.then((response) => {
console.log(response.data.key);
this.storeKey(response.data.key);
LoginCall;
})
//.then(this.props.navigation.navigate("HomeScreen"))
.catch((error) => {
console.log(error);
});
};
storeKey = async (value) => {
try {
await AsyncStorage.setItem("#storage_Key", value);
} catch (e) {
console.log("error" + e);
} finally {
console.log("done");
}
};
render() {
return (
<View
style={{
backgroundColor: "#fff",
paddingTop: 40,
alignItems: "center",
flex: 1,
}}
>
<TouchableOpacity onPress={() => this.login()}>
<Text>Login</Text>
</TouchableOpacity>
</View>
);
}
}
I think its because you're not importing axios on LoginSreen.
Try to add import axios from 'axios' with others importations
I'm new to react native concepts. I'm trying to pass functions and a parameter to context API so that I can access those in my child component. I'm trying to implement basic login functionality that will show different messages based on user login status. The functionality works when I pass the SignIn method to my child component but the same function is not accessible when I send it along with some variable. The below code and notes can explain the problem clearly.
In the below code as you can see I'm passing my reducer function and initial states from which I get the error messages and sign function <AuthenticationContext.Provider value={[authContext, initialLoginState]}>
App.js
import * as React from 'react';
import { NavigationContainer } from '#react-navigation/native';
import BottomNavigation from './src/Navigations/BottomNavigation';
import AuthStackNavigation from './src/Navigations/AuthStackNavigation'
import { useEffect, useMemo, useReducer } from 'react';
import { View } from 'react-native-animatable';
import apiCaller from "./src/api/apiCaller";
import { ActivityIndicator, Text } from 'react-native';
import { AuthenticationContext } from './src/context/AuthenticationContext'
import { Provider as VideoProvider } from './src/context/videoContext'
import { Context as VideoContext } from './src/context/videoContext'
import AsyncStorage from '#react-native-async-storage/async-storage'
export default function App() {
//Initial state values
const initialLoginState = {
isLoading: true,
userName: null,
userToken: null,
errorMessage: ''
}
//Reducer function
const loginReducer = (prevState, action) => {
switch (action.type) {
case 'LOGIN':
return {
...prevState,
userToken: action.token,
userName: action.id,
isLoading: false
};
case 'LOGOUT':
return {
...prevState,
userName: null,
userToken: null,
isLoading: false,
};
case 'REGISTER':
return {
...prevState,
userToken: action.token,
userName: action.id,
isLoading: false,
};
case 'ERROR':
return {
...prevState,
errorMessage: action.response,
isLoading: false
};
default:
return { userToken: null }
}
}
//Defining useReducer
const [newLoginState, dispatch] = useReducer(loginReducer, initialLoginState);
const authContext = useMemo(() => ({
signIn: async (email, password) => {
try {
const userData = {
email: email,
password: password,
};
const response = await apiCaller.post("/login", userData);
if (response.data.code == '200') {
await AsyncStorage.setItem('userToken', response.data.token)
dispatch({ type: 'LOGIN', id: email, token: response.data.token })
}
else if (response.data.code == '404') {
dispatch({ type: 'ERROR', response: response.data.message })
}
} catch (err) {
console.log(err);
}
}
}), []);
return (
<VideoProvider value={VideoContext}>
<AuthenticationContext.Provider value={{authContext, initialLoginState}}>
<NavigationContainer>
{newLoginState.userToken == null ?
<AuthStackNavigation />
:
<BottomNavigation />
}
</NavigationContainer>
</AuthenticationContext.Provider>
</VideoProvider>
);
}
and in the below file, I'm fetching the provider vales by
const { signIn, initialLoginState } = useContext(AuthenticationContext) but its giving me "signIn is not a function. (In 'signIn(email, password)', 'signIn' is undefined)" error but signIn method is accessible when i just try to pass and access SignIn method alone.
Signinscreen.js
import React, { useState } from 'react'
import { Ionicons, MaterialIcons } from '#expo/vector-icons';
import { Text, View, StyleSheet, TouchableOpacity, Platform, StatusBar } from 'react-native';
import { WindowHeight, WindowWidth } from '../utils/PlatformDimention'
import PrimaryFormInput from "../components/PrimaryFormInput";
import PrimaryFormButton from "../components/PrimaryFormButton";
import { AuthenticationContext } from "../context/AuthenticationContext";
import { useContext } from 'react';
const UserLogin = ({ navigation }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { signIn, initialLoginState } = useContext(AuthenticationContext)
const loginHandle = (email, password) => {
signIn(email, password);
}
return (
<View style={styles.container}>
<StatusBar barStyle='light-content' />
<View style={styles.header}>
<View style={{ flex: 1, }}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<MaterialIcons style={styles.goBack} name="arrow-back" size={WindowHeight / 27} />
</TouchableOpacity>
</View>
<View style={{ flex: 2, alignItems: 'center', justifyContent: 'center', }}>
<Ionicons
style={{ color: "#fff" }} name="logo-bitbucket" size={WindowHeight * 10 / 100} />
</View>
<View style={{ flex: 1, alignItems: 'flex-end' }}>
</View>
</View>
<View style={styles.footer}>
<View style={styles.topFlex}></View>
<View style={styles.middleFlex}>
<Text style={styles.loginText}>Welcome!</Text>
<Text style={styles.loginSubTextSub}>Login to your existing account.</Text>
<PrimaryFormInput
inputValue={email}
onChangeText={(userEmail) => setEmail(userEmail)}
inputPlaceHolder='Email'
iconName="user"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<PrimaryFormInput
inputValue={password}
onChangeText={(userPassword) => setPassword(userPassword)}
inputPlaceHolder='Password'
iconName="key"
secureTextEntry={true}
/>
<PrimaryFormButton
ButtonText='Login'
onPress={() => loginHandle(email, password)}
/>
</View>
<View style={styles.bottomFlex}></View>
</View>
</View>
)
}
export default UserLogin
Could anyone of you please let me know how to pass both method and parameters to my child component and access it?
Your mistake is just here
const { signIn, initialLoginState } = useContext(AuthenticationContext)
Because you pass the context object like this
<AuthenticationContext.Provider value={{authContext, initialLoginState}}>
You need to do the destructruring like this
const { authContext : { signIn }, initialLoginState } = useContext(AuthenticationContext)
I setup a basic auth system following the react-navigation auth flow guide with FeathersJS react-native client.
Here is my main index.tsx file:
import 'react-native-gesture-handler';
import React, {
useReducer, useEffect, useMemo, ReactElement,
} from 'react';
import { Platform, StatusBar } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { AppLoading } from 'expo';
import { useFonts } from '#use-expo/font';
import SplashScreen from './screens/SplashScreen';
import { AuthContext } from './contexts';
import { client } from './utils';
import DrawerNavigator from './navigation/DrawerNavigator';
import LogonStackNavigator from './navigation/LogonStackNavigator';
interface State {
isLoading: boolean;
isSignOut: boolean;
userToken: string|null;
}
const App = (): ReactElement => {
/* eslint-disable global-require */
const [fontsLoaded] = useFonts({
'Lato-Regular': require('../assets/fonts/Lato-Regular.ttf'),
'Lato-Bold': require('../assets/fonts/Lato-Bold.ttf'),
'Poppins-Light': require('../assets/fonts/Poppins-Light.ttf'),
'Poppins-Bold': require('../assets/fonts/Poppins-Bold.ttf'),
});
/* eslint-enable global-require */
const [state, dispatch] = useReducer(
(prevState: State, action): State => {
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,
};
default:
return prevState;
}
},
{
isLoading: true,
isSignOut: false,
userToken: null,
},
);
useEffect(() => {
const bootstrapAsync = async (): Promise<void> => {
let userToken;
try {
const auth = await client.reAuthenticate();
console.log('reAuthenticate:', auth);
userToken = auth.accessToken;
} catch (e) {
// eslint-disable-next-line no-console
console.log('reAuthenticate failure:', e);
}
dispatch({
type: 'RESTORE_TOKEN',
token: userToken,
});
};
bootstrapAsync();
}, []);
const authContext = useMemo(
() => ({
signIn: async (data) => {
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `AsyncStorage`
// In the example, we'll use a dummy token
// eslint-disable-next-line no-console
console.log('signIn', data);
try {
const auth = await client.authenticate({
strategy: 'local',
...data,
});
console.log(auth);
dispatch({
type: 'SIGN_IN',
token: 'dummy-auth-token',
});
} catch (e) {
console.log('signIn failure:', e);
}
},
signOut: async () => {
try {
await client.logout();
dispatch({ type: 'SIGN_OUT' });
} catch (e) {
console.log('signOut failure:', e);
}
},
signUp: async (data) => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `AsyncStorage`
// In the example, we'll use a dummy token
// eslint-disable-next-line no-console
console.log('signUp', data);
dispatch({
type: 'SIGN_IN',
token: 'dummy-auth-token',
});
},
}),
[],
);
if (!fontsLoaded) {
return <AppLoading />;
}
if (state.isLoading) {
return <SplashScreen />;
}
return (
<AuthContext.Provider value={authContext}>
{Platform.OS === 'ios' && (
<StatusBar barStyle="dark-content" />
)}
{state.userToken == null ? (
<NavigationContainer>
<LogonStackNavigator />
</NavigationContainer>
) : (
<NavigationContainer>
<DrawerNavigator />
</NavigationContainer>
)}
</AuthContext.Provider>
);
};
export default App;
And my SiginScreen.tsx file which handle the login form:
import React, { ReactElement } from 'react';
import { StyleSheet, KeyboardAvoidingView } from 'react-native';
import { StackNavigationProp } from '#react-navigation/stack';
import { RouteProp } from '#react-navigation/core';
import { AuthContext } from '../contexts';
import {
LogonHeader, Button, Input, Text, TextLink,
} from '../components';
import { LogonStackParamList } from '../navigation/LogonStackNavigator';
interface Props {
navigation: StackNavigationProp<LogonStackParamList, 'SignIn'>;
route: RouteProp<LogonStackParamList, 'SignIn'>;
}
const SignInScreen = ({ navigation }: Props): ReactElement => {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
return (
<KeyboardAvoidingView
style={styles.container}
behavior="padding"
>
<LogonHeader title="Se connecter" />
<Input
placeholder="E-mail"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
/>
<Input
placeholder="Mot de passe"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button
title="Connexion"
onPress={() => signIn({
email,
password,
})}
/>
</KeyboardAvoidingView>
);
};
export default SignInScreen;
It works as expected, but I can't figure out how to handle the error case.
Currently, it's just a console.log statement on index.tsx file.
How can I properly informs the SignInScreen component that the logins fail to show a message at the user end? Should I use redux or something?
More exactly: I would like to put an error text message directly on SignInScreen in case of failure.
As I can see, you could add a new property to your global state which could be possibly null that indicates the given Auth error, then you can dispatch everytime the service returns an error. If you are willing to change your approach, I'd suggest that you only store in a global state the user token, you can manage the isLoading individually in each screen and the isSignOut could be derived from the token. You will reduce the the number of re renders and will simplify the logic behind it.
EDIT:
This is a code sample of what you can do:
// main index.tsx
import 'react-native-gesture-handler';
import React, {
useReducer, useEffect, useMemo, ReactElement,
} from 'react';
import { Platform, StatusBar } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { AppLoading } from 'expo';
import { useFonts } from '#use-expo/font';
import SplashScreen from './screens/SplashScreen';
import { AuthContext } from './contexts';
import { client } from './utils';
import DrawerNavigator from './navigation/DrawerNavigator';
import LogonStackNavigator from './navigation/LogonStackNavigator';
interface State {
isLoading: boolean;
isSignOut: boolean;
userToken: string|null;
//we add a new property to your state object to make visible the errors
authError: string|null;
}
const App = (): ReactElement => {
// keep the same code, deleted for simplicity
const [state, dispatch] = useReducer(
(prevState: State, action): State => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
authError: null,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignOut: false,
authError: null,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignOut: true,
authError: null,
userToken: null,
};
case: 'AUTH_ERROR':
return {
...prevState,
authError: action.error
};
default:
return prevState;
}
},
{
isLoading: true,
isSignOut: false,
userToken: null,
authError: null,
},
);
useEffect(() => {
const bootstrapAsync = async (): Promise<void> => {
try {
const auth = await client.reAuthenticate();
console.log('reAuthenticate:', auth);
dispatch({
type: 'RESTORE_TOKEN',
token: auth.accessToken,
});
} catch (e) {
// eslint-disable-next-line no-console
console.log('reAuthenticate failure:', e);
dispatch({
type: 'AUTH_ERROR',
token: e, //or what ever your want to show
});
}
};
bootstrapAsync();
}, []);
const authContext = useMemo(
() => ({
//we add state here in order to access it from
//React.useContext(AuthContext);
state,
//put your code here
}),
[],
);
/*
...YOUR CODE...
*/
Basically, in the code above we added a new property authError to your global state. Then, you need a new action to update that property. Then, in your authContext we add the global state to be retrieved from anywhere.
To keep simplicity, I just added one dispatch with AUTH_ERROR action in your code, but you can do the same in signIn, signUp and signOut.
in your SiginScreen.tsx you can do the following:
const SignInScreen = ({ navigation }: Props): ReactElement => {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
//we get the state we added in our AuthContext
const { signIn, state } = React.useContext(AuthContext);
return (
<KeyboardAvoidingView
style={styles.container}
behavior="padding"
>
<LogonHeader title="Se connecter" />
<Input
placeholder="E-mail"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
/>
<Input
placeholder="Mot de passe"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button
title="Connexion"
onPress={() => signIn({
email,
password,
})}
/>
{/* shows the error if any, otherwise it won't be shown */}
{state.authError && <Text>{state.authError}</Text>}
</KeyboardAvoidingView>
);
};