Authentication flows | React Navigation 6 - How to get webToken? - react-native

When I learn authentication flow of react navigation 6.0, I read the sample code which used redux, it used dummy token, but In my react native project, I have to get the real token from server, So I tried to add some code in the sample project.
import * as React from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { useMutation, gql } from '#apollo/client';
import {
ApolloClient,
ApolloProvider,
createHttpLink,
InMemoryCache
} from '#apollo/client';
import { setContext } from 'apollo-link-context';
const SIGNIN_USER = gql`
mutation signIn($email: String!, $password: String!) {
signIn(email: $email, password: $password)
}
`;
// for example, my server address
const API_URI='https://xxxx.herokuapp.com/api';
const cache = new InMemoryCache();
const client = new ApolloClient({
uri: API_URI,
cache: new InMemoryCache()
});
const AuthContext = React.createContext();
function SplashScreen() {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
function HomeScreen() {
const { signOut } = React.useContext(AuthContext);
return (
<View>
<Text>Signed in!</Text>
<Button title="Sign out" onPress={signOut} />
</View>
);
}
function SignInScreen() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
return (
<View>
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Sign in" onPress={() => signIn({ username, password })} />
</View>
);
}
const Stack = createStackNavigator();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
// Restore token stored in `SecureStore` or any other encrypted storage
// userToken = await SecureStore.getItemAsync('userToken');
} catch (e) {
// Restoring token failed
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async ({username, password}) => {
useMutation(SIGNIN_USER, {
variables:{
email: username,
password: password
},
onCompleted: data => {
console.log(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 `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: data.signIn });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
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 `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<ApolloProvider client={client}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
// We haven't finished checking for the token yet
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.userToken == null ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
// When logging out, a pop animation feels intuitive
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</ApolloProvider>
</AuthContext.Provider>
);
}
And I get the error,
WARN Possible Unhandled Promise Rejection (id: 0):
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
I searched it, it is because I use hooks in useMemo.
https://reactjs.org/warnings/invalid-hook-call-warning.html
To avoid confusion, it’s not supported to call Hooks in other cases:
πŸ”΄ Do not call Hooks in class components.
πŸ”΄ Do not call in event handlers.
πŸ”΄ Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
But, I have to send some data to server to get a token, and save token by Securestore or something else, If I can't write this section here, where?
I have no idea about where to write this section of code. My understanding to Redux in this situation:
when a user click button of 'sign in', call the function of signIn which is declared in authContext, useMemo, in use useMemo, get the token, save the token, then dispatch to notify data change, then go to the reducer.

After read a basic tutorial of redux, I have finished that by:
In SignInScreen, get the web token by 'login' function, which is triggered by the button of 'Sign in'
After execute it successfully, invoke the 'signIn' to update the state(by call dispatch).
I was confused with the name of 'signIn' function before, which is the same with my eventHandler function of 'signIn' button in SignInScreen. Actually, they are in duty of different logic.
import * as React from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { useMutation, gql } from '#apollo/client';
import {
ApolloClient,
ApolloProvider,
createHttpLink,
InMemoryCache
} from '#apollo/client';
import { setContext } from 'apollo-link-context';
const SIGNIN_USER = gql`
mutation signIn($email: String!, $password: String!) {
signIn(email: $email, password: $password)
}
`;
const API_URI='https://jseverywhere.herokuapp.com/api';
const cache = new InMemoryCache();
const client = new ApolloClient({
uri: API_URI,
cache: new InMemoryCache()
});
const AuthContext = React.createContext();
function SplashScreen() {
return (
<View>
<Text>Loading...</Text>
</View>
);
}
function HomeScreen() {
const { signOut } = React.useContext(AuthContext);
return (
<View>
<Text>Signed in!</Text>
<Button title="Sign out" onPress={signOut} />
</View>
);
}
function SignInScreen() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
const { signIn } = React.useContext(AuthContext);
const [logIn, {loading, error}] = useMutation(SIGNIN_USER, {
variables:{
email: username,
password: password
},
onCompleted: data => {
console.log(data);
signIn(data);
}
});
if(loading)
return <Text>Loading...</Text>;
if(error)
return <Text>Error--{error.message}</Text>;
return (
<View>
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button title="Sign in" onPress={() => logIn()} />
</View>
);
}
const Stack = createStackNavigator();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
// Restore token stored in `SecureStore` or any other encrypted storage
// userToken = await SecureStore.getItemAsync('userToken');
} catch (e) {
// Restoring token failed
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.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 `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: data.signIn });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
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 `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<ApolloProvider client={client}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
// We haven't finished checking for the token yet
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.userToken == null ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
// When logging out, a pop animation feels intuitive
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</ApolloProvider>
</AuthContext.Provider>
);
}

Related

Move code for React-Context to a dedicated file and using it

I followed the tutorial from react-navigation for the authentication context:
https://reactnavigation.org/docs/auth-flow/
But now i want to move my context out of the App.js in something like AuthContext.js
but i dont succeed in using it in App.js, but there is no tutorial for i tried multiple times but it always break something, here's my code:
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';
import { NavigationContainer } from "#react-navigation/native";
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
import * as SecureStore from 'expo-secure-store';
import * as React from 'react';
import SignInScreen from "./screens/SignInScreen";
import SignUpScreen from "./screens/SignUpScreen";
import HomeScreen from "./screens/HomeScreen";
import SplashScreen from "./screens/SplashScreen";
import { getTokens } from "./services/AuthServices";
import "react-native-gesture-handler";
const Stack = createBottomTabNavigator();
export const AuthContext = React.createContext();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
accessToken: action.accesToken,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
isSignedIn: true,
accessToken: action.token,
};
case 'SIGN_OUT':
const deleteTokens = async () => {
try {
await SecureStore.deleteItemAsync("access_token");
await SecureStore.deleteItemAsync("refresh_token");
} catch (error) {
console.log(error);
}
};
deleteTokens();
return {
...prevState,
isSignout: true,
accessToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
isSignedIn: false,
accessToken: null,
refreshToken: null
}
);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let accessToken;
try {
accessToken = await SecureStore.getItemAsync('access_token');
} catch (e) {
console.log(e)
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', accessToken: accessToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async (data) => {
let tokens = await getTokens(data);
if (tokens.access_token && tokens.refresh_token) {
try {
await SecureStore.setItemAsync("access_token", tokens.access_token);
await SecureStore.setItemAsync("refresh_token", tokens.refresh_token);
} catch (error) {
console.log(error);
}
dispatch({ type: 'SIGN_IN', token: tokens.access_token });
}
console.log(tokens);
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
<Stack.Screen
name="Splash"
component={SplashScreen}
options={{ title: "Splash" }}
/>
) : state.accessToken != null ? (
<>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: "Home" }}
/>
</>
) : (
<>
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{ title: "SignIn" }}
/>
<Stack.Screen
name="SignUp"
component={SignUpScreen}
options={{ title: "SignUp" }}
/>
</>
)}
</Stack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}

How to import AuthContext created in App.js in other components

I am following the procedures mentioned in React navigation Docs for implementing Authentication using Expo secure Store. https://reactnavigation.org/docs/auth-flow/
I tried to minimise their code snippet as short as possible if this is not clear please refer this snippet
//some imports
const AuthContext = React.createContext();
function SplashScreen() {
//some jsx
}
function HomeScreen() {
const { signOut } = React.useContext(AuthContext);
return (
//somejsx
);
}
function SignInScreen() {
const { signIn } = React.useContext(AuthContext);
return (//somejsx);
}
const Stack = createStackNavigator();
export default function App({ navigation }) {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
const authContext = React.useMemo(
() => ({
signIn: () => dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' }),
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: () => dispatch({ type: 'SIGN_IN', token: 'dummy' });
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
<Stack.Screen name="Splash" component={SplashScreen} />
) : state.userToken == null ? (
<Stack.Screen
name="SignIn"
component={SignInScreen}
/>
) : (
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
Now Coming to my Issue, In the given snippet they used the Authcontext they created in the login and register screens directly
const { signOut } = React.useContext(AuthContext);
But in my use case scenario i have to use the signOut function outside the App component, in my signOut screen.
When i tried to use the AuthContext in a separate file, react native is throwing up an error "can't find variable AuthContext". I encoded My App component with the and also provided the value with correct syntax.
I am not sure if i have to import the AuthContext in the login and register components.
Any help would be highly appreciated, Thanks in Advance.
You need to wrap the compenent where you need to use the context, like this as example
<AuthenticationProvider>
<TheComponent/>
</AuthenticationProvider>
You can first create a component called Provider
const Providers: FC = (props) => (
<AuthenticationProvider>
{props.children}
</AuthenticationProvider>
);
export default Providers;
and then pass your component as children
In App.js:
export const AuthContext = React.createContext();
In SignInScreen.js:
import { AuthContext } from 'App.js'

Could not find "store" in the context of "Connect(HomeScreen)". Either wrap the root component in a... or pass a custom React context provider

Check multitude of questioned already asked and but still can't figure this one out.
We are rewriting our authentication layer using
export default AuthContext = React.createContext();
and wrapping it around our AppNavigator
function AppNavigator(props) {
const [state, dispatch] = useReducer(accountReducer, INITIAL_STATE);
const authContext = React.useMemo(
() => ({
loadUser: async () => {
const token = await keychainStorage.getItem("token");
if (token) {
await dispatch({ type: SIGN_IN_SUCCESS, token: token });
}
},
signIn: async (data) => {
client
.post(LOGIN_CUSTOMER_RESOURCE, data)
.then((res) => {
const token = res.data.accessToken;
keychainStorage.setItem("token", token);
dispatch({ type: SIGN_IN_SUCCESS, token: token });
})
.catch((x) => {
dispatch({ type: SIGN_IN_FAIL });
});
},
signOut: () => {
client.delete({
LOGOUT_CUSTOMER_RESOURCE
});
dispatch({ type: SIGN_OUT_SUCCESS });
}
}),
[]
);
console.log("token start", state.token);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer
theme={MyTheme}
ref={(navigatorRef) => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
onStateChange={(state) => {
NavigationService.setAnalytics(state);
}}
>
<AppStack.Navigator initialRouteName="App" screenOptions={hideHeader}>
{state.token != null ? (
<AppStack.Screen name="App" component={AuthMainTabNavigator} />
) : (
<>
<AppStack.Screen name="App" component={MainTabNavigator} />
<AppStack.Screen name="Auth" component={AuthNavigator} />
</>
)}
</AppStack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
export default AppNavigator;
App.js - render fucnction
<Root>
<StoreProvider store={store} context={AuthContext}>
<PersistGate loading={null} persistor={persistor}>
<SafeAreaProvider>
<AppNavigator context={AuthContext}/>
</SafeAreaProvider>
</PersistGate>
</StoreProvider>
</Root>
HomeScreen.js
export default connect(mapStateToProps, mapDispatchToProps, null, { context: AuthContext })(HomeScreen);
But still receiving
Error: Could not find "store" in the context of "Connect(HomeScreen)". Either wrap the root component in a <Provider>, or pass a custom React context provider to <Provider> and the corresponding React context consumer to Connect(HomeScreen) in connect options.
We have gone through the REDUX documentation:
https://react-redux.js.org/using-react-redux/accessing-store#using-the-usestore-hook
Simply can not work out why we are receiving this error.
I'm not sure what you're trying to accomplish here, but this is very wrong:
export default connect(mapStateToProps, mapDispatchToProps, null, { context: AuthContext })(HomeScreen);
It looks like you're mixing up two different things. You're trying to create a context for use with your own auth state, but you're also trying to use that same context instance to override React-Redux's own default context instance. Don't do that! You should not be passing a custom context instance to connect and <Provider> except in very rare situations.
I understand what you are trying to achieve only after reading through your discussion in the comments with #markerikson.
The example from the React Navigation docs creates a context AuthContext in order to make the auth functions available to its descendants. It needs to do this because the state and the dispatch come from the React.useReducer hook so they only exist within the scope of the component.
Your setup is different because you are using Redux. Your state and dispatch are already available to your component through the React-Redux context Provider and can be accessed with connect, useSelector, and useDispatch. You do not need an additional context to store your auth info.
You can work with the context that you already have using custom hooks. Instead of using const { signIn } = React.useContext(AuthContext) like in the example, you can create a setup where you would use const { signIn } = useAuth(). Your useAuth hook can access your Redux store by using the React-Redux hooks internally.
Here's what that code looks like as a hook:
import * as React from 'react';
import * as SecureStore from 'expo-secure-store';
import { useDispatch } from "react-redux";
export const useAuth = () => {
// access dispatch from react-redux
const dispatch = useDispatch();
React.useEffect(() => {
// same as in example
}, []);
// this is the same as the example too
const authContext = useMemo(
() => ({
signIn: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);
// but instead of passing that `authContext` object to a `Provider`, just return it!
return authContext;
}
In your component, which must be inside your React-Redux <Provider>:
function App() {
const { signIn } = useAuth();
const [username, setUsername] = React.useState('');
return (
<Button onPress={() => signIn(username)}>Sign In</Button>
)
}

Logging out of react app results in nullpointer in homescreen

I'm working on a react application with a login page. When the user logs in successfully, the backend returns a user-object (JSON), containing some user properties (id, username, ...) aswel as JWT token. The user-object gets stored in AsyncStorage (so the user only has to log in once).
try {
await AsyncStorage.setItem('authUser', JSON.stringify(data));
} catch (e) {
console.log(e.message);
}
dispatch({ type: 'SIGN_IN', authUser:data});
The state :
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
authUser: action.authUser,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
authUser: action.authUser,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
authUser: null,
};
}
},
{
isLoading: true,
isSignout: false,
authUser: "",
}
);
The second time i retrieve the user-object from the AsyncStorage and pass it to the state.
try {
authUser = await AsyncStorage.getItem('authUser');
} catch (e) {
console.log(e.message);
}
dispatch({ type: 'RESTORE_TOKEN', authUser: JSON.parse(authUser) });
I use a UserContext to pass the authUser to my welcome screen :
<AuthContext.Provider value={authContext}>
<UserContext.Provider value={{authUser: state.authUser }}>
<NavigationContainer>
<RootStack.Navigator mode="modal" headerMode="none">
{state.isLoading ? (
<RootStack.Screen name="Splash" component={SplashScreen} />
) : state.authUser == null ? (
<RootStack.Screen name="Login" component={AuthenticationStackScreen} />
) : (
<RootStack.Screen name="Home" component={HomeScreen} />
)}
</RootStack.Navigator>
</NavigationContainer>
</UserContext.Provider>
</AuthContext.Provider>
In my homescreen I print a welcome message containing the username and a signOut button :
function HomeScreen(props) {
const { signOut } = React.useContext(AuthContext);
const { authUser } = React.useContext(UserContext);
return (
<View style={{ flex:1, alignItems: 'center', justifyContent: 'center', backgroundColor:'cyan' }}>
<Text>Hallo {authUser.username}</Text>
<Button title="Log out" onPress={signOut} />
<Text>HomeScreen </Text>
</View>
);
}
When a user logs in or was already logged in, the Homescreen is shown correctly with the correct username. The problem appears when the logout button is clicked. The signOut method in the authContext gets called, which logs out the user and removes the object from the AsyncStorage :
signOut: async () => {
try {
await AsyncStorage.removeItem('authUser');
} catch (e) {
console.log(e.message);
}
dispatch({ type: 'SIGN_OUT' })
}
The problem :
The state changes to SIGN_OUT which brings me back to the login page. But since i've cleared the user-object, the welcome notification throws an error because {authUser.username} is null:
TypeError: null is not an object (evaluating 'authUser.username')
I didn't expect the WelcomeScreen to be re-rendered when clicking the sign-out button. Did i mess up the structure of my application, or why is this? Any help on how to fix this would be greatly appreciated. Thanks in advance!
According to React documentation useContext will always re-render when the context value changes, so that's why HomeScreen is been called again.
Another thing you need to fix is the authUser initial state to be equal to authUser signOut state:
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
authUser: action.authUser,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
authUser: action.authUser,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
authUser: null,
};
}
},
{
isLoading: true,
isSignout: false,
authUser: "", //MAKE THIS THE SAME AS SIGN_OUT (null)
}
);

what is cause infinite rerenders?

So i am trying to follow the react navigation tutorial for auth flow https://reactnavigation.org/docs/auth-flow/
But for some reason its causing infinite rerenders, the screen flickering like crazy, and i have no idea why other than the dispatch in useEffect is just updating the state every render, but as far as i understand it that shouldn't be happening.
import React from "react"
import { NavigationContainer, NavigationContainerRef } from "#react-navigation/native"
import { createNativeStackNavigator } from "react-native-screens/native-stack"
import { PrimaryNavigator, AuthNavigator, DrawerNavigator } from "./primary-navigator"
import { SplashScreen } from "../screens/splash-screen"
import AsyncStorage from "#react-native-community/async-storage"
export type RootParamList = {
primaryStack: undefined,
authStack: undefined,
splashScreen: undefined,
}
const Stack = createNativeStackNavigator<RootParamList>()
const RootStack = () => {
const AuthContext = React.createContext()
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
}
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
}
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
}
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
)
React.useEffect(() => {
const bootstrapAsync = async () => {
let userToken
try {
userToken = await AsyncStorage.getItem('userToken')
console.log(userToken)
} catch (e) {
console.log(e)
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken })
}
bootstrapAsync()
})
const authContext = React.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
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' })
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
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
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' })
},
}), [])
return (
<AuthContext.Provider value={authContext}>
<Stack.Navigator
screenOptions={{
headerShown: false,
gestureEnabled: true,
stackPresentation: "modal",
}}
>
{ state.userToken == null
? <Stack.Screen
name="authStack"
component={AuthNavigator}
options={{
headerShown: false
}}
/>
: <Stack.Screen
name="primaryStack"
component={DrawerNavigator}
options={{
headerShown: false,
}}
/>
}
</Stack.Navigator>
</AuthContext.Provider>
)
}
export const RootNavigator = React.forwardRef<
NavigationContainerRef,
Partial<React.ComponentProps<typeof NavigationContainer>>
>((props, ref) => {
return (
<NavigationContainer {...props} ref={ref}>
<RootStack />
</NavigationContainer>
)
})
RootNavigator.displayName = "RootNavigator"
You missed the dependency '[]' which is provided in the tutorial. When you provide a dependency to the useEffect it will run only when a dependency changes. This is provided in the tutorial seems like you missed it.
This is from the tutorial
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
userToken = await AsyncStorage.getItem('userToken');
} catch (e) {
// Restoring token failed
}
// After restoring token, we may need to validate it in production apps
// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);