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>
);
};
Related
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 a simple next.js app, that allows a user to login via a login-page. The login is done via a graphql-api. I'm using the react context-API and after the user has logged in succesfully I'm updating the context. Afterwards I would like to redirect the user to a dashboard-page. It actually works as intended, however always (and only) on the second login (= login, logout, login again) I get the following Error in my console:
I understand the error (or warning?), but I don't know why it occurs or what I'm doing wrong. Any suggestion to point me in the right direction is much appreciated.
Here's my code:
auth-context.ts
import { createContext, useEffect, useState } from 'react';
//type-definitions removed for better readability
const AuthContext = createContext<AuthContext>({
isAuthenticated: false,
isAdmin: false,
userId: '',
loginSuccessHandler: () => {},
logoutHandler: () => {},
});
const AuthContextProvider = ({ children }: AuthContextProviderProps) => {
const [authData, setAuthData] = useState<AuthData>({
isAuthenticated: false,
isAdmin: false,
userId: null,
});
useEffect(() => {
const storedIsAuthenticated = localStorage.getItem('isAuthenticated');
const storedUserId = localStorage.getItem('userId');
const storedRole = localStorage.getItem('role');
if (storedIsAuthenticated === '1') {
setAuthData({
isAuthenticated: true,
userId: storedUserId,
isAdmin: storedRole === 'ADMIN',
});
}
}, []);
const loginSuccessHandler = (isAuthenticated: boolean, userId: string, role: string) => {
localStorage.setItem('isAuthenticated', '1');
localStorage.setItem('userId', userId);
localStorage.setItem('role', role);
setAuthData({
isAuthenticated: isAuthenticated,
userId: userId,
isAdmin: role === 'ADMIN',
});
};
const logoutHandler = () => {
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('userId');
localStorage.removeItem('role');
setAuthData({
isAuthenticated: false,
userId: null,
isAdmin: false,
});
};
return (
<AuthContext.Provider
value={{
isAuthenticated: authData.isAuthenticated,
isAdmin: authData.isAdmin,
userId: authData.userId,
loginSuccessHandler,
logoutHandler,
}}
>
{children}
</AuthContext.Provider>
);
};
export { AuthContext, AuthContextProvider };
pages/login.tsx
import { gql, useLazyQuery } from '#apollo/client';
import { useRouter } from 'next/router';
import { SyntheticEvent, useContext, useEffect, useRef } from 'react';
import TextInput from '../components/Input/TextInput';
import { AuthContext } from '../contexts/auth-context';
import type { NextPage } from 'next';
const Login: NextPage = () => {
const emailInputRef = useRef<HTMLInputElement>(null);
const passwordInputRef = useRef<HTMLInputElement>(null);
const authCtx = useContext(AuthContext);
const router = useRouter();
const LOGIN_QUERY = gql`
query LoginQuery($email: String!, $password: String!) {
login(email: $email, password: $password) {
userId
token
role
}
}
`;
const [login, { loading, error }] = useLazyQuery(LOGIN_QUERY);
const submitButtonHandler = (event: SyntheticEvent) => {
event.preventDefault();
const email = emailInputRef?.current?.value || null;
const password = passwordInputRef?.current?.value || null;
login({
variables: { email, password },
onCompleted(data) {
authCtx.loginSuccessHandler(true, data.login.userId, data.login.role);
},
onError(error) {
authCtx.logoutHandler();
},
});
};
/*
The following Effect leads to the warning/error, when I remove it, the error disappears.
However, that's not what I want, I want it to work like this
*/
useEffect(() => {
authCtx.isAuthenticated
? authCtx.isAdmin
? router.push('/admin')
: router.push('/my-account')
: router.push('/login');
}, [authCtx.isAuthenticated, authCtx.isAdmin]);
return (
<div>
<form>
<h1>Login</h1>
{loading && <p>Loading</p>}
{error && <p>Error: {error.message}</p>}
<TextInput type="email" id="email" ref={emailInputRef} />
<TextInput type="password" id="password" ref={passwordInputRef} />
<button onClick={submitButtonHandler} >
Submit
</button>
</form>
</div>
);
};
export default Login;
In _app.tsx I use my AuthContextProvider like this:
import '../styles/globals.scss';
import Layout from '../components/Layout/Layout';
import type { AppProps } from 'next/app';
import { AuthContextProvider } from '../contexts/auth-context';
import { ApolloProvider } from '#apollo/client';
import apolloClient from '../lib/apollo-client';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ApolloProvider client={apolloClient}>
<AuthContextProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</AuthContextProvider>
</ApolloProvider>
);
}
export default MyApp;
Thanks to #Anthony Ma's comment, I found that the onCompleted-Handler in login.tsx seemed to be the issue here. It updates the authContext while still being in the rendering-process of the login-component.
I changed my login.tsx file to add the data-response from my graphql-API to a state-object and then use an effect with that state-object as dependency (Find -> changed comments in the code below to see all changes).
Updated login.tsx:
import { gql, useLazyQuery } from '#apollo/client';
import { useRouter } from 'next/router';
/*
-> changed: add useState to the list of imports
*/
import { SyntheticEvent, useContext, useEffect, useRef, useState } from 'react';
import TextInput from '../components/Input/TextInput';
import { AuthContext } from '../contexts/auth-context';
import type { NextPage } from 'next';
//type definition "AuthData" goes here
const Login: NextPage = () => {
const emailInputRef = useRef<HTMLInputElement>(null);
const passwordInputRef = useRef<HTMLInputElement>(null);
const authCtx = useContext(AuthContext);
const router = useRouter();
/*
-> changed: Add authData State
*/
const [authData, setAuthData] = useState<AuthData>({
isAuthenticated: false,
userId: '',
role: '',
token: '',
});
const LOGIN_QUERY = gql`
query LoginQuery($email: String!, $password: String!) {
login(email: $email, password: $password) {
userId
token
role
}
}
`;
const [login, { loading, error }] = useLazyQuery(LOGIN_QUERY);
const submitButtonHandler = (event: SyntheticEvent) => {
event.preventDefault();
const email = emailInputRef?.current?.value || null;
const password = passwordInputRef?.current?.value || null;
login({
variables: { email, password },
/*
-> changed: Important: Set fetchPolicy to 'network-only' to prevent
caching of the response (otherwise the response wont change on
every login request and therefore the state wont change resulting
in the effect (see below) not being triggered.
*/
fetchPolicy: 'network-only',
onCompleted(data) {
/*
-> changed: add response data to the new state object "authData"
*/
setAuthData({
isAuthenticated: true,
userId: data.login.userId,
role: data.login.role,
token: data.login.token,
});
},
onError(error) {
authCtx.logoutHandler();
},
});
};
/*
-> changed: added this new effect with the authData state object as
dependency
*/
useEffect(() => {
authCtx.loginSuccessHandler({
isAuthenticated: authData.isAuthenticated,
userId: authData.userId,
role: authData.role,
token: authData.token,
});
}, [authData]);
useEffect(() => {
authCtx.isAuthenticated
? authCtx.isAdmin
? router.push('/admin')
: router.push('/my-account')
: router.push('/login');
}, [authCtx.isAuthenticated, authCtx.isAdmin]);
return (
<div>
<form>
<h1>Login</h1>
{loading && <p>Loading</p>}
{error && <p>Error: {error.message}</p>}
<TextInput type="email" id="email" ref={emailInputRef} />
<TextInput type="password" id="password" ref={passwordInputRef} />
<button onClick={submitButtonHandler} >
Submit
</button>
</form>
</div>
);
};
export default Login;
Could someone please let me know why the state isn't being updated from the reducer? The useEffect(()=>{}) isn't being triggered when the state is being returned from the reducer. I have validated the correct information is being passed to the return, but nothing can be seen from the LoginScreen.
Context Script
import React, { createContext, useReducer } from "react";
import userReducer from "./UserReducer";
export const UserContext = createContext();
const initialState = {
userData: [],
isLoggedIn: false,
isAdmin: false,
isEmployee: false,
errorMessage: [{ success: false, statusCode: 0, error: null }],
};
const UserContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, initialState);
const registerUser = (user) =>
dispatch({ type: "REGISTER_USER", payload: user });
const loginUser = (user) => dispatch({ type: "LOGIN_USER", payload: user });
const deleteUser = (user) => dispatch({ type: "DELETE_USER", payload: user });
const updateUser = (user) => dispatch({ type: "UPDATE_USER", payload: user });
const contextValues = {
...state,
registerUser,
loginUser,
deleteUser,
updateUser,
};
return (
<UserContext.Provider value={contextValues}>
{children}
</UserContext.Provider>
);
};
export default UserContextProvider;
Reducer Script
import axios from "axios";
axios.defaults.withCredentials = true;
const userReducer = (state = {}, action) => {
let config = {
header: {
"Content-Type": "application/json",
},
};
switch (action.type) {
case "REGISTER_USER":
break;
case "LOGIN_USER":
console.log(state);
const email = action.payload.email;
const password = action.payload.password;
axios
.post("/api/user/login", { email, password }, config)
.then((response) => {
if (response.data.success) {
// localStorage.setItem("authToken", response.data.authToken);
state.userData = response.data.user;
state.isLoggedIn = true;
if (response.data.user.role === 9) {
state.isAdmin = true;
state.isEmployee = true;
} else {
state.isAdmin = false;
state.isEmployee = false;
}
}
})
.catch((error) => {
state.errorMessage = {
success: error.response.data.success,
statusCode: error.response.status,
message: error.response.data.error,
};
});
return {
...state,
userData: [state.userData],
isLoggedIn: state.isLoggedIn,
isAdmin: state.isAdmin,
isEmployee: state.isEmployee,
errorMessage: [state.errorMessage],
};
default:
return state;
}
};
export default userReducer;
Login Form
import { useState, useEffect, useContext } from "react";
import { Link } from "react-router-dom";
import {
Button,
Form,
Grid,
Message,
Segment,
Image,
Container,
} from "semantic-ui-react";
//Custom Imports
import "./LoginScreen.css";
import Logo from "../../../img/logo.png";
//Context
import { UserContext } from "../../context/UserContext";
const LoginScreen = ({ history }) => {
const { userData, loginUser, isLoggedIn, errorMessage, clearErrorMessage } =
useContext(UserContext);
const [user, setUser] = useState({ email: "", password: "" });
const [error, setError] = useState("");
useEffect(() => {
console.log(errorMessage);
if (localStorage.getItem("authToken")) {
history.push("/dashboard");
}
}, [history]);
useEffect(() => {
if (isLoggedIn) {
console.log(userData);
console.log("User is Logged in");
// history.push("/");
}
if (!errorMessage.success && errorMessage.error != null) {
console.log(errorMessage);
setError(errorMessage.message);
setTimeout(() => {
setError("");
}, 5000);
}
}, [userData, errorMessage, isLoggedIn]);
return (
<Container className="login-container">
<Grid
textAlign="center"
style={{ height: "100vh" }}
verticalAlign="middle"
>
<Grid.Column style={{ maxWidth: 450 }}>
<Image src={Logo} className="login-logo" />
<Form size="large" onSubmit={() => loginUser(user)}>
<Segment stacked>
<Form.Input
fluid
icon="user"
iconPosition="left"
placeholder="Email Address"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
/>
<Form.Input
fluid
icon="lock"
iconPosition="left"
placeholder="Password"
value={user.password}
type="password"
onChange={(e) => setUser({ ...user, password: e.target.value })}
/>
{error && <span>{error}</span>}
<Button color="blue" fluid size="large" type="submit">
Login
</Button>
</Segment>
</Form>
<Message>
Don't have an account? <Link to="/register">Sign Up</Link>
</Message>
</Grid.Column>
</Grid>
</Container>
);
};
export default LoginScreen;
Refactor your login function like this
const loginUser({ email, password }) => {
let config = {
header: {
"Content-Type": "application/json",
},
};
axios
.post("/api/user/login", { email, password }, config)
.then((response) => {
if (response.data.success) {
dispatch({ type: 'LOGIN_SUCCESS', payload: response.data });
}
})
.catch((error) => {
dispatch({ type: 'LOGIN_FAILED', payload: error });
});
}
and then your reducer
...
switch(action.type) {
...
case 'LOGIN_SUCCESS':
// return here a new object
// do not mutate the state (state.something = something) is not allowed
...
case 'LOGIN_FAILED':
// handle error
}
Prerequisite Reducer Concepts
Redux and useReducer use reducer like (previousState, action) => newState.
The reducer should be a 'pure' function as in this document. The promises, api calls should not be use inside reducers.
The problem:
Because you call api/promise inside the reducer. The reducer function returns the value before the promise finish. So when the promise finishes, nothing happen.
// A will be return before B, C are going to call
case "LOGIN_USER":
promiseFn()
.then(/* B */ ...)
.catch(/* C */ ...)
// A
return {
...
}
Solution:
Separate the non-pure calls from the reducer. And put them in the other code blocks (like inside hooks, event handlers...).
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.
My react native application is showing a blank screen after running it
with redux for state management.
The HomeScreen functional component could also be failing to render the Flatlist properly.
Maybe I am using the useSelector hook wrongly or the Redux actions aren't correct. The navigation is handled by the react navigation.
import {
GET_CURRENCY,
GET_CURRENCY_SUCCESS,
GET_CURRENCY_FAILURE
} from "../ActionTypes";
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", "{BearerToken} ");
const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
export const getCurrency = () =>{
return{
type: GET_CURRENCY,
}
}
export const currencySuccess = instruments => {
return{
type: GET_CURRENCY_SUCCESS,
payload: instruments
}}
export const currencyFailure = error => {
return{
type: GET_CURRENCY_FAILURE,
payload:
error
}
}
export const fetchCurrency =() => {
return dispatch => {
dispatch(getCurrency())
fetch("https://api-fxpractice.oanda.com/v3/instruments/EUR_USD/candles?price=M", requestOptions)
// eslint-disable-next-line no-unused-vars
.then(response => response.data)
.then(instruments =>{
dispatch (currencySuccess(instruments))
})
// eslint-disable-next-line no-undef
.catch (error => {
const errorMsg = error.message
dispatch(currencyFailure(errorMsg))
})
}
}
export default fetchCurrency
Homescreen
import React, {useEffect} from 'react'
import { FlatList, ActivityIndicator, View, Text, SafeAreaView} from 'react-native'
import Instrument from '../../../components/Instrument'
import styles from './styles'
import {useSelector, useDispatch} from 'react-redux'
import fetchCurrency from '../../Redux/Actions/currencyActions'
function HomeScreen () {
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchCurrency())
}, []);
const { instruments, loading, error} = useSelector(state => state.reducer
);
{error &&
<View><Text>Error</Text></View>
}
{loading && <ActivityIndicator size='large' />}
return(
<SafeAreaView
style={styles.container}
>
<FlatList
data={instruments}
numColumns={1}
contentContainerStyle = {styles.list}
keyExtractor = {({item}) => item.toString() }
renderItem = {(item, index) => (
<Instrument currency={item} />
)}
/>
</SafeAreaView>
);
}
export default HomeScreen
reducer
import {
GET_CURRENCY,
GET_CURRENCY_SUCCESS,
GET_CURRENCY_FAILURE
} from '../ActionTypes'
const initialState = {
instruments: [],
loading: false,
error: null
}
const reducer = (state= initialState, action) => {
switch(action.type){
case GET_CURRENCY:
return {...state, loading: true}
case GET_CURRENCY_SUCCESS:
return {...state, loading: false, instruments: action.payload.instruments }
case GET_CURRENCY_FAILURE:
return { ...state, loading: false, error: action.payload}
default:
return state
}
}
export default reducer;
You need to return the components that you create in the error or loading case.
if (error)
return (<View><Text>Error</Text></View>)
else if (loading)
return (<ActivityIndicator size='large' />)
else
return (<SafeAreaView
...
The coding pattern
{error &&
<View><Text>Error</Text></View>
}
That you have in your code can only be used inside a JSX component to render another component conditionally. For example
<OuterComponent>
{flag && (<OptionalComponent/>)}
</OuterComponent>
It works because the curly braces in JSX contain regular JavaScript code, and the result of flag && (<OptionalComponent/>) is either false or <OptionalComponent> and for React false simply doesn't generate any output.