Does anyone know where I have gone wrong here?
I am trying to dispatch an action when the user taps a button that calls the onSkip function, In the reducer I set the item to local storage and I retrieve it in the rootstack component so I can conditionally set the initial screen in my stack navigator. It always returns false and goes to the onboarding screen instead of login screen.
import React, { useEffect } from 'react'
import { NavigationContainer } from '#react-navigation/native'
import { createNativeStackNavigator } from '#react-navigation/native-stack'
import { RootStackParamList } from '../interfaces/RootStackParamList'
import AsyncStorage from '#react-native-async-storage/async-storage'
import { theme } from '../themes/themes'
import { useAppSelector } from '../app/hooks'
import DrawerNav from './DrawerNav'
import Tabs from './Tabs'
import HomeScreen from '../screens/HomeScreen'
import Login from '../screens/Auth/Login'
import OnboardingScreen from '../screens/Onboarding/OnboardingScreen'
import OtpScreen from '../screens/Auth/OtpScreen'
function RootStack() {
const Stack = createNativeStackNavigator<RootStackParamList>()
const onboardingState = useAppSelector(
(state) => state.onboarding.viewedOnBoarding
)
const [onboarded, setOnboarded] = React.useState(false)
const checkOnboarding = async () => {
try {
const value = await AsyncStorage.getItem('#onBoarding')
if (value !== null) {
setOnboarded(true)
}
} catch (err) {
} finally {
}
}
useEffect(() => {
checkOnboarding()
}, [])
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName={
onboarded === true ? 'Login' : 'OnboardingScreen'
}
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen options={{}} name='Login' component={Login} />
<Stack.Screen
name='OnboardingScreen'
component={OnboardingScreen}
/>
<Stack.Screen name='Tabs' component={Tabs} />
<Stack.Screen name='DrawerNav' component={DrawerNav} />
<Stack.Screen
name='OtpScreen'
component={OtpScreen}
options={{
title: 'OTP',
headerShown: true,
headerStyle: {
backgroundColor: theme.colors.whiteSmoke,
},
}}
/>
</Stack.Navigator>
</NavigationContainer>
)
}
export default RootStack
import { createSlice, PayloadAction } from '#reduxjs/toolkit'
import AsyncStorage from '#react-native-async-storage/async-storage'
export interface onboardingState {
viewedOnBoarding: boolean
}
const initialState: onboardingState = {
viewedOnBoarding: false,
}
export const onboardingSlice = createSlice({
name: 'onboarding',
initialState,
reducers: {
setOnboardingAsync: (state, action: PayloadAction<boolean>) => {
state.viewedOnBoarding = action.payload
AsyncStorage.setItem('#onBoarding', JSON.stringify(action.payload))
},
},
})
// Action creators are generated for each case reducer function
export const { setOnboardingAsync } = onboardingSlice.actions
export default onboardingSlice.reducer
onSkip={() => (
dispatch(setOnboardingAsync(true)),
navigation.replace('Login'),
AsyncStorage.setItem('#onBoarding', JSON.stringify(true))
)}
If you want to persist user's onboarding status, just use Redux-persist in your project (no need to directly use AsyncStorage).
After you implemented redux-persist, just use the
const onboardingState = useAppSelector((state) => state.onboarding.viewedOnBoarding)
to check whether user is onboarded.
Related
unable to pass params from deep link. getting undefined when running:
npx uri-scheme open [prefix]://news/3 --android
NewsScreen.js
import React from 'react';
const NewsScreen = ({ route, navigation }) => {
console.log(route.params); // undefined
};
Linking.js
import LINKING_PREFIXES from 'src/shared/constants';
export const linking = {
prefixes: LINKING_PREFIXES,
config: {
screens: {
Home: {
screens: {
News: {
path: 'news/:id?',
parse: {
id: id => `${id}`,
},
},
},
},
NotFound: '*',
},
},
};
Router.js
import React from 'react';
import { NavigationContainer } from '#react-navigation/native';
import {useAuth} from 'src/contexts/AuthContext';
import {Loading} from 'src/components/Loading';
import {AppStack} from './AppStack';
import {AuthStack} from './AuthStack';
import {GuestStack} from './GuestStack';
import SplashScreen from 'src/screens/guest/SplashScreen';
import linking from './Linking.js';
export const Router = () => {
const {authData, loading, isFirstTime, appBooting} = useAuth();
if (loading) {
return <Loading />;
}
const loadRoutes = () => {
if (appBooting) {
return <SplashScreen />;
}
if (isFirstTime) {
return <GuestStack />;
}
if (!authData || !authData.name || !authData.confirmed) {
return <AuthStack />;
}
return <AppStack />;
};
return <NavigationContainer>{loadRoutes()}</NavigationContainer>;
};
AppStack.js
import React from 'react';
import {createDrawerNavigator} from '#react-navigation/drawer';
import {SpeedNewsStack} from 'src/routes/NewsStack';
const Drawer = createDrawerNavigator();
export const AppStack = () => {
return (
<Drawer.Navigator>
<Drawer.Screen name="Home" component={NewsStack} />
</Drawer.Navigator>
);
};
NewsStack.js
import React from 'react';
import {createStackNavigator} from '#react-navigation/stack';
import SpeedNewsScreen from 'src/screens/NewsScreen';
const Stack = createStackNavigator();
export const NewsStack = () => {
return (
<Stack.Navigator>
<Stack.Screen name="News" component={NewsScreen} />
</Stack.Navigator>
);
};
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!
Version:
"dependencies": {
"react-native": "0.63.4",
"#react-navigation/bottom-tabs": "^5.11.2",
"#react-navigation/native": "^5.8.10",
"#react-navigation/stack": "^5.12.8",
}
Test website link test://info_register?token=1111 successfully, I can see route.params includes token
but when I get into my Tabs screen, and try to to use test://setting_register?token=1111, App just open it doesn't navigate to SettingScreen and route.params is undefined
I take reference from official document https://reactnavigation.org/docs/5.x/configuring-links
What is wrong with my deep linking for Tabs ?
Here is my code:
index.js
import * as React from 'react';
import {NavigationContainer} from '#react-navigation/native';
import LoginStack from './LoginStack';
import Linking from './Linking';
const AppContainer = () => {
return (
<NavigationContainer linking={Linking}>
<LoginStack />
</NavigationContainer>
);
};
export default AppContainer;
Linking.js
const config = {
screens: {
// set config for App init screen
PersonalInfoScreen: {
path: 'info_register/',
parse: {
token: (token) => `${token}`,
},
},
// set config for Tabs screen
Setting: {
screens: {
SettingScreen: 'setting_register/:token',
},
},
},
},
};
const Linking = {
prefixes: ['test://'],
config,
};
export default Linking;
LoginStack.js
import * as React from 'react';
import {useSelector} from 'react-redux';
import {createStackNavigator} from '#react-navigation/stack';
import LoginScreen from '../screens/Login/LoginScreen';
import PersonalInfoScreen from '../screens/Login/PersonalInfoScreen';
import TabStack from './TabStack';
const Stack = createStackNavigator();
const LoginStack = () => {
const {uid, userToken} = useSelector((state) => state.LoginRedux);
const showLoginFlow = uid === '' || userToken === '' ? true : false;
return (
<Stack.Navigator
initialRouteName={'LoginScreen'}
screenOptions={{headerShown: false, gestureEnabled: false}}>
{showLoginFlow ? (
<>
<Stack.Screen name="LoginScreen" component={LoginScreen} />
<Stack.Screen
name="PersonalInfoScreen"
component={PersonalInfoScreen}
/>
</>
) : (
<>
<Stack.Screen name="TabStack" component={TabStack} />
</>
)}}
</Stack.Navigator>
);
};
export default LoginStack;
TabStack.js
import {createBottomTabNavigator} from '#react-navigation/bottom-tabs';
const Tab = createBottomTabNavigator();
const TabStack = () => {
return (
<Tab.Navigator
screenOptions={...mySetting}
tabBarOptions={...myStyle},
}}>
<Tab.Screen name={'Free'} component={FreeStack} />
<Tab.Screen name={'Video'} component={VideoStack} />
<Tab.Screen name={'News'} component={NewsStack} />
<Tab.Screen name={'Consultation'} component={ConsulationStack} />
<Tab.Screen name={'Setting'} component={SettingStack} />
</Tab.Navigator>
);
};
export default TabStack;
If review nested navigation https://reactnavigation.org/docs/5.x/configuring-links/#handling-nested-navigators docs.
You have this navigation tree for SettingScreen:
TabStack -> Setting -> SettingStack -> SettingScreen.
Routes configuration should match this tree also as below:
const config = {
screens: {
// set config for App init screen
PersonalInfoScreen: {
path: "info_register/",
parse: {
token: (token) => `${token}`,
},
},
// set config for Tabs screen
TabStack: {
screens: {
Setting: {
screens: {
SettingScreen: "setting_register/:token",
},
},
},
},
},
};
Just a note, it seems like whenever you are enabling debug-chrome configuration deep linking with react-navigation is not working, I disabled it and it worked!
I want to check If the user has a secure Token in a useEffect but I get this error Message.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application.
This happens when I use the useEffect. If I remove it, then I get no error message but I need to check if the user has the token.
import React, { useEffect } from 'react';
import { View, Text } from 'react-native';
import getSecureKey from '../utilies/getSecureKey';
const Stack = createStackNavigator();
const AppStack = ({ navigation }) => {
useEffect(() => {
getSecureKey().then(res => console.log(res)).catch(e => console.log(e));
}, []);
return (
<Stack.Navigator showIcon={true} initialRouteName="AppTabs">
<Stack.Screen name="AppTabs" component={AppTabs} options={{headerTitle: () => <Header />, headerStyle: {
backgroundColor: '#fff'
}}} />
.....
getSecureToken:
import * as SecureStore from 'expo-secure-store';
const getSecureKey = async () => {
const key = await SecureStore.getItemAsync('jwt');
return key;
};
export default getSecureKey;
App.js
import React, { useState, useEffect } from 'react';
import * as Font from 'expo-font';
import { NavigationContainer } from '#react-navigation/native';
import AppLoading from 'expo-app-loading';
import { Provider } from 'react-redux';
import store from './src/redux/store/index';
import AppStack from './src/navigation/stack';
const getFonts = async () => {
await Font.loadAsync({
"nunito-regular": require("./assets/fonts/Nunito-Regular.ttf"),
"nunito-bold": require("./assets/fonts/Nunito-Bold.ttf"),
});
};
const App = () => {
const [fontsLoaded, setFontsLoaded] = useState(false);
if(fontsLoaded) {
return (
<Provider store={store}>
<NavigationContainer><AppStack /></NavigationContainer>
</Provider>)
} else {
return (<AppLoading startAsync={getFonts} onFinish={() => setFontsLoaded(true)} onError={() => {}} />)
}
};
export default App;
Don't restore token in the navigator. Instead do this -
Firstly, install expo-app-loading from here
Then, create a folder called navigation where your App.js is located. Then inside it create a File called AppNavigator.js.
Inside AppNavigator.js, paste this
import React, { useEffect } from 'react';
import { View, Text } from 'react-native';
import { createStackNavigator } from '#react-navigation/stack';
import getSecureKey from '../utilities/getSecureKey';
const Stack = createStackNavigator();
const AppNavigator = () => {
// Remove these Lines --
// useEffect(() => {
// getSecureKey()
// .then((res) => console.log(res))
// .catch((e) => console.log(e));
// }, []);
return (
<Stack.Navigator showIcon={true} initialRouteName="AppTabs">
<Stack.Screen
name="AppTabs"
component={AppTabs}
options={{
headerTitle: () => <Header />,
headerStyle: {
backgroundColor: '#fff',
},
}}
/>
</Stack.Navigator>
);
};
export default AppNavigator;
For your fonts create a folder called hooks where your App.js is located and inside that create a file useFonts.js
In useFonts.js write like this -
import * as Font from "expo-font";
export default useFonts = async () => {
await Font.loadAsync({
"nunito-regular": require("./assets/fonts/Nunito-Regular.ttf"),
"nunito-bold": require("./assets/fonts/Nunito-Bold.ttf"),
});
};
In your App.js
import React, { useState } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import Constants from 'expo-constants';
import { NavigationContainer } from '#react-navigation/native';
import AppLoading from 'expo-app-loading';
import useFonts from "./hooks/useFonts";
import getSecureKey from './utilities/getSecureKey';
import AppNavigator from './navigation/AppNavigator';
export default function App() {
const [IsReady, SetIsReady] = useState(false);
// Always perform Token Restoration in App.js just to keep code clear.
const FontAndTokenRestoration = async () => {
await useFonts(); // Font is being loaded here
const token = await getSecureKey();
if (token) {
console.log(token);
}
};
if (!IsReady) {
return (
<AppLoading
startAsync={FontAndTokenRestoration}
onFinish={() => SetIsReady(true)}
onError={() => {}}
/>
);
}
return (
<NavigationContainer>
<AppNavigator />
</NavigationContainer>
);
}
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';