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.
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.
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.
I'm pretty new to Redux, but am trying to add it to my existing project and am getting an infinite refresh loop back to my /login page. I don't see an immediate error and can't locate where the issue might be coming from. I think the loop might be coming from the render in App.js that pulls in the Login component, but can't seem to pinpoint it. I'd really appreciate any help with this one!
App.js:
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import './App.css';
import MuiThemeProvider from '#material-ui/core/styles/MuiThemeProvider';
import createMuiTheme from '#material-ui/core/styles/createMuiTheme';
import themeFile from './util/theme';
import jwtDecode from 'jwt-decode';
// Redux
import { Provider } from 'react-redux';
import store from './redux/store';
// Components
import Navbar from './components/Navbar';
import AuthRoute from './util/AuthRoute';
// Pages
import home from './pages/home';
import login from './pages/login';
import signup from './pages/signup';
const theme = createMuiTheme(themeFile);
let authenticated;
const token = localStorage.FBIdToken;
if(token){
const decodedToken = jwtDecode(token);
if(decodedToken.exp * 1000 < Date.now()){
window.location.href='/login'
authenticated = false;
} else {
authenticated = true;
};
}
class App extends Component {
render() {
return (
<MuiThemeProvider theme={theme}>
<Provider store={store}>
<Router>
<Navbar/>
<div className="container">
<Switch>
<Route exact path="/" component={home}/>
<AuthRoute exact path="/login" component={login} authenticated={authenticated}/>
<AuthRoute exact path="/signup" component={signup} authenticated={authenticated}/>
</Switch>
</div>
</Router>
</Provider>
</MuiThemeProvider>
);
}
}
export default App;
Login page:
import React, { Component } from 'react'
import withStyles from '#material-ui/core/styles/withStyles'
import PropTypes from 'prop-types'
import AppIcon from '../images/micrologo.png'
import { Link } from 'react-router-dom'
//MUI Stuff
import Grid from '#material-ui/core/Grid';
import Typography from '#material-ui/core/Typography'
import TextField from '#material-ui/core/TextField'
import Button from '#material-ui/core/Button'
import CircularProgress from '#material-ui/core/CircularProgress'
//Redux stuff
import { connect } from 'react-redux';
import { loginUser } from '../redux/actions/userActions';
const styles = (theme) => ({
...theme.spreadThis
})
class login extends Component {
constructor(){
super();
this.state = {
email: '',
password: '',
errors: {}
}
}
componentWillReceiveProps(nextProps){
if(nextProps.UI.errors){
this.setState({ errors: nextProps.UI.errors })
}
}
handleSubmit = (event) => {
event.preventDefault();
const userData = {
email: this.state.email,
password: this.state.password
};
this.props.loginUser(userData, this.props.history)
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
})
}
render() {
const { classes, UI: { loading } } = this.props;
const { errors } = this.state;
return (
<Grid container className={classes.form}>
<Grid item sm/>
<Grid item sm>
<img src={AppIcon} alt="micrologo" className={classes.image}/>
<Typography variant="h3" className={classes.pageTitle}>
Login
</Typography>
<form noValidate onSubmit={this.handleSubmit}>
<TextField
id="email"
name="email"
type="email"
label="Email"
className={classes.textField}
helperText={errors.email}
error={errors.email ? true : false}
value={this.state.email}
onChange={this.handleChange}
fullWidth
/>
<TextField
id="password"
name="password"
type="password"
label="Password"
className={classes.textField}
helperText={errors.password}
error={errors.password ? true : false}
value={this.state.password}
onChange={this.handleChange}
fullWidth
/>
{errors.general && (
<Typography variant="body2" className={classes.customError}>
{errors.general}
</Typography>
)}
<Button
type="submit"
variant="contained"
color="primary"
className={classes.button}
disabled={loading}
>
Login
{loading && (
<CircularProgress size={30} className={classes.progress}/>
)}
</Button>
<br />
<small>Don't have an account? Sign up <Link to="/signup">here</Link>
</small>
</form>
</Grid>
<Grid item sm/>
</Grid>
)
}
}
login.propTypes = {
classes: PropTypes.object.isRequired,
loginUser: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
UI: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
user: state.user,
UI: state.UI
});
const mapActionsToProps = {
loginUser
}
export default connect(mapStateToProps, mapActionsToProps)(withStyles(styles)(login));
User Details:
import { SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI } from '../type';
import axios from 'axios'
export const loginUser = (userData, history) => (dispatch) => {
dispatch({ type: LOADING_UI});
axios
.post('/login', userData)
.then((res) => {
const FBIdToken = `Bearer ${res.data.token}`;
localStorage.setItem('FBIdToken', FBIdToken);
axios.defaults.headers.common['Authorization'] = FBIdToken;
dispatch(getUserData());
dispatch({ type: CLEAR_ERRORS });
history.push('/');
})
.catch((err) => {
dispatch({
type: SET_ERRORS,
payload: err.response.data
})
})
}
export const getUserData = () => (dispatch) => {
axios.get('/user')
.then((res) => {
dispatch({
type: SET_USER,
payload: res.data
})
})
.catch((err) =>
console.log(err)
)
}
EDIT: After making changes to my User Actions file that references my User Reducer, could it possibly be something to do with the User Reducer code?
import { SET_USER, SET_ERRORS, CLEAR_ERRORS, LOADING_UI, SET_AUTHENTICATED, SET_UNAUTHENTICATED } from '../type';
const initialState = {
authenticated: false,
credentials: {},
likes: [],
notifications: []
};
export default function(state = initialState, action){
switch(action.type){
case SET_AUTHENTICATED:
return {
...state,
authenticated: true
};
case SET_UNAUTHENTICATED:
return initialState
case SET_USER:
return {
authenticated: true,
...action.payload
};
default:
return state;
}
}
EDIT 2: Adding copy > copy fetch code from network tab:
fetch("http://localhost:3000/login", {
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "en-US,en;q=0.9",
"if-none-match": "W/\"717-3FVndTj2FHm3TgZjXTrLARSY62Q\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "same-origin",
"upgrade-insecure-requests": "1"
},
"referrer": "http://localhost:3000/login",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
});
You can try the following:
export const loginUser = (userData, history) => (
dispatch
) => {
dispatch({ type: LOADING_UI });
axios
.post("/login", userData)
.then((res) => {
const FBIdToken = `Bearer ${res.data.token}`;
localStorage.setItem("FBIdToken", FBIdToken);
axios.defaults.headers.common[
"Authorization"
] = FBIdToken;
//wait for getUserData to finish
return getUserData()(dispatch);
})
.then(() => {
//getUserData is finished
dispatch({ type: CLEAR_ERRORS });
history.push("/");
})
.catch((err) => {
dispatch({
type: SET_ERRORS,
payload: err.response.data,
});
});
};
export const getUserData = () => (dispatch) => {
//return a promise so loginUser action can wait for
// it to finish
return axios.get("/user").then((res) => {
dispatch({
type: SET_USER,
payload: res.data,
});
});
//if you catch it then the catch that set errors is
// pointless
// .catch((err) => console.log(err));
};
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;