I have a React Native app that I am adding navigation to. I have two different stacks I want to navigate between. I have searched SO and came up with the two versions which I have in my below code. The setup is that I have a main stack navigator which has a drawer stack and a normal stack (Auth) that I'd like to be able to go between. The main things are redirecting on login and logout from the header of the Drawer stack.
Below is my code, I am looking for help to figure out how to enable the mentioned navigation that I need the app to do. In theory, this is a common thing so I am pretty sure it should be supported and documented though I can't seem to find anything documented on it.
Error message
The action 'NAVIGATE' with payload {"name":"Auth","params":{}} was not handled by any navigator.
Do you have a screen named 'Auth'?
HeaderRight
export default function HeaderRight ({ navigation }) {
const { removeAuth } = useContext(AuthContext);
const logout = () => {
removeAuth()
// Note both are uncommented for sharing and are not actually meant to be when running the app
navigation.navigate('Auth', { screen: 'Login' }); // { screen: 'Login' }
navigation.navigate(
'Auth',
{},
NavigationActions.navigate({
routeName: 'Login'
})
)
}
return (
<View style={styles.header}>
<Icon name='logout' onPress={logout} size={28} style={styles.icon} color="white" />
</View>
)
}
Root navigation
<Stack.Navigator
initialRouteName={"Splash"}
screenOptions={{}}
component={SplashScreen}
>
{ checkAuth ?
<Stack.Screen name="Drawer" component={DrawerStack} options={({ navigation }) => ({
title: 'My App',
headerRight: () => (
<HeaderRight navigation={navigation}/>
),
headerTitleAlign: 'center',
headerTintColor: 'white',
headerStyle: {
backgroundColor: '#5742f5'
},
})} />
:
<Stack.Screen name="Auth" component={AuthStack} options={{
headerShown: false
}}/>
}
</Stack.Navigator>
drawer stack
export const DrawerStack = (props) => {
return (
<Drawer.Navigator
initialRouteName="Lists"
>
<Drawer.Screen
name="Conversations"
options={{
title: props.title,
}}
component={ListScreen}
/>
<Drawer.Screen
name="Room"
options={{
drawerLabel: () => null,
title: null,
drawerIcon: () => null,
}}
component={RoomScreen}
/>
</Drawer.Navigator>
);
};
auth stack
export const AuthStack = () => {
return (
<Stack.Navigator
initialRouteName={'Login'}
>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Forgot" component={ForgotScreen} />
</Stack.Navigator>
);
};
Edit
I think I have found the potential cause for this but I have no clue about the solution to it.
I have this in my root navigator (same as above)
{ checkAuth ?
<Stack.Screen name="Drawer" component={DrawerStack} options={({ navigation }) => ({
title: 'My App',
headerLeft: () => (
<HeaderLeft />
),
headerRight: () => (
<HeaderRight navigation={navigation}/>
),
})} />
:
<Stack.Screen name="Auth" component={AuthStack} options={{
headerShown: false
}}/>
}
This means that the stack/screen for Auth is only being rendered/exists when the drawer stack isn't shown. How can I refactor my root navigator to fix this?
I am thinking I need to somehow force the checkAuth function to re-render based on different actions taken or to watch a certain local storage.
This is the code for checkAuth
const checkAuth = () => {
try {
const authData = globalStorage.getString('accessToken')
if(authData !== null && authData !== undefined) {
setAuth(authData)
return true
} else {
return false
}
} catch(e) {
console.error(e)
return false
}
}
Context on what globalStorage is
import { MMKV } from 'react-native-mmkv'
export const globalStorage = new MMKV()
Related
I have a FlatList inside of a screen that uses BottomTabNavigator from react-navigation-bottom-tabs. Inside of the FlatList are swipeable items, using Swipeable from react-native-gesture-handler. Inside of the tab screens these items aren't swipeable for some reason. However, when I put the screen with the FlatList inside of a StackNavigator screen, and nest that inside of the BottomNavigator tabscreen, it does work. I'm at a loss for why it works this way but not the other way. Does anyone have the slightest clue why this is the case?
Edit: I've seen the issue of not being able to navigate bottom tabs using swipe. I'm not trying to navigate tabs though. Swipeable is applied to items inside a FlatList, that is inside its own component, that is rendered by the Home screen, which in turn is used by the navigator.
The TabsNavigator:
export default function TabsNavigator() {
const Tab = createBottomTabNavigator();
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen
name="List"
component={HomeStack}
options={{
tabBarIcon: ({ focused }) => (
<FontAwesomeIcon
icon={faList}
style={{ color: focused ? "#104543" : "#CCC" }}
/>
),
tabBarActiveTintColor: "#104543",
}}
/>
)
}
The HomeStack (StackNavigator):
export default function StackNavigator() {
const Stack = createStackNavigator();
return (
<Stack.Navigator>
<Stack.Screen
options={{ headerShown: false }}
name="Home"
component={Home}
/>
</Stack.Navigator>
);
}
The Home screen:
export const Home = ({ navigation }) => {
const dispatch = useDispatch();
useEffect(() => {
const fetchUserData = async () => {
const data = await axios.get(
`http://10.0.2.2:80/
`,
{
auth: {
username: "***",
password: "***",
},
}
);
dispatch(setUsersData(data.data));
};
return navigation.addListener("focus", async () => {
await fetchUserData();
});
}, [navigation]);
return (
<View style={styles.container}>
<SearchBarHeader />
//The component with the FlatList in question that contains swipeable items
<SwipeableFlastList />
</View>
);
};
The preferred outcome would be to place the Home screen inside of the Tab.Screen component like this:
<Tab.Screen
name="List"
component={Home}
/>
Per request, the Swipeable component. It's inside of another ListItem component, which inturn is inside a component that renders the flatlist:
<KeyboardAvoidingView style={styles.container}>
<Swipeable
renderRightActions={(
progress: Animated.AnimatedInterpolation,
dragAnimatedValue: Animated.AnimatedInterpolation
) => renderRightActions(progress, dragAnimatedValue, item.Id)}
renderLeftActions={(
progress: Animated.AnimatedInterpolation,
dragAnimatedValue: Animated.AnimatedInterpolation
) => renderLeftActions(progress, dragAnimatedValue, item.Id)}
useNativeAnimations={true}
onSwipeableOpen={() => setSwipeOpen(!swipeOpen)}
onSwipeableClose={() => setSwipeOpen(!swipeOpen)}
>
<TouchableWithoutFeedback>
<AccordionListItem item={item} />
</TouchableWithoutFeedback>
</Swipeable>
</KeyboardAvoidingView>
I have my navigators arranged like this.
Bottom Navigator
2. Stack Navigator
3. Material Top Tabs Navigator
4. Home Screen
I have created headerRight button in StackNavigator's ScreenComponent like this.
export default HomeDashboardNavigation = ({route, navigation}) => {
return (
<DashboardStack.Navigator>
<DashboardStack.Screen name="TopTabNavigator" component={TopTabNavigator}
options={{
headerShown: true,
headerShadowVisible: false,
headerStyle:{backgroundColor:'#FEF8F0'},
headerTitle: (props) => <LogoTitle {...props} />,
headerTitleAlign: 'center',
headerRight: () => (
<TouchableOpacity onPress={() => {
// Need to pass a boolean value to HomeScreen
}}
>
<HintComponent />
</TouchableOpacity>
),
}}
/>
</DashboardStack.Navigator>
)
}
My TopTabNavigator looks like this:
const TopTabNavigator = ({route, navigation}) => {
return (
<Tab.Navigator tabBar={props => <TabBar {...props} />} >
<Tab.Screen name="HomeScreen" component={HomeScreen} />
<Tab.Screen name="Second" component={SecondScreen} />
<Tab.Screen name="Third" component={ThirdScreen} />
<Tab.Screen name="Fourth" component={FourthScreen} />
</Tab.Navigator>
)
}
My HomeScreen is:
const HomeScreen = (props) => {
const isHeaderRightClicked = false; //Need to get the value here
return (
<ScrollView>
</ScrollView>
);
}
I am using react native navigation 6. I tried using setParams and setting route.params and tried to get is in HomeScreen using useEffect on props.navigation but it never gets executed.
I don't know how to pass value from onPress event to the screen. HomeScreen is embedded in TopTabNavigator which is a Screen component of StackNavigator whose header button click sets the value.
Could you please suggest a good way to do this.
Try something like my example below. Here a useState boolean hook [isClicked, setIsClicked] is defined, where the onPress-function changes this boolean on every press. The state isClicked in then passed through the TopTabNavigator to the HomeScreen where it can be used.
HomeDashboardNavigation file:
export default HomeDashboardNavigation = ({ route, navigation }) => {
const [isClicked, setIsClicked] = useState(false);
return (
<DashboardStack.Navigator>
<DashboardStack.Screen
name="TopTabNavigator"
options={{
headerShown: true,
headerShadowVisible: false,
headerStyle: { backgroundColor: "#FEF8F0" },
headerTitle: (props) => <LogoTitle {...props} />,
headerTitleAlign: "center",
headerRight: () => (
<TouchableOpacity
onPress={() => {
setIsClicked((state) => !state);
}}
>
<HintComponent />
</TouchableOpacity>
),
}}
>
{(props) => <TopTabNavigator isClicked={isClicked} {...props} />}
</DashboardStack.Screen>
</DashboardStack.Navigator>
);
};
TopTabNavigator file
const TopTabNavigator = ({ isClicked, route, navigation }) => {
return (
<Tab.Navigator tabBar={(props) => <TabBar {...props} />}>
<Tab.Screen name="HomeScreen">
{(props) => <HomeScreen isClicked={isClicked} {...props} />}
</Tab.Screen>
<Tab.Screen name="Second" component={SecondScreen} />
<Tab.Screen name="Third" component={ThirdScreen} />
<Tab.Screen name="Fourth" component={FourthScreen} />
</Tab.Navigator>
);
};
HomeScreen file
export const HomeScreen = (props) => {
const isHeaderRightClicked = props.isClicked;
return <ScrollView></ScrollView>;
};
Im following a react native app tutorial that has stack navigation only but I'm trying to also add nested navigation so I have a tab bar at the bottom that leads to additional pages I'm building but can't seem to figure out how to implement in my current code. Here is my code so far not sure if i'm on the right track or way off
{
DashBoard: DashBoardScreen,
Articles: ArticlesScreen,
Article: ArticleScreen,
Search: SearchScreen
},
{
initialRouteName: 'DashBoard',
defaultNavigationOptions: ({navigation}) => ({
headerStyle, headerTitleStyle,
headerRight: (<SearchBtn onPress={() => navigation.navigate('Search')}/>)
})
}
);
const Tab = createBottomTabNavigator();
const TabNavigator = () => (
<Tab.Navigator>
<Tab.Screen name="News" component={DashBoard}/>
<Tab.Screen name="Game" component={Game}/>
</Tab.Navigator>
)```
Here's how you can achieve this:
const BottomTab = createBottomTabNavigator();
const Stack = createStackNavigator();
const AppStack = () => {
return (
<BottomTab.Navigator>
<BottomTab.Screen name="Screen1" component={Screen1} />
<BottomTab.Screen name="Screen2" component={Screen2} />
</BottomTab.Navigator>
);
};
const MainStack = () => {
return (
<Stack.Navigator headerMode="none">
<Stack.Screen name="App" component={AppStack} />
<Stack.Screen name="Auth" component={AuthStack} />
</Stack.Navigator>
);
};
I have an app that has a bottom tab navigator inside a drawer navigator as my Home Screen and I also have a stack navigator comprising of my auth screens (including login screen).
I am unable to navigate from my login screen to my Home stack screen after a successful login.
I get the error - the action navigate with payload was not handled by any navigator.
Please see my code. How can I resolve this
Navigation file - Navigation.js
const MainTabScreen = () => (
<Tab.Navigator
initialRouteName="Main"
activeColor="#fff"
>
<Tab.Screen
name="Main"
component={Main}
options={{
tabBarLabel: 'Home',
tabBarColor: "#009387",
tabBarIcon: ({ color }) => (
<Icon name="md-home" color={color} size={26} />
),
}}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarLabel: 'Profile',
tabBarColor: "#694fad",
tabBarIcon: ({ color }) => (
<Icon name="md-contact" color={color} size={26} />
),
}}
/>
<Tab.Screen
name="Explore"
component={ExploreScreen}
options={{
tabBarLabel: 'Account',
tabBarColor: "#d02860",
tabBarIcon: ({ color }) => (
<Icon name="md-planet" color={color} size={26} />
),
}}
/>
</Tab.Navigator>
)
// HOME STACK SCREEN
const DrawerScreen = () => (
<Drawer.Navigator drawerContent={props => <DrawerContent {...props} /> } >
<Drawer.Screen name='MainTab' component={MainTabScreen} />
</Drawer.Navigator>
);
const AuthStack = createStackNavigator();
const AuthStackScreen = ({navigation}) => (
<AuthStack.Navigator headerMode='none'>
<AuthStack.Screen name="SplashScreen" component={SplashScreen} />
<AuthStack.Screen name="SignInScreen" component={SignInScreen} /> // LOGIN SCREEN
<AuthStack.Screen name="SignUpScreen" component={SignUpScreen} />
</AuthStack.Navigator>
);
const RootStack = createStackNavigator();
const RootStackScreen = () => {
const [isLoading, setIsLoading] = React.useState(true);
const [user, setUser] = React.useState(null);
React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;
try {
userToken = await AsyncStorage.getItem('FBIdToken');
console.log("toen", userToken)
if(userToken !== null) {
setIsLoading(!isLoading);
setUser(userToken);
} else {
setIsLoading(!isLoading);
}
} catch (e) {
console.log(error)
}
// After restoring token, we may need to validate it in production apps
};
bootstrapAsync();
}, []);
return (
<RootStack.Navigator
headerMode="none"
screenOptions={{ animationEnabled: false }}
mode="modal"
>
{isLoading ? (
<RootStack.Screen name="Loading" component={LoadingScreen} />
) : user ? (
<RootStack.Screen name="DrawerScreen" component={DrawerScreen} /> //HOME STACK ) : (
<RootStack.Screen name="AuthStackScreen" component={AuthStackScreen} />
)}
</RootStack.Navigator>
);
};
export default RootStackScreen;
App.js
<Provider store={store}>
<NavigationContainer ref={navigationRef}>
<RootStackScreen />
</NavigationContainer>
</Provider>
Login.js
loginHandle = () => {
this.props.navigation.navigate("DrawerScreen")
};
I suspect that in your RootStackScreen component, your condition isLoading is true so the route to your DrawerScreen does not exists when you navigate. Try removing the condition to be sure. Then if this isthe reason, yu could try applying your condition to the component you pass to your DrawerScreen instead, something like :
<RootStack.Screen name="DrawerScreen" component={isLoading ? LoadingScreen : DrawerScreen} />
I have created a nested tab navigator in my stack navigator as follows:
const Stack = createStackNavigator()
const Tab = createBottomTabNavigator();
function TabNav () {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen}></Tab.Screen>
<Tab.Screen name="Category" component={CategoryScreen}></Tab.Screen>
<Tab.Screen name="Settings" component={SettingsScreen}></Tab.Screen>
</Tab.Navigator>
)
}
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Tab" component={TabNav}></Stack.Screen>
<Stack.Screen name="Category" component={CategoryScreen}>
</Stack.Screen>
<Stack.Screen
name="SpecialBeerScreen" component= {SpecialBeerScreen}>
</Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
)
}
However, the header now looks like Tab for each of the screens. How do I override this Header for each of the screens with a custom text, such as Home, Category etc.
Update: This is still valid in react-navigation v6
Use getFocusedRouteNameFromRoute as outlined in the react-navigation docs in the section Setting parent screen options based on child navigator's state to access information of screens in nested navigators.
Change this line
<Stack.Screen name="Tab" component={TabNav}></Stack.Screen>
to the following (adding the options prop):
<Stack.Screen
name="Tab"
component={TabNav}
options={({ route }) => {
const routeName = getFocusedRouteNameFromRoute(route) ?? 'Home';
switch (routeName) {
case 'Category': {
return {
headerTitle: 'Category',
};
}
case 'Settings': {
return {
headerTitle: 'Settings',
};
}
case 'Home':
default: {
return {
headerTitle: 'Home',
};
}
}
}}
/>
You also have to import getFocusedRouteNameFromRoute, so also add:
import { getFocusedRouteNameFromRoute } from '#react-navigation/native';
If I understand your problem correctly, you wanted to change the stack title when the tab is changed. In this case, you may use React Context to control it.
(I also put this code in snack https://snack.expo.io/#gie3d/change-header-from-tab)
edit: 1, I separated it into 3 files and assume that it's all in the same directory.
HomeTitleContext.js
export const HomeTitleContext = React.createContext({
title: 'default title',
setTitle: () => {},
});
App.js
import { HomeTitleContext } from './HomeTitleContext';
export default function App() {
const [title, setTitle] = useState('default title');
return (
<HomeTitleContext.Provider
value={{
title,
setTitle,
}}
>
<HomeTitleContext.Consumer>
{(ctx) => (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Tab"
component={TabNav}
options={{ title: ctx.title }} // The title will be dynamically changed here
/>
<Stack.Screen
name="Category"
component={OtherScreen}
></Stack.Screen>
<Stack.Screen
name="SpecialBeerScreen"
component={OtherScreen}
></Stack.Screen>
</Stack.Navigator>
</NavigationContainer>
)}
</HomeTitleContext.Consumer>
</HomeTitleContext.Provider>
);
}
In your component, for example: HomeScreen, you set up a useFocusEffect and change the title from setTitle which you'll get from the context
HomeScreen.js
import React, { useContext } from 'react';
import { useFocusEffect } from '#react-navigation/native';
import { View, Text } from 'react-native';
import { HomeTitleContext } from './HomeTitleContext';
const HomeScreen = ({ navigation }) => {
const { setTitle } = useContext(HomeTitleContext);
useFocusEffect(() => {
setTitle('this is home');
});
return (
<View>
<Text>Home</Text>
</View>
);
};