Passing a props component through a Stack Navigator (React Navigation v5) - react-native

I am working on an expo react native application with react-navigation v5.
I have created 2 navigators : one StackNavigator if the user is not signed in, and one bottomTabNavigator if he is. In the first stackNavigator, I have a screen Login which takes one prop onAuthSucces used in a TouchableOpacity like this :
<TouchableOpacity style={styles.button} onPress={this.signIn}>
<Text style={{ color: "dimgray", fontWeight: "bold" }}>
Se connecter
</Text>
</TouchableOpacity>
The signIn function is written like this :
signIn = () => {
const user = authenticationService.authenticate(
this.state.login,
this.state.password
);
if (user !== null) {
this.props.onAuthSuccess(user);
} else
Alert.alert(
"Erreur de connexion",
"Votre identifiant et/ou votre mot de passe sont incorrects. Réessayez."
);
};
with a props onAuthSuccess of type : onAuthSuccess: (loggedUser: User) => void;
I tried to create a StackNavigator which can take parameters, and will affect these to the Login props, with :
const LoginPageStack = createStackNavigator<RootStackParamList>();
export const LoginStackScreen = (onAuthSuccess: any) => {
return (
<NavigationContainer>
<LoginPageStack.Navigator screenOptions={{ headerShown: false }}>
<LoginPageStack.Screen name="Login">
{(props) => <Login {...props} onAuthSuccess={onAuthSuccess} />}
</LoginPageStack.Screen>
<LoginPageStack.Screen name="Register" component={Register} />
</LoginPageStack.Navigator>
</NavigationContainer>
);
};
Finally, in App.tsx I use these different pages this like :
interface AppState {
currentUser: User | null;
isConnected: boolean;
}
export default class App extends Component<AppState> {
state: AppState = {
currentUser: null,
isConnected: false,
};
updateCurrentUser = (loggedUser: User) => {
this.setState({ currentUser: loggedUser });
this.setState({ isConnected: true });
};
render() {
if (this.state.isConnected) return <MainTabNavigator />;
else {
return (
<LoginStackScreen onAuthSuccess={this.updateCurrentUser} />
);
}
}
But when I try to connect me with a login and a password that are stocked in the database, Expo Go tells me : "TypeError: _this.props.onAuthSuccess is not a function. (In '_this.props.onAuthSuccess(user)', '_this.props.onAuthSuccess' is an instance of Object)"
Do you have any idea from where the problem can come ? I don't understand.
Thank you !

Related

stack from NativeStackNavigator (nested in BottomTabNavigator) resets everytime the BottomTabNavigator changes tabs

Situation:
The react native app has a BottomTabNavigator (react-navigation/material-bottom-tabs) and one of the tabs has a NativeStackNavigator (react-navigation/native-stack).
BottomTabNavigator:
tab1
tab2
tab3:
NativeStackNavigator:
screen1
screen2
Problem:
When I press tab3 (from the BottomTabNavigator) I see screen 1.
If I press a button on screen 1 then it will navigate from screen 1 to screen 2.
When I press tab1 or tab2 and then I press tab3 again then I see screen 1.
I want to see screen 2 (because that's what should be at the top of the stack from the NativeStackNavigator right?).
Did the stack from NativeStackNavigator reset?
Did the whole NativeStackNavigator render again?
What causes this behavior?
Code BottomTabNavigator:
export type MainNavigationBarParam = {
tab1: undefined;
tab2: undefined;
tab3: undefined;
};
const Tab = getMainNavigationBarTabNavigator();
export const tab3Stack = createNativeStackNavigator();
export function MainNavigationBar() {
const sizeToUse = 25;
return (
<Tab.Navigator
screenOptions={defaultScreenOptions()}
barStyle={{backgroundColor: theme.colors.primary}}>
<Tab.Screen
name="tab1"
component={Component1}
/>
<Tab.Screen
name="tab2"
component={Component2}
/>
<Tab.Screen
name="tab3"
component={Component3}
/>
</Tab.Navigator>
);
}
function defaultScreenOptions() {
const screenOptions: any = {
headerShown: false,
tabBarHideOnKeyboard: true,
};
if (Platform.OS === 'web') {
screenOptions.swipeEnabled = false;
}
return screenOptions;
}
Code Component3:
export type Tab3Stack ParamList = {
Screen1: undefined;
Screen2: {id: string; name: string};
};
export default function Component3() {
const [topComponentHeight, setTopComponentHeight] = useState(0);
function onLayout(event: LayoutChangeEvent) {
if ('top' in event.nativeEvent.layout) {
const withTop = event.nativeEvent.layout as unknown as {top: number};
setTopComponentHeight(withTop.top);
}
event;
}
return (
<View style={navContainerStyle(topComponentHeight).navContainer} onLayout={onLayout}>
<tab3Stack.Navigator screenOptions={{headerShown: false}}>
<tab3Stack.Screen
name={'Screen1'}
component={Screen1}
/>
<tab3Stack.Screen
name={'Screen2'}
component={Screen2}
/>
</tab3Stack.Navigator>
</View>
);
}
Update 1
Reproduced the code above for 2 tabs: https://snack.expo.dev/#jacobdel/182747
The problem does not occur on snack, only in my app.
Console in chrome:
No errors or warnings.
Sometimes this is shown, but after disabling the quill editor it doesn't appear anymore
Update 2
Cause is found: getMainNavigationBarTabNavigator(); from BottomTabNavigator
File MainNavigationBarTabNavigator.ts looks like this:
import {createMaterialBottomTabNavigator} from '#react-navigation/material-bottom-tabs';
import {MainNavigationBarParam} from './MainNavigationTabs';
export function getMainNavigationBarTabNavigator() {
return createMaterialBottomTabNavigator<MainNavigationBarParam>();
}
While MainNavigationBarTabNavigator.web.ts looks like this:
import {createMaterialTopTabNavigator} from '#react-navigation/material-top-tabs';
import {MainNavigationBarParam} from './MainNavigationTabs';
export function getMainNavigationBarTabNavigator() {
return createMaterialTopTabNavigator<MainNavigationBarParam>();
}
Snack only works as intended when using MainNavigationBarTabNavigator.ts
Update 3
Stuck..
Replacing the code from MainNavigationBarTabNavigator.web.ts with the code from MainNavigationBarTabNavigator.ts does not show the intended behavior as shown in the Snack example.
There seems to be an issue with the navigation event handling of #react-navigation/material-top-tabs.
If we handle this on our own, then the nested stack is not reset. We can prevent the default navigation action by using a tabPress event listener and calling e.preventDefault.
In your case, this is done as follows for screen2 (which is the nested stack).
<Tab.Screen
name="screen2"
component={Screen2}
listeners={({ navigation, route }) => ({
tabPress: (e) => {
e.preventDefault();
navigation.navigate({ name: route.name, merge: true });
},
})}
/>
Notice that this works fine on the phone, but has some issues on the web. If we navigate fast between screen2 and screen1 multiple times, then the navigation does not work at all. I haven't found the root cause for this issue.
However, we can make this work on the web as well as on the phone by providing a custom top navigation bar using the tabBar prop of the tab navigator. We override the default behavior of the tabPress function.
The original component is exported and is named MaterialTopTabBar. Sadly, it does not provide the possibility to provide a custom onTabPress function via props.
I have forked the component and 'fixed' (without knowing what exactly is going wrong here) the onTabPress function.
import {
useTheme,
} from '#react-navigation/native';
import Color from 'color';
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { TabBar, TabBarIndicator } from 'react-native-tab-view';
export default function FixedTabBarTop({
state,
navigation,
descriptors,
...rest
}) {
const { colors } = useTheme();
const focusedOptions = descriptors[state.routes[state.index].key].options;
const activeColor = focusedOptions.tabBarActiveTintColor ?? colors.text;
const inactiveColor =
focusedOptions.tabBarInactiveTintColor ??
new Color(activeColor).alpha(0.5).rgb().string();
return (
<TabBar
{...rest}
navigationState={state}
scrollEnabled={focusedOptions.tabBarScrollEnabled}
bounces={focusedOptions.tabBarBounces}
activeColor={activeColor}
inactiveColor={inactiveColor}
pressColor={focusedOptions.tabBarPressColor}
pressOpacity={focusedOptions.tabBarPressOpacity}
tabStyle={focusedOptions.tabBarItemStyle}
indicatorStyle={[
{ backgroundColor: colors.primary },
focusedOptions.tabBarIndicatorStyle,
]}
indicatorContainerStyle={focusedOptions.tabBarIndicatorContainerStyle}
contentContainerStyle={focusedOptions.tabBarContentContainerStyle}
style={[{ backgroundColor: colors.card }, focusedOptions.tabBarStyle]}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
}
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
onTabPress={({ route }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!event.defaultPrevented) {
navigation.navigate({ name: route.name, merge: true });
}
}}
onTabLongPress={({ route }) =>
navigation.emit({
type: 'tabLongPress',
target: route.key,
})
}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (options.tabBarShowIcon === false) {
return null;
}
if (options.tabBarIcon !== undefined) {
const icon = options.tabBarIcon({ focused, color });
return (
<View style={[styles.icon, options.tabBarIconStyle]}>{icon}</View>
);
}
return null;
}}
renderLabel={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (options.tabBarShowLabel === false) {
return null;
}
const label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name;
if (typeof label === 'string') {
return (
<Text
style={[styles.label, { color }, options.tabBarLabelStyle]}
allowFontScaling={options.tabBarAllowFontScaling}
>
{label}
</Text>
);
}
return label({ focused, color });
}}
renderBadge={({ route }) => {
const { tabBarBadge } = descriptors[route.key].options;
return tabBarBadge?.() ?? null;
}}
renderIndicator={({ navigationState: state, ...rest }) => {
return focusedOptions.tabBarIndicator ? (
focusedOptions.tabBarIndicator({
state: state,
...rest,
})
) : (
<TabBarIndicator navigationState={state} {...rest} />
);
}}
/>
);
}
const styles = StyleSheet.create({
icon: {
height: 24,
width: 24,
},
label: {
textAlign: 'center',
textTransform: 'uppercase',
fontSize: 13,
margin: 4,
backgroundColor: 'transparent',
},
});
I have used it as follows.
export function MainNavigationBar() {
return (
<Tab.Navigator
tabBar={props => <FixedTabBarTop {...props} />}
screenOptions={defaultScreenOptions()}>
<Tab.Screen
name="screen1"
component={Screen1}
/>
<Tab.Screen
name="screen2"
component={Screen2}
/>
</Tab.Navigator>
);
}
I have updated your snack. You might want to submit an issue on GitHub as well. It feels like a bug.

Expo BarCodeScanner keeps camera open upon navigation

I have a simple expo managed React Native project setup with react-navigation
I have 2 screens one of which is the Home screen and the other is a screen for QRCode scanner which uses expo's BarCodeScanner.
The issue here is that when navigating from the Home screen to the QRCode screen and back to the Home screen keeps the camera alive. I can see the activity in the status bar saying 'Expo Go is using your camera'
I tried various ways to tackle this,
Passing the screen as a render prop to Stack.Screen so it mounts every time we navigate, but still the same issue
Tried using the isFocused method to conditionally render the component but still no luck
<NavigationContainer fallback={<Text>Loading...</Text>}>
<Stack.Navigator
screenOptions={({ route, navigation }) => ({
headerShadowVisible: false,
headerTitle: () => (
<Text
style={{
fontSize: 30,
fontFamily: Font["900"],
}}
>
Test
</Text>
),
headerRight: () =>
route.name !== "QR" ? (
<Pressable onPress={() => navigation.navigate("QR")}>
<QrcodeIcon size={26} color="black" />
</Pressable>
) : null,
})}
>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="QR" children={() => <QRCode />} />
</Stack.Navigator>
</NavigationContainer>
And the code for the QRCode component looks like the following:
const QRCode = () => {
const [hasPermission, setHasPermission] = useState<boolean>();
const [scanned, setScanned] = useState<boolean>(false);
const isFocused = useIsFocused();
const linkTo = useLinkTo();
useEffect(() => {
(async () => {
const { status } = await BarCodeScanner.requestPermissionsAsync();
setHasPermission(status === "granted");
})();
}, []);
const handleBarCodeScanned = ({ type, data }: BarCodeEvent) => {
setScanned(true);
linkTo(data);
};
if (hasPermission === null) {
return <Text>Requesting for camera permission</Text>;
}
if (hasPermission === false) {
return <Text>No access to camera</Text>;
}
return (
<View style={styles.container}>
{isFocused ? (
<BarCodeScanner
onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
style={StyleSheet.absoluteFill}
/>
) : null}
</View>
);
};

React Native Navigation: screen flickers when function interacts with SecureStorage

The left gif shows the flickering of the screen when the Switch is pressed and the right image shows the screen at the moment when the flickering happens. That means that my Stack.Screen from #react-navigation/native-stack, that displays the Settings page gets removed for a small moment when the Switch is pressed, which results in the flicker effect.
The function that gets called when the Switch is pressed is my toggleLocalAuth function. This is an asynchronous function, so my guess would be that the screen flickers when the toggleLocalAuth is running.
const toggleLocalAuth = async () => {
await SecureStorage.save('local-auth', JSON.stringify(!usesLocalAuth))
setUsesLocalAuth((usesLocalAuth) => !usesLocalAuth)
}
This is my Security component which contains the Switch and the "Security" subheader.
const Security: React.FC = () => {
const theme = useTheme()
const { usesLocalAuth, toggleLocalAuth } = useAuth()
return (
<Box>
<Text variant="subheader" paddingHorizontal="m" paddingBottom="xs">
{i18n.t('settings.security')}
</Text>
<Paper>
<Box
flexDirection="row"
alignItems="center"
justifyContent="space-between"
>
<Box flexDirection="row">
<Ionicons
name="finger-print"
size={24}
color={theme.colors.icon}
style={{ paddingRight: theme.spacing.m }}
/>
<Text variant="subtitle">
{Platform.OS === 'ios' || Platform.OS === 'macos'
? i18n.t('settings.touchId')
: i18n.t('settings.fingerprint')}
</Text>
</Box>
<Switch value={usesLocalAuth} onValueChange={toggleLocalAuth} />
</Box>
</Paper>
</Box>
)
}
This is my whole screen that is shown in the image, which is a component of a StackNavigator Screen from #react-navigation/native-stack.
const Settings = ({
navigation,
}: {
navigation: NativeStackNavigationProp<SessionParamList, 'Settings'>
}) => {
const theme = useTheme()
return (
<ScrollView style={{ paddingVertical: theme.spacing.m }}>
<Profile
onPersonalPress={() => navigation.push('PersonalData')}
onResidencePress={() => navigation.push('Residence')}
onContactPress={() => navigation.push('ContactInformation')}
/>
<Preferences />
<Security />
</ScrollView>
)
}
export default Settings
This is the StackNavigator Screen which holds the Settings page.
const Stack = createNativeStackNavigator<SessionParamList>()
const Tab = createBottomTabNavigator()
function SettingsStack() {
return (
<Stack.Navigator>
<Stack.Screen
options={{
...stackOptions,
headerTitle: i18n.t('settings.settings'),
}}
name="Settings"
component={Settings}
/>
</Stack.Navigator>
)
}
My guess is that the Settings Stack.Screen gets removed for a small moment when the async toggleLocalAuth function is called. The background and Tabbar do not flicker because they are defined as screenOptions on my Tab.Navigator which sits above the Settings page in the component tree.
Edit:
I use useContext and createContext to gloablly manage the authentication state.
export type ContextType = {
isAuthenticated: boolean
usesLocalAuth: boolean
jwt: string
logout: () => void
login: (jwt: string) => void
toggleLocalAuth: () => void
}
const AuthContext = createContext<ContextType>({
isAuthenticated: false,
jwt: '',
usesLocalAuth: false,
logout: () => console.warn('Not Auth provider above component'),
login: () => console.warn('Not Auth provider above component'),
toggleLocalAuth: () => console.warn('Not Auth provider above component'),
})
export const useAuth = () => useContext(AuthContext)
These are the relevant excerpts of my App.tsx file:
export default function App() {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false)
const [usesLocalAuth, setUsesLocalAuth] = useState<boolean>(false)
const [jwt, setJwt] = useState('')
function logout() {
setIsAuthenticated(false)
setDid(null)
}
function login(jwt: string) {
setIsAuthenticated(true)
setJwt(jwt)
}
const authProvider = useMemo(
() => ({
isAuthenticated: isAuthenticated,
jwt: jwt,
usesLocalAuth: usesLocalAuth,
logout: logout,
login: login,
toggleLocalAuth: toggleLocalAuth,
}),
[isAuthenticated, usesLocalAuth, did, jwt, logout, login, toggleLocalAuth],
...
return (
<AuthContext.Provider value={authProvider}>
...
</AuthContext.Provider>
)
)

Multiple prompts AzureAD promptAuthAsync() Expo

I am new here and to Expo/RN. I’m trying to implement some basic authentication in my app with Azure ad to then make Graph API Calls. Right now I have a log in button that is displayed displayed on the WelcomeScreen, however when a user presses it and logs in (via AzureAd prompt), it takes them back to the Welcomescreen until they login again. Its like state isn’t being update or re-evaluated after they login. Am I missing something obvious with promptAsync?
App.js
export default function App() {
const [user, setUser] = useState(null);
WebBrowser.maybeCompleteAuthSession();
const discovery = useAutoDiscovery(
"placeholder"
);
// Request
const [request, response, promptAsync] = useAuthRequest(
{
clientId: "placeholder",
scopes: ["openid", "profile", "email", "offline_access"],
// For usage in managed apps using the proxy
redirectUri: makeRedirectUri({
// For usage in bare and standalone
native: "placeholder",
}),
responseType: ResponseType.Token,
},
discovery
);
const handleLogin = async () => {
await promptAsync();
if (response && response.type === "success") {
var userDecoded = jwtDecode(response.params.access_token);
setUser({
accessToken: response.params.access_token,
userDetails: userDecoded,
});
}
};
return (
<AuthContext.Provider value={{ user, handleLogin }}>
<NavigationContainer theme={navigationTheme}>
{user ? <AppNavigator /> : <AuthNavigator />}
</NavigationContainer>
</AuthContext.Provider>
);
}
(Where login button is)
WelcomeScreen.js
function WelcomeScreen({ navigation }) {
const authContext = useContext(AuthContext);
return (
<ImageBackground
style={styles.background}
source={require("../assets/background.jpg")}
blurRadius={4}
>
<View style={styles.logoContainer}>
<Image source={require("../assets/lo.png")} />
<Text style={styles.titleText}>ITS Inventory</Text>
</View>
<View style={styles.buttonsContainer}>
<AppButton
title={"Login Via Azure"}
style={styles.loginButton}
onPress={() => authContext.handleLogin()}
></AppButton>
</View>
</ImageBackground>
);
}
Thanks!!

Cannot call Element Props inside React Navigation Stack.Navigator

I am using React-Navigation v5 with Redux. I like to call my logOut action creator to trigger my log out function from my headerRight.
However, I can only access logOut from inside Home element and not inside HomeStack.
One idea I have is to also wrap my HomeStack with connect. I haven't tried it yet to know whether it can work or not. Even should it work, this isn't my preferred solution because i feel it makes my code very verbose
Anyone has a solution on how to access my logOut function from within my HomeStack? Thanks in advance
const Home = props => {
const { loading, error, data, subscribeToMore } = useQuery(FIND_BOOKS_QUERY);
console.log("home props", props);
return (
<View>
<Text> Welcome {props.user && props.user.name} </Text>
{loading && <Text>Loading...</Text>}
{error && <Text>Error: {error.message}</Text>}
{data &&
data.findBooks.map((x, index) => {
return (
<View key={index}>
<Text>
{x.title} - {x.author}
</Text>
</View>
);
})}
</View>
);
};
const HomeContainer = connect(
state => {
// console.log("home state", state);
return {
user: state.auth.user
};
},
{ logOut }
)(Home);
export const HomeStack = props => {
console.log("home stack props", props);
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeContainer}
options={{
headerRight: () => {
return (
<TouchableOpacity
onPress={() => {
// props.logOut // - cannot access logOut here as HomeStack props does not have logOut
console.log("exit");
}}
>
<Text>Exit</Text>
</TouchableOpacity>
);
}
}}
/>
</Stack.Navigator>
);
};
Wrapping my HomeStack with connect works. Its able to make available logOut inside H which allows me to call logOut inside headerRight.
However, such a method will be verbose because I will need to connect both H and Home to redux. Is there a more elegant way? Thanks
const Home = props => {
const { loading, error, data, subscribeToMore } = useQuery(FIND_BOOKS_QUERY);
console.log("home props", props);
return (
<View>
<Text> Welcome {props.user && props.user.name} </Text>
{loading && <Text>Loading...</Text>}
{error && <Text>Error: {error.message}</Text>}
{data &&
data.findBooks.map((x, index) => {
return (
<View key={index}>
<Text>
{x.title} - {x.author}
</Text>
</View>
);
})}
</View>
);
};
const HomeContainer = connect(
state => {
// console.log("home state", state);
return {
user: state.auth.user
};
},
{ logOut }
)(Home);
export const H = props => {
console.log("home stack props", props);
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeContainer}
options={{
headerRight: () => {
return (
<TouchableOpacity
onPress={() => {
props.logOut(); // now works OK
console.log("exit");
}}
>
<Text>Exit</Text>
</TouchableOpacity>
);
}
}}
/>
</Stack.Navigator>
);
};
export const HomeStack = connect(
state => {
// console.log("home state", state);
return {
user: state.auth.user
};
},
{ logOut }
)(H);