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

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>
);
}

Related

Authentication flows | React Navigation v6

First i want to thank you for all answers.
I'm trying do Authentication Flow with React Navigation v6.x.
I want to catch in my App.js token value from AuthContext.js
Because of new navigation version i'm looking for new way to create a AuthFlow.
I see in Navigation documentation example for Authentication flow but there all logic be located in App.js and the whole thing is quite illegible.
I can take token from device but this solution dosent work with async changes. For example when i click SignIn button (signinscreen) it change token in mobile device but it refresh only singin screen.
App.js
import React, {useContext} from 'react';
import { NavigationContainer } from '#react-navigation/native';
import { createNativeStackNavigator } from '#react-navigation/native-stack';
import { createBottomTabNavigator } from '#react-navigation/bottom-tabs';
import SigninScreen from './src/screens/SigninScreen';
import SignupScreen from './src/screens/SignupScreen';
import AccountScreen from './src/screens/AccountScreen';
import TrackListScreen from './src/screens/TrackListScreen';
import TrackDetailsScreen from './src/screens/TrackDetailsScreen';
import CreateTrackScreen from './src/screens/CreateTrackScreen';
import { Provider as AuthProvider } from './src/context/AuthContext';
import { Context as AuthContext } from './src/context/AuthContext';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
const stackNestNav = () => {
return(
<Stack.Navigator>
<Stack.Screen name="List" component={TrackListScreen} />
<Stack.Screen name="Details" component={TrackDetailsScreen} />
</Stack.Navigator>
)
};
const AppNavigator = () => {
return(
<AuthProvider>
<NavigationContainer>
{token === null ? (
<>
<Stack.Navigator screenOptions={{headerShown: false}}>
<Stack.Screen name="SignIn" component={SigninScreen} />
<Stack.Screen name="SignUp" component={SignupScreen} />
</Stack.Navigator>
</>
) : (
<>
<Tab.Navigator>
<Tab.Screen name="Track List" component={stackNestNav} options={{ headerStyle: {height: 0}}}/>
<Tab.Screen name="Create" component={CreateTrackScreen} />
<Tab.Screen name="Account" component={AccountScreen} />
</Tab.Navigator>
</>
)
}
</NavigationContainer>
</AuthProvider>
)
}
export default AppNavigator;
and AuthContext.js
import AsyncStorage from '#react-native-async-storage/async-storage';
import createDataContext from './createDataContext';
import trackerApi from '../api/tracker';
const authReducer = (state,action) => {
switch(action.type){
case 'sign_error':
return {...state, errorMessage: action.payload}
case 'success_sign':
return {errorMessage: '', token: action.payload}
case 'clear_error_message':
return {...state, errorMessage: ''}
default:
return state;
}
};
//Czyszczenie wiadomości błędu
const clear_error_message = (dispatch) => () => {
dispatch({ type: 'clear_error_message' })
}
//Rejestracja
const signup = (dispatch) => async({email,password}) => {
try{
const response = await trackerApi.post('/signup', {email,password});
await AsyncStorage.setItem('token',response.data.token);
dispatch({ type: 'success_sign', payload: response.data.token });
console.log('Rejestracja przebiegła pomyślnie!');
} catch (err) {
dispatch({ type: 'sign_error', payload: 'Próba rejestracji nieudana.' })
}
};
//Logowanie
const signin = (dispatch) => async({email,password}) => {
try {
const response = await trackerApi.post('/signin', {email,password});
await AsyncStorage.setItem('token',response.data.token);
dispatch({ type: 'success_sign', payload: response.data.token });
console.log('Logowanie przebiegło pomyślnie.');
} catch (err) {
dispatch({ type: 'sign_error', payload: 'Próba logowania nieudana.' });
console.log(err.message);
}
};
//Wyloguj
const signout = (dispatch) => {
return({email,password}) => {
}
};
export const {Provider,Context} = createDataContext(
authReducer,
{signup,signin,signout,clear_error_message},
{ token: null, errorMessage: ''}
)
It is possible or should i do Context in App.js like in https://reactnavigation.org/docs/auth-flow?
One more time, thank you! :)
Solved.
exported whole NavigatoContainer content to the other Component.
created function in Context what catch token from mobile device and via dispatch updated state
in componen with whole NavigatoContainer content used state and function from point 2.
Cya!

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'

TypeError: undefined is not an object (evaluating 'loginState.isLoading')

I am trying to authenticate a login using Async Storage with React Native. Every time I click on the SignUp button in my simulator it throws me a type error saying "TypeError: undefined is not an object (evaluating 'loginState.isLoading')". I believe the problem is how I'm defining loginState. Here is the page where I define loginState below:
import 'react-native-gesture-handler';
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet,Component, Text,TouchableWithoutFeedback, View,Image,SafeAreaView, Button, ScrollView, ActivityIndicator } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createDrawerNavigator } from '#react-navigation/drawer';
import MainTabScreen from './screens/MainTab';
import {DrawerContent} from './screens/DrawerContent';
import RootStackScreen from './screens/RootStack'
import { useEffect } from 'react';
//import { firebaseConfig } from './screens/config';
import { AuthContext } from './components/context';
import AsyncStorage from '#react-native-async-storage/async-storage';
const Drawer = createDrawerNavigator();
function App() {
// const [isLoading, setIsLoading] = React.useState(true);
// const [userToken, setUserToken] = React.useState(null);
const initialLoginState = {
isLoading: true,
userName: null,
userToken: null,
};
const loginReducer = (prevState, action) => {
switch( action.type ) {
case 'RETRIEVE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'LOGIN':
return {
...prevState,
userName: action.id,
userToken: action.token,
isLoading: false,
};
case 'LOGOUT':
return {
...prevState,
userName: null,
userToken: null,
isLoading: false,
};
case 'REGISTER':
return {
...prevState,
userName: action.id,
userToken: action.token,
isLoading: false,
};
}
};
const [loginState, dispatch] = React.useReducer(loginReducer, initialLoginState);
const authContext = React.useMemo(() => ({
signIn: (userName,password) =>{
// setUserToken('tony');
// setIsLoading(false);
let userToken;
userToken = null;
if (userName == 'user' && password == 'pass'){
userToken = 'lexi';
}
dispatch({type:'LOGIN,',id:userName, token:userToken});
},
signOut: () => {
// setUserToken(null);
// setIsLoading(false);
dispatch({type:'LOGOUT'});
},
signUp: () => {
setUserToken('tony');
setIsLoading(false);
},
}),[])
useEffect(() => {
setTimeout(() => {
// setIsLoading(false);
dispatch({type:'RETRIEVE_TOKEN',token:'mario'});
},1000);
},[]);
if (loginState.isLoading == true)
{
return(
<View style = {{flex:1,justifyContent:'center',alignItems:'center'}}>
<ActivityIndicator size = "large"/>
</View>
);
}
return (
<AuthContext.Provider value = {authContext}>
<NavigationContainer>
{loginState.userToken !== null ? (
<Drawer.Navigator drawerContent = {props => <DrawerContent {... props}/>}>
<Drawer.Screen name="HomeDrawer" component={MainTabScreen} />
</Drawer.Navigator>
)
:
<RootStackScreen/>
}
</NavigationContainer>
</AuthContext.Provider>
);
}
export default App;
Found the error. I changed:
dispatch({type:'LOGIN,',id:userName, token:userToken});
To:
dispatch({type:'LOGIN',id:userName, token:userToken});

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();
}, []);

React context undefined

I have a basic authentication form created that works when I have all the code within the App.js file.
However, when I attempt to refactor the pages into separate files, the context is throwing an exception of it being empty.
App.js
import 'react-native-gesture-handler';
import * as React from 'react';
import { Button, Text, TextInput, View, StyleSheet } from 'react-native';
import AsyncStorage from '#react-native-community/async-storage';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import AuthContext from './src/utils/AuthContext';
import HomeScreen from './src/components/HomeScreen';
import SignInScreen from './src/components/SignInScreen';
import SplashScreen from './src/components/SplashScreen';
const Stack = createStackNavigator();
export default function App() {
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 {
userToken = await AsyncStorage.getItem('userToken');
} catch (e) {
// Restoring token failed
console.log("Restoring token failed" + e);
}
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};
bootstrapAsync();
}, []);
const authContext = React.useMemo(
() => ({
signIn: async data => {
dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signUp: async data => {
dispatch({ type: 'TO_SIGNUP_PAGE' });
},
signOut: () =>
dispatch({ type: 'SIGN_OUT' }),
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<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', animationTypeForReplace: state.isSignout ? 'pop' : 'push',}} />
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</AuthContext.Provider>
);
}
AuthContext.js
import { createContext } from 'react';
const AuthContext = createContext();
export default AuthContext;
HomeScreen.js
import React, { useContext } from 'react';
import { View, Text, TextInput, Button } from 'react-native';
import AuthContext from '../utils/AuthContext';
const HomeScreen = () => {
const { signOut } = useContext(AuthContext);
return (
<AuthContext>
<Text>Signed in!</Text>
<Button title="Sign out" onPress={signOut} />
</AuthContext>
);
}
export default HomeScreen;
Error:
I have spent many hours on this and looked through countless tutorials, for the life of me I can't figure out what I'm missing...
You are not exporting the context. Your App.js file exports the App component but you are trying to access the AuthContext from that file.
The best approach to take is to place the context in a separate file and import it to both App.js and HomeScreen.js
Your context file should look like this
import { createContext } from 'react';
const AppContext = createContext();
export default AppContext;
And you can import in other files like below
import AppContext from './AppContext';