Disable back button in a specific Screen (React Navigation Stack) - react-native

Hello!
I need to disable the back button on the navigation bar. help me plsss.
Routes
Home: I don't want to leave the application
Success: I don't want to go back to Checkout.
Example: click here
import React from 'react';
import { createStackNavigator } from '#react-navigation/stack';
import { OrderProvider } from '../contexts/order';
import Home from '../pages/Home';
import Checkout from '../pages/Checkout';
import Success from '../pages/Checkout/success';
const AppStack = createStackNavigator();
const AppRoutes = () => (
<OrderProvider>
<AppStack.Navigator screenOptions={{ headerShown: false }}>
<AppStack.Screen name="Home" component={Home} /> <-- here
<AppStack.Screen name="Checkout" component={Checkout} />
<AppStack.Screen name="Success" component={Success} /> <--- here
</AppStack.Navigator>
</OrderProvider>
);
export default AppRoutes;
import React from 'react';
import { View} from 'react-native';
const Success = () => {
return (
<View />
);
};
export default Success;

You can do the following:
const Home = () => {
useFocusEffect(
React.useCallback(() => {
const onBackPress = () => {
return true;
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, []),
);
// ...
};
const Success = ({navigation}) => {
useFocusEffect(
React.useCallback(() => {
const onBackPress = () => {
navigation.navigate('Home');
return true;
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, []),
);
// ...
};
I've set it up so that clicking back on the Home screen does nothing by returning true when the hardwareBackPress event is called.
For the Success screen I navigate back to Home.
I assumed this is the behavior you're looking for from reading your question.
It is important that you don't forget to import useFocusEffect from
react-navigation everywhere you use it:
import { useFocusEffect } from '#react-navigation/native';
When to return true or false in the hardwareBackPress event handler function is explained in the react navigation documentation:
Returning true from onBackPress denotes that we have handled the
event, and react-navigation's listener will not get called, thus not
popping the screen. Returning false will cause the event to bubble up
and react-navigation's listener will pop the screen.
If you want to read more read the documentation here: https://reactnavigation.org/docs/custom-android-back-button-handling/.

Related

Is there - a drawer-equivalent to the property tabPress react-navigation

I want to execute a certain code whenever a drawer element is clicked.
In other words, I am looking for a drawer equivalent to the property tabPress.
I tried adding my callback to the property focus. However, this callback is executed whenever "any screen" in the relevant stack is "in focus."
I want the code to be exuted only if the user "clicks" on a "drawer item". Is this at all possible?
import { createDrawerNavigator } from "#react-navigation/drawer";
const MyNav = createDrawerNavigator();
<MyNav.Navigator>
<MyNav.Screen name="abc" component={abcStack}
listeners={ ({ navigation, route }) => ({
// This is executed whenever any screen in the stack is accessed. This does NOT satisfy my requirements.
focus: (e) => {
console.log("We are in focus");
},
// Is there something like "tabPress" for Drawer stacks?
tabPress: (e) => {
console.log("We are on tabPress");
},
})}
/>
</MyNav.Navigator>
This question is related to React-Navigation Version 6.
I found a workaround, which should work in many cases. The idea is to send a parameter to the screen, which the drawer is calling, whenever the drawer is clicked. If the drawer calls a stack, you would have to add the parameter to the 1st screen of the stack
Drawer navigation with a single screen:
import { createDrawerNavigator } from "#react-navigation/drawer";
const MyNav = createDrawerNavigator();
<MyNav.Navigator>
<MyNav.Screen name="screen1" component={screen1} initialParams={{ opParam: "abc" }}/>
</MyNav.Navigator>
Drawer navigation with a stack:
import { createDrawerNavigator } from "#react-navigation/drawer";
import { createNativeStackNavigator } from "#react-navigation/native-stack";
const stack1 = () => {
const MyStack = createNativeStackNavigator();
return (
<MyStack.Navigator>
<MyStack.Screen name="screen1" component={screen1}/>
</MyStack.Navigator>
);
};
const MyNav = createDrawerNavigator();
<MyNav.Navigator>
<MyNav.Screen name="stack1" component={stack1}/>
</MyNav.Navigator>
Screen 1:
import React, { useEffect } from "react";
import { useIsFocused } from "#react-navigation/native";
const screen1 = (props) => {
const isFocused = useIsFocused();
useEffect(() => {
async function fetchData() {
// If this screen was loaded because of a click on the drawer, the following condition would be true.
if (props.route?.params?.opParam === "abc") {
// do whatever you want done when the user clicks on the drawer...
console.log("drawer was clicked");
}
}
// Only if this screen is in focus, we execute the function above.
if (isFocused) {
fetchData();
}
}, [ isFocused, props.route?.params?.opParam, ]);
return(
// ............
);
};

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!

Show Onboarding screen once

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.

react native memory leak react navigation

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

mount a component only once and not unmount it again

Perhaps what I think can solve my issue is not the right one. Happy to hearing ideas. I am getting:
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. To fix, cancel all subscriptions and async task in a useEffect cleanup function
and tracked it down to one component that is in my headerRight portion of the status bar. I was under the impression it mounts only once. Regardless, the component talks to a syncing process that happens and updates the state. For each status of the sycing, a different icon is displayed.
dataOperations is a NativeModules class that talks to some JAVA that does the background syncing and sends the status to RN.
import React, {useState, useEffect} from 'react';
import {DeviceEventEmitter } from 'react-native';
import DataOperations from "../../../../lib/databaseOperations"
const CommStatus: () => React$Node = () => {
let [status, updateStatus] = useState('');
const db = new DataOperations();
const onCommStatus = (event) => {
status = event['status'];
updateStatus(status);
};
const startSyncing = () => {
db.startSyncing();
};
const listner = DeviceEventEmitter.addListener(
'syncStatusChanged',
onCommStatus,
);
//NOT SURE THIS AS AN EFFECT
const removeListner = () =>{
DeviceEventEmitter.removeListener(listner)
}
//REMOVING THIS useEffect hides the error
useEffect(() => {
startSyncing();
return ()=>removeListner(); // just added this to try
}, []);
//TODO: find icons for stopped and idle. And perhaps animate BUSY?
const renderIcon = (status) => {
//STOPPED and IDLE are same here.
if (status == 'BUSY') {
return (
<Icon
name="trending-down"
/>
);
} else if (status == 'IS_CONNECTING') {
...another icon
}
};
renderIcon();
return <>{renderIcon(status)}</>;
};
export default CommStatus;
The component is loaded as part of the stack navigation as follows:
headerRight: () => (
<>
<CommStatus/>
</>
),
you can use App.js for that.
<Provider store={store}>
<ParentView>
<View style={{ flex: 1 }}>
<AppNavigator />
<AppToast />
</View>
</ParentView>
</Provider>
so in this case will mount only once.