React-Native, Navigation, Drawers and passing inline functions with props - react-native

I have a Drawer Navigator nested inside a Stack navigator. It all works fine but I get the following warning:-
Looks like you're passing an inline function for 'component' prop for the screen 'Home' (e.g. component={() => }). Passing an inline function will cause the component state to be lost on re-render and cause perf issues since it's re-created every render. You can pass the function as children to 'Screen' instead to achieve the desired behaviour.
Here is the code.
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
const [initialRoute, setInitialRoute] = React.useState("");
const handleSignOut = () => {
// TODO implement sign out mechanism
};
return (
<NavigationContainer>{
<Stack.Navigator initialRouteName="Sign In" >
{isAuthenticated ? (
<Stack.Screen name="Home"
component={() => <HomeDrawer initialRoute={initialRoute} handleSignOut={handleSignOut}/>}
options={({ route, navigation }) => ({
headerTitle: getFocusedRouteNameFromRoute(route),
headerLeft: () => (
<Button
title="Menu"
onPress={() =>
navigation.dispatch(DrawerActions.toggleDrawer())
}
/>
),
headerRight: () => (
<Button onPress={handleSignOut} title="Sign Out" />
),
})}
/>
) : (
...
I fixed the warning by moving the component, but now the Drawer has stop working completely and I get this error:-
The action 'TOGGLE_DRAWER' was not handled by any navigator.
Is your screen inside a Drawer navigator?
Here is the new code.
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
const [initialRoute, setInitialRoute] = React.useState("");
const handleSignOut = () => {
// TODO implement sign out mechanism
};
const Draw = () => {<HomeDrawer initialRoute={initialRoute} handleSignOut={handleSignOut}/>};
return (
<NavigationContainer>{
<Stack.Navigator initialRouteName="Sign In" >
{isAuthenticated ? (
<Stack.Screen name="Home"
component={Draw}
options={({ route, navigation }) => ({
headerTitle: getFocusedRouteNameFromRoute(route),
headerLeft: () => (
<Button
title="Menu"
onPress={() =>
navigation.dispatch(DrawerActions.toggleDrawer())
}
/>
),
headerRight: () => (
<Button onPress={handleSignOut} title="Sign Out" />
),
})}
/>
) : (
...
I'm new to React-Native and on a steep learning curve. I've read a bunch of tutorials but stumped on this one. I can see I need to pass initialRoute at least, but not sure how to do that. A fix with an explanation would be great.

I found the answer in the end. It was pretty straight forward, but not when there are so many things to learn at once :-) When I moved the component like so it all worked.
<Stack.Screen name="Home"
options={({ route, navigation }) => ({
headerTitle: getFocusedRouteNameFromRoute(route),
headerLeft: () => (
<Button
title="Menu"
onPress={() =>
navigation.dispatch(DrawerActions.toggleDrawer())
}
/>
),
headerRight: () => (
<Button onPress={handleSignOut} title="Sign Out" />
),
})}
>
{(props) => (
<HomeDrawer {...props} initialRoute={initialRoute} handleSignOut={handleSignOut} />
)}
</Stack.Screen>
<Stack.Screen name="TEST" component={TestScreen}
screenOptions={{ headerShown: true }}
options={{ headerTitle: "Hello" }}
/>

Related

React Navigation Authentication Flow: The action 'NAVIGATE' with payload {"name":"HomeScreen"} was not handled by any navigator

A beginner at React Native here, trying to combine this doc with AWS Amplify Authentication to implement React Navigation Authentication Flow but I can't seem to figure out what's wrong. Whenever I click on the login button, this error appears.
Navigation Code (excluding imports):
const NavigationGeneral = () => {
const [user, setUser] = useState(undefined);
const checkUser = async () => {
try {
const authUser = await Auth.currentAuthenticatedUser({bypassCache: true});
setUser(authUser);
} catch (e) {
setUser(null);
}
}
useEffect(() => {
checkUser();
}, []);
if (user === undefined) {
return (
<View style = {{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<ActivityIndicator />
</View>
)
}
return (
<Stack.Navigator screenOptions={{headerShown: false}}>
{user ? (
<>
<Stack.Screen name = "Login" component = {LoginScreen} />
<Stack.Screen name = "SignUp" component = {SignUpScreen} />
<Stack.Screen name = "ConfirmEmail" component = {ConfirmEmailScreen} />
<Stack.Screen name = "ForgotPassword" component = {ForgotPasswordScreen} />
<Stack.Screen name = "NewPassword" component = {NewPasswordScreen} />
</>
): (
<Stack.Screen name = "HomeScreen" component = {HomeTabNavigator} />
)}
</Stack.Navigator>
);
};
//* TAB NAVIGATOR FOR APP SCREENS
const HomeTabNavigator = () => {
return (
<Tab.Navigator
initialRouteName="Home"
screenOptions={{
headerShown: false,
tabBarStyle: {backgroundColor: '#0052cc'},
tabBarInactiveTintColor: '#fff',
tabBarActiveTintColor: '#fff',
tabBarActiveBackgroundColor: '#006600'
}}>
<Tab.Screen
name="Home"
component={HomeScreen}
options={{
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Courses"
component={CourseScreen}
options={{
tabBarLabel: 'Courses',
tabBarIcon: ({ color, size }) => (
<Ionicons name="library-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarLabel: 'My Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Forensic Tools"
component={ForensicToolsScreen}
options={{
tabBarLabel: 'Tools List',
tabBarIcon: ({ color, size }) => (
<Ionicons name="list-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Leaderboard"
component={LeaderboardScreen}
options={{
tabBarLabel: 'Leaderboard',
tabBarIcon: ({ color, size }) => (
<Ionicons name="podium-outline" color={'#fff'} size={25} />
),
}}
/>
<Tab.Screen
name="Settings"
component={SettingsScreen}
options={{
tabBarLabel: 'Settings',
tabBarIcon: ({ color, size }) => (
<Ionicons name="settings-outline" color={'#fff'} size={25} />
),
}}
/>
</Tab.Navigator>
);
};
export default NavigationGeneral;
LoginScreen.js code snippet:
const onLoginPressed = async data => {
if (loading) {
return;
}
setLoading(true);
try {
await Auth.signIn(data.username, data.password);
navigation.navigate("HomeScreen");
console.log("Login");
} catch (e) {
Alert.alert('Oops', e.message);
}
setLoading(false);
};
I understand that I'm not supposed to manually navigate using navigation.navigate() but rather to conditionally define the screens. But whenever I remove navigation.navigate("HomeScreen"); from the code, nothing happens when I press the login button. So I assume something is wrong with my conditioning in my NavigationGeneral code, I just can't seem to figure out the problem.
Some help or additional tips would be greatly appreciated, thanks in advance. Please let me know if more info is required.
When you try to navigate to the HomeScreen I assume the user is not yet set in the state and therefore the screen with the name HomeScreen does not exist yet, so the navigator has nowhere to go.
Try setting the user in the NavigationGeneral upon login, it should event redirect automatically without using navigation.navigate.
You should not trigger navigation manually when implementing authentication flow with react-navigation. After the successful login, user will be truthy value. This means conditional rendering inside of navigation will handle it automatically.
Moreover, in case of condition user===undefined do not render LoadingIndicator, instead, create a new loading state and render LoadingIndicator when the state loading becomes true.
And I believe it should be !user instead of user, since not authenticated user (user===undefined) will want to see Login, Signup etc. screens.
return (
<Stack.Navigator screenOptions={{headerShown: false}}>
// Here it should be !user
{!user ? (
<>
<Stack.Screen name = "Login" component = {LoginScreen} />
<Stack.Screen name = "SignUp" component = {SignUpScreen} />
<Stack.Screen name = "ConfirmEmail" component = {ConfirmEmailScreen} />
<Stack.Screen name = "ForgotPassword" component = {ForgotPasswordScreen} />
<Stack.Screen name = "NewPassword" component = {NewPasswordScreen} />
</>
): (
<Stack.Screen name = "HomeScreen" component = {HomeTabNavigator} />
)}
</Stack.Navigator>
);
const [checkAuthUserLoading, setCheckAuthUserLoading] = React.useState(false);
const user = useSelector((state) => state.user); // Retrieve user from redux store. (It can be mobx, zustand or any other state management.)
const checkUser = async () => {
try {
setCheckAuthUserLoading(true);
const authUser = await Auth.currentAuthenticatedUser({
bypassCache: true,
});
setCheckAuthUserLoading(false);
dispatch(setAuthUser(user)); // I would suggest to use state management such as redux or mobx instead of storing user in component state
} catch (e) {
dispatch(setAuthUserError(e)); // Here you do not need to setUser(undefined) since its already failed and will be stay as undefined
}
};
if (checkAuthUserLoading) {
return <LoadingIndicator />;
}

Can i disable tabBarBadge on click of tab screen?

I have used Tab.Navigator to display 4 bottom navigation icons, one of which is Notification Icon. I want to disable the tabBarBadge option when i click on it. How to do it?
Here is the code I used
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarIcon: ({ focused }) =>
focused ? (
<Notification_icon />
) : (
<Notification_icon_inactive />
),
tabBarBadge: 2,
}}
/>
There is a way to do this using NavigationContainer's ref and controlling the visibility of the badges through some state variable. Listen for state event on the ref. In the handler of this event you get the name of the current route using ref.current?.getCurrentRoute()?.name. If this name is equal to the Notifications screen name then you set notificationBadgeVisible to false thus hiding the badges. Check the following code.
const App = () => {
const ref = React.useRef<NavigationContainerRef>(null);
const [notificationBadgeVisible, setNotificationBadgeVisible] = React.useState(true);
const navigationStateChangeHandler = () => {
if (ref.current?.getCurrentRoute()?.name === 'Notifications') {
setNotificationBadgeVisible(false);
}
}
React.useEffect(() => {
ref.current?.addListener('state', navigationStateChangeHandler);
return () => { ref.current?.removeListener('state', navigationStateChangeHandler); }
});
return (
<NavigationContainer ref={ref}>
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen
name="Notifications"
component={Notifications}
options={{
tabBarIcon: ({ focused }) =>
focused ? (
<Notification_icon />
) : (
<Notification_icon_inactive />
),
tabBarBadge: (notificationBadgeVisible ? 2 : undefined)
}}
/>
</Tab.Navigator>
</NavigationContainer>
);
};

opening drawer by pressing on bottom tab not working

I am trying to toggleDrawer on TabPress in BottomTab, but when I press on tab I get the following error navigation.toggleDrwer() is not a function.
This is my code:
export default function BottomTab() {
return (
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="Home" component={DrawerTab} style={styles} listeners={({ navigation }) => ({
tabPress: e => {
e.preventDefault()
navigation.toggleDrawer()
},
})} />
</Tab.Navigator>
</NavigationContainer>
);
}
i have manage to find the answer. So you actually have to use dispatch.
<Tab.Screen name="Home" component={DrawerTab} style={styles} listeners={({ navigation }) => ({
tabPress: e => {
e.preventDefault()
navigation.dispatch(DrawerActions.toggleDrawer())
},
})} />

React-Navigation v5: How to get screen's state from header

According to the React-Navigation docs (https://reactnavigation.org/docs/header-buttons/), in the header, we can interact with screen's state:
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={({ navigation, route }) => ({
headerTitle: props => <LogoTitle {...props} />,
})}
/>
</Stack.Navigator>
);
}
function HomeScreen({ navigation }) {
const [count, setCount] = React.useState(0);
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<Button onPress={() => setCount(c => c + 1)} title="Update count" /> // <===== SET STATE
),
});
}, [navigation]);
return <Text>Count: {count}</Text>;
}
It works fine when I set screen's state from header. But when I try to get screen's state from header, it's always return the initial value of state.
<Button
onPress={() => {
setCount(c => c + 1);
alert(count)
}}
title="Update count" />
How can I get the current state of screen from header?
I've had the solution:
Problem is in the React.useLayoutEffect() function.
I need to pass [count] to the second argument of this function instead of [navigation]:
React.useLayoutEffect(()=>{
...
}, [count]);
Then, it works fine

onPress to navigate on a screen not navigating

I am a beginner in React Native and I would like to navigate on a screen when I press the Button of my Navigator screen but it's not working.
Because when I press the Button in TabOneStack.Screen nothing is happening I don't understand, I would like to navigate to TabTwoScreen. I use React Navigation 5.6.1.
const BottomTab = createBottomTabNavigator<BottomTabParamList>();
export default function BottomTabNavigator() {
return (
<BottomTab.Navigator
initialRouteName="TabOne">
<BottomTab.Screen
name="TabOne"
component={TabOneNavigator}
/>
<BottomTab.Screen
name="TabTwo"
component={TabTwoNavigator}
options={{
tabBarLabel: 'Autour de moi',
tabBarIcon: ({ color }) => <TabBarIcon name="ios-navigate" color={color} />,
}}
/>
</BottomTab.Navigator>
);
}
const TabOneStack = createStackNavigator<TabOneParamList>();
function TabOneNavigator() {
return (
<TabOneStack.Navigator>
<TabOneStack.Screen
name="TabOneScreen"
component={TabOneScreen}
options={({ navigation }) => ({
headerTitle: 'Rejoindre', headerRight: () => (
<Button onPress={() => navigation.navigate('TabTwoScreen')}
icon={
<Icon
name='ios-log-in'
type='ionicon'
size={15}
color="white"
/>
}
/>
),
})}
/>
</TabOneStack.Navigator>
);
}
const TabTwoStack = createStackNavigator<TabTwoParamList>();
function TabTwoNavigator() {
return (
<TabTwoStack.Navigator>
<TabTwoStack.Screen
name="TabTwoScreen"
component={TabTwoScreen}
options={{ headerTitle: 'Autour de moi' }}
/>
</TabTwoStack.Navigator>
);
}
Why when I press the Button in TabOneStack.Screen nothing is happening?
Thank you in advance
You need to use props for navigation and you can try this
options={({ navigation }) => ({
headerTitle: 'Rejoindre', headerRight: props => (
<Button onPress={() => props.navigation.navigate('TabTwoScreen')}