Best practices when using Drawer Navigator with react navigation - reactjs

I'm building an app with the typical Navigator, which contains a Welcome Screen, Login Screen, Signups, etc. When a user is authenticated, I'm then switching to a Drawer Navigation. However, I'm not entirely sure I'm implementing the Drawer Navigator correctly.
Sample rootNavigator:
const RootNavigator = () => {
const app = useSelector(state => state.app);
const isLoggedIn = app.user ? true : false;
const showWelcome = app.showWelcome;
if (isLoggedIn) {
return (
<Navigator headerMode="none">
<Screen name="Home" component={AppDrawerNavigator} />
</Navigator>
);
}
return (
<Navigator
headerMode="none"
initialRouteName={showWelcome ? 'Walkthrough' : 'Welcome'}>
<Screen name="Walkthrough" component={WalkthroughScreen} />
<Screen name="Welcome" component={WelcomeScreen} />
<Screen name="Login" component={LoginScreen} />
<Screen name="Signup" component={SignupScreen} />
<Screen name="ForgotPassword" component={ForgotPasswordScreen} />
<Screen name="ResetPassword" component={ResetPasswordScreen} />
</Navigator>
);
};
Then, when a user is logged in, I'm returning the following for the AppDrawerNavigator:
const AppDrawerNavigator = () => {
const insets = useSafeAreaInsets();
return (
<SafeAreaView
mode="padding"
style={[
styles.safeArea,
{ paddingTop: insets.top, paddingBottom: insets.bottom },
]}
edges={['right', 'left']}>
<Drawer.Navigator
screenOptions={{
drawerType: 'front',
gestureEnabled: true,
swipeEnabled: false,
}}
drawerContent={props => <HomeDrawer {...props} />}>
{/* Screens */}
<Drawer.Screen name="Landing" component={LandingScreen} />
<Drawer.Screen name="Profile" component={ProfileScreen} />
</Drawer.Navigator>
</SafeAreaView>
);
};
The downside that I'm noticing with this type of implementation is on any of the screens where I try to detect the current view, the route returned is "Home", which appears to be an issue of how I've implemented the drawer.
What is the best way to go about this problem; how I should render the Drawer Navigation?

Related

How to pass a value from headerRight component from navigator to nested navigator's screen in react native?

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

Drawer's screen component is re-rendering when a drawer opens/closes

I have the main Stack navigator with multiple screens and one of those screens is a nested Drawer navigator with dynamically created screens.
<Provider store={store}>
<NavigationContainer onStateChange={onStateChange}>
<Stack.Navigator initialRouteName="Launch">
<Stack.Screen name="Launch" component={Launch} />
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="ChatBox" component={ChatBox} /> /* <- Here is the drawer navigator screen */
</Stack.Navigator>
</NavigationContainer>
</Provider>
As you can see in the example gif the ChatComponent is always re-rendering when drawer actions were. It does even if the current screen hasn't changed in the drawer's list.
<Drawer.Navigator>
{userChannels.map((channel, index) => {
const chatName = userChannels[index].friendlyName;
const screen = React.memo(() => (
<View style={{ flex: 1 }}>
<Header text={chatName} />
<ChatComponent
channel={channel}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={onLoadEarlier}
savedMessages={savedMessages}
onSend={onSend}
isTyping={isTyping}
user={{
_id: me.member.sid || 0,
}}
onInputTextChanged={async () => {
currentChannel &&
(await currentChannel.typing());
}}
enterChannel={() =>
enterChannel(channel.uniqueName)
}
/>
</View>
));
return (
<Drawer.Screen
key={index}
name={chatName || String(index)}
component={screen}
/>
);
})}
</Drawer.Navigator>
The header component is:
export default (props) => {
const { text } = props;
const navigation = useNavigation();
const logout = async () => {
try {
await AsyncStorage.removeItem('accessToken');
await AsyncStorage.removeItem('refreshToken');
} catch (e) {
//
}
navigation.navigate('Login');
};
return (
<View style={styles.header}>
<TouchableOpacity
onPress={() => navigation.dispatch(DrawerActions.openDrawer())}>
<Icon name="menu" color="#FFF" size={18} />
</TouchableOpacity>
{text && <Text style={styles.headerText}>{text}</Text>}
<Text style={styles.headerText} onPress={logout}>
Logout
</Text>
</View>
);
};
The chat component is:
const ChatComponent = React.memo((props) => {
const {
channel,
loadEarlier,
isLoadingEarlier,
onLoadEarlier,
savedMessages,
onSend,
isTyping,
user,
onInputTextChanged,
enterChannel,
} = props;
useEffect(() => {
console.log('render ChatComponent');
enterChannel();
}, []);
return (
<GiftedChat
inverted={false}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={onLoadEarlier}
messages={savedMessages}
onSend={onSend}
user={user}
onInputTextChanged={onInputTextChanged}
isTyping={isTyping}
renderBubble={renderBubble}
renderTime={renderTime}
renderInputToolbar={renderInputToolbar}
/>
);
});
Here is the onStateChange of NavigationContainer
How to avoid unnecessary renders of a ChatComponent?

Unable to navigate to drawer stack in react navigation 5

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} />

Open Drawer of child from parent React Navigation V5

I have the main navigation of App setup. Here I initialize route stacks which can have drawer, stacks or tabs stacks.
navigation.js
const Stack = createStackNavigator();
const AppNavigation = () => {
return (
<NavigationContainer style={{backgroundColor: 'red'}}>
<Stack.Navigator headerMode="screen">
<Stack.Screen
options={{headerShown: false}}
name="Auth"
component={AuthRoutes}
/>
<Stack.Screen
name="collector"
component={CollectorRoutes}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
Problem is when I am trying to change the header from one of the stacks , I am not able to change the header settings of that stack like:
collector.routing.js
const Drawer = createDrawerNavigator();
const CollectorRoutes = () => {
return (
<Drawer.Navigator options ={ // options here also have no effect}>
<Drawer.Screen
options={({navigation}) => ({ // this has no effect
headerTitle: props => (
<View>
<Text>HEDAER</Text>
</View>
),
headerLeft: props => (
<Button
block
style={{backgroundColor: themeStyle.textColor}}
onPress={() => navigation.toggleDrawer()}>
<Text>Login</Text>
</Button>
),
})}
name="list"
component={Collector}
/>
</Drawer.Navigator>
);
};
However if I give header options in navigation.js on the scree on CollectorRoutes then the headers are customized.
But then one more problems comes in, I cannot toggle the drawer from there as the navigation stack of child drawer stack is not accessible in parent.
navigation.js -> with header enabled for DrawerStack (CollectorRoutes)
const Stack = createStackNavigator();
const AppNavigation = () => {
return (
<NavigationContainer style={{backgroundColor: 'red'}}>
<Stack.Navigator headerMode="screen">
<Stack.Screen
options={{headerShown: false}}
name="Auth"
component={AuthRoutes}
/>
<Stack.Screen
options={({navigation}) => ({ // this setting gives me header in drawer stack
headerTitle: props => (
<View>
<Text>HEDAER</Text>
</View>
),
headerLeft: props => (
<Button
block
style={{backgroundColor: themeStyle.textColor}}
onPress={() => navigation.toggleDrawer()}> // but here I cannot access drawer toggle
<Text>Login</Text>
</Button>
),
})}
name="collector"
component={CollectorRoutes}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
Is there any solution to access the drawer toggle in the parent? Or can we somehow instead of giving the header option in parent we can configure it in child?

DrawerActions.toggleDrawer() from Parent Navigator

I have a nested navigator structure as follows - the Parent element is a Stack Navigator with a Drawer nested inside. I need to toggle the drawer open from a button in the Stack Navigator.
const Drawer = createDrawerNavigator();
function DrawerNav() {
return (
<Drawer.Navigator initialRouteName="Home">
<Drawer.Screen
name="Main"
component={HomeScreen}
/>
<Drawer.Screen
name="Details"
component={DetailsScreen}
/>
<Drawer.Screen
name="About"
component={AboutScreen}
/>
</Drawer.Navigator
);
}
const Stack = createStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="DrawerNav"
>
<Stack.Screen
name="DrawerNav"
component={DrawerNav}
options={{
headerLeft: () => (
<Icon
style={{ padding: 10 }}
name="menu"
size={30}
color="#900"
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
/>
),
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
The current onpress in the Stack Navigator is the closest I got to something that should work to toggle the child drawer. However, adding
const navigation = useNavigation();
Into the App() function creates the following error
Error: Couldn't find a navigation object. Is your component inside a screen in a navigator?\\
In summation, I need to toggle the drawer navigation though a button in its parent. It is clear how to pass navigation actions to children but I am having a difficult time doing it the other way.
In options you need to extract the navigation argument:
const Drawer = createDrawerNavigator();
function DrawerNav() {
return (
<Drawer.Navigator initialRouteName="Home">
<Drawer.Screen
name="Main"
component={HomeScreen}
/>
<Drawer.Screen
name="Details"
component={DetailsScreen}
/>
<Drawer.Screen
name="About"
component={AboutScreen}
/>
</Drawer.Navigator
);
}
const Stack = createStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator
initialRouteName="DrawerNav"
>
<Stack.Screen
name="DrawerNav"
component={DrawerNav}
options={({ navigation }) => ({
headerLeft: () => (
<Icon
style={{ padding: 10 }}
name="menu"
size={30}
color="#900"
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
/>
),
})}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
DrawerActions must be imported, so
import { NavigationContainer, DrawerActions } from '#react-navigation/native';
should be in your imports, other than that,
onPress={() => this.props.navigation.dispatch(DrawerActions.toggleDrawer())}
does the trick.
I know this is an old topic, but I thought I would offer some clarification.
Read up on Drawer Actions -> https://reactnavigation.org/docs/drawer-actions/
Try this out, i was stuck on the same issue and this worked for me :
<Stack.Screen
name="screen name"
component={ScreenComponent}
options={({ navigation }) => ({
headerRight: (props) => {
return <Button onPress={() => navigation.toggleDrawer() }} />
}
})}
/>
You can toggle Drawer by Button click using the following code-
onPress={() => this.props.navigation.dispatch(DrawerActions.toggleDrawer())}
`
options can take a function and provide arguments for you, one of which is navigation. Here's the code:
options={({navigation}) => {
headerLeft: () => (
<Icon
style={{ padding: 10 }}
name="menu"
size={30}
color="#900"
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
/>
),
}}
Hope this helps.

Resources