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.
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
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.
Current Behavior
The app should redirect the user after loggin in or singin up to the homepage, but it throws this error instead
App.js :
import 'react-native-gesture-handler';
import React, { useEffect, useState } from 'react'
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack'
import { LoginScreen, HomeScreen, RegistrationScreen } from './src/screens'
import {decode, encode} from 'base-64'
if (!global.btoa) { global.btoa = encode }
if (!global.atob) { global.atob = decode }
const Stack = createStackNavigator();
export default function App() {
const [loading, setLoading] = useState(true)
const [user, setUser] = useState(null)
return (
<Stack.Navigator>
{ user ? (
<Stack.Screen name="Home">
{props => <HomeScreen {...props} extraData={user} />}
</Stack.Screen>
) : (
<>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Registration" component={RegistrationScreen} />
</>
)}
</Stack.Navigator>
);
}
Login.js
. . .
const onLoginPress = () => {
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
const uid = response.user.uid
const usersRef = firebase.firestore().collection('users')
usersRef
.doc(uid)
.get()
.then(firestoreDocument => {
if (!firestoreDocument.exists) {
alert("User does not exist anymore.")
return;
}
const user = firestoreDocument.data()
navigation.navigate('Home', {user})
})
.catch(error => {
alert(error)
});
})
.catch(error => {
alert(error)
})
}
. . .
Register.js
const onRegisterPress = () => {
if (password !== confirmPassword) {
alert("Passwords don't match.")
return
}
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then((response) => {
const uid = response.user.uid
const data = {
id: uid,
email,
fullName,
};
const usersRef = firebase.firestore().collection('users')
usersRef
.doc(uid)
.set(data)
.then(() => {
navigation.navigate('Home', {user: data})
})
.catch((error) => {
alert(error)
});
})
.catch((error) => {
alert(error)
});
}
The error is cuased by this line in App.js:
{ user ? (
<Stack.Screen name="Home">
` `{props => <HomeScreen {...props} extraData={user} />}
</Stack.Screen>
) : (
**using these versions:
"#react-navigation/native": "^5.8.10",
"#react-navigation/stack": "^5.12.7",
**I updated "#react-navigation/stack" to 5.12.8
still same erorr
before it throws the error it gives me this warning
"Setting a timer for a long period of time"**
This error is shown when you try to navigate to a screen that does not exist in your stack navigator, I see that you are trying to make navigation based on user data while this user value remains not updated null, as a result Home screen will not be added to the stack and react-navigation will show you an error.
I suggest we sort the code first and separate AppNavigation into a new JSX script as follows:
import 'react-native-gesture-handler';
import React, { useEffect, useState } from 'react'
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack'
import { LoginScreen, HomeScreen, RegistrationScreen } from './src/screens'
import {decode, encode} from 'base-64'
if (!global.btoa) { global.btoa = encode }
if (!global.atob) { global.atob = decode }
const Stack = createStackNavigator();
export default function App() {
return (
<AppNavigation/>
);
}
Then inside AppNavigation:
Since you are using firebase there is no need to pass user data to the next screen, you can use fire.auth().currentUser to get your current user data.
Also is have used fire.auth().onAuthStateChanged to listen for user state changes so whenever the user logs in it should get updated instantly and replace Login & Register screens with Login screen without the need for calling navigation.navigate('Home', {user}) inside onLoginPress
Last thing you have to create a splash screen to be shown to the user until verifing your user is logged in or not.
import React, {Component} from 'react';
import { LoginScreen, HomeScreen, RegistrationScreen } from '../src/screens'
import { createStackNavigator } from '#react-navigation/stack';
import { NavigationContainer } from '#react-navigation/native';
import fire from '../config/Fire';
import Splash from '../components/common/Splash';
const Stack = createStackNavigator();
class AppNavigation extends Component {
constructor(props){
super(props);
this.state = {
loggedIn : null,
}
}
componentDidMount(){
this.authListner();
}
componentWillUnmount(){
// unsubscribe
this.setState({
loggedIn:null,
})
}
authListner(){
fire.auth().onAuthStateChanged(user => {
//console.log(user)
if (!user){
this.setState({
loggedIn:false,
})
} else if (user){
this.setState({
loggedIn:true,
})
}
}
)
}
render = () => {
if (this.state.loggedIn == null) {
// We haven't finished checking for the token yet
return <Splash />;
}
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Login">
{
this.state.loggedIn == false ? (
<>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Registration" component={RegistrationScreen} />
</>
) :
<>
<Stack.Screen name="Home" component={HomeScreen}/>
</>
}
</Stack.Navigator>
</NavigationContainer>
)
}
export default AppNavigation;
I want to separate my navigation code into separate files, 3 files to be precise and I want to load a stack navigator depending on the role of the user, which I will receive on the logging in process (like 3 separate apps), the problem is that if I would've kept the file stacked together it'll be very huge file
here is my main navigation file
import * as React from 'react';
// Apps Navigators
import VendorNavigator from '#vendor-app/routes';
// utils
import Api from '#utils/api';
import AsyncStorage from '#react-native-community/async-storage';
import Container from '#components/container';
// Icons
import Ionicons from 'react-native-vector-icons/Ionicons';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
// contexts
const AuthContext = React.createContext(null);
// navigators
import {createStackNavigator} from '#react-navigation/stack';
import {createBottomTabNavigator} from '#react-navigation/bottom-tabs';
// import Screens
// Public Screens
import LoginScreen from '#layouts/auth/login';
import PasswordReset from '#layouts/auth/password-reset';
import PasswordResetCode from '#layouts/auth/password-reset-code';
import NewPassword from '#layouts/auth/new-password';
const Navigator = () => {
const AuthStack = createStackNavigator();
const Authenticated = createStackNavigator();
const UnAuthenticated = createStackNavigator();
React.useEffect(() => {
const bootstrapAsync = async () => {
await verifyUser();
let token = await AsyncStorage.getItem('access_token');
if (!token)
dispatch({ type: 'SIGN_OUT' })
else
dispatch({ type: 'SIGN_IN', token: token})
}
bootstrapAsync()
}, [])
const verifyUser = async () => {
let response = await Api.get('/auth/verify-token');
if (response && response.status_code == 200) {
let token = await AsyncStorage.getItem('access_token');
dispatch({ type: 'SIGN_IN', token: token })
} else {
dispatch({ type: 'SIGN_OUT' })
}
}
const authContext = React.useMemo(
() => ({
signIn: async (email_address, password, app) => {
let response = await Api.post(`auth/login`, {
email_address: email_address,
password: password,
});
if (response && response.status_code == 200) {
await AsyncStorage.setItem('access_token', response.data.access_token)
await AsyncStorage.setItem('refresh_token', response.data.refresh_token)
await AsyncStorage.setItem('user', JSON.stringify(response.data.user))
dispatch({ type: 'SIGN_IN', token: response.data.access_token });
}
return response;
},
signOut: async () => {
await AsyncStorage.removeItem('access_token')
await AsyncStorage.removeItem('refresh_token')
await AsyncStorage.removeItem('user')
dispatch({ type: 'SIGN_OUT' })
},
signUp: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'SIGN_IN':
return {
...prevState,
is_authenticated: true,
token: action.token,
is_loading: false
};
case 'SIGN_OUT':
return {
...prevState,
is_authenticated: false,
token: null,
is_loading: false
}
}
},
{
is_loading: true,
is_authenticated: false,
token: null
}
)
function AuthenticatedStack() {
return (
<Authenticated.Navigator>
{/*
The idea is to have a conditional route here depending on the user's role, for example:
{
user.role == 'vendor'
? <Authenticated.Screen options={{headerShown: false}} name={'Tabs'} component={VendorNavigator} />
: user.role == 'collector'
? <Authenticated.Screen options={{headerShown: false}} name={'Tabs'} component={CollectorNavigator} />
: null
}
*/}
<Authenticated.Screen options={{headerShown: false}} name={'Tabs'} component={VendorNavigator} />
</Authenticated.Navigator>
)
}
function UnAuthenticatedStack() {
return (
<UnAuthenticated.Navigator
screenOptions={{
headerShown: false
}}>
{/* Login and password resets */}
<UnAuthenticated.Screen name={'Login'} component={LoginScreen} />
<UnAuthenticated.Screen name={'PasswordReset'} component={PasswordReset} />
<UnAuthenticated.Screen name={'PasswordResetCode'} component={PasswordResetCode} />
<UnAuthenticated.Screen name={'NewPassword'} component={NewPassword} />
</UnAuthenticated.Navigator>
)
}
return (
<Container blockRender is_loading={state.is_loading}>
<AuthContext.Provider value={authContext}>
<AuthStack.Navigator>
{
state.is_authenticated
? <AuthStack.Screen options={{headerShown: false}} name={'Home'} component={AuthenticatedStack} />
: <AuthStack.Screen options={{headerShown: false}} name={'Login'} component={UnAuthenticatedStack} />
}
</AuthStack.Navigator>
</AuthContext.Provider>
</Container>
)
}
export {AuthContext};
export default Navigator;
and here is my nested stack
import React from 'react';
// utils
import Api from '#utils/api';
import AsyncStorage from '#react-native-community/async-storage';
import Container from '#components/container';
// Icons
import Ionicons from 'react-native-vector-icons/Ionicons';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
// navigators
import {createStackNavigator} from '#react-navigation/stack';
import {createBottomTabNavigator} from '#react-navigation/bottom-tabs';
// import Screens
import ListScreen from '#layouts/list';
import RetailerScreen from '#layouts/list/retailer';
import RetailerInvoiceScreen from '#layouts/list/retailer/invoice';
import CreateInvoiceScreen from '#layouts/list/retailer/create-invoice';
import CreateUserScreen from '#layouts/list/create-user';
import MapScreen from '#layouts/map';
import SettingsScreen from '#layouts/settings';
function VendorNavigator() {
const tabNavigatorConfig = ({ route }) => ({
tabBarIcon: ({focused, color, size}) => {
let iconName;
if (route.name == 'Retailers')
iconName = focused ? 'store' : 'store-outline';
else if (route.name == 'Map')
iconName = focused ? 'map-sharp' : 'map-outline';
else if (route.name == 'Settings')
iconName = focused ? 'settings' : 'settings-sharp';
return route.name == 'Retailers' ? (
<MaterialCommunityIcons name={iconName} color={color} size={size} />
) : (
<Ionicons name={iconName} color={color} size={size} />
)
}
})
const TabNavigator = createBottomTabNavigator();
const RetailersListStack = createStackNavigator();
function RetailersScreensAndSubScreens() {
return (
<RetailersListStack.Navigator
screenOptions={{
headerShown: false
}}>
<RetailersListStack.Screen name={'Retailers'} component={ListScreen} />
<RetailersListStack.Screen name={'Retailer'} component={RetailerScreen} />
<RetailersListStack.Screen name={'RetailerInvoice'} component={RetailerInvoiceScreen} />
<RetailersListStack.Screen name={'CreateRetailer'} component={CreateInvoiceScreen} />
<RetailersListStack.Screen name={'CreateUser'} component={CreateUserScreen} />
</RetailersListStack.Navigator>
)
}
return (
<TabNavigator.Navigator
screenOptions={tabNavigatorConfig}
tabBarOptions={{
activeTintColor: '#33A788',
inactiveTintColor: '#000000',
}}>
<TabNavigator.Screen name={'Retailers'} component={RetailersScreensAndSubScreens} />
<TabNavigator.Screen name={'Map'} component={MapScreen} />
<TabNavigator.Screen name={'Settings'} component={SettingsScreen} />
</TabNavigator.Navigator>
)
}
export default VendorNavigator;
the app loads initially in a perfect way, but as soon as I navigate, it triggers an infinite re-render.
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>
);
};