Tutorial de React Navigation 6: Native Stack, Drawer Menu, Bottom Tabs Menu e Top Tabs Menu, como juntar tudo?
Se você é programador React Native muito provavelmente utiliza a biblioteca React Navigation para estruturar toda a navegação/roteamento do seu aplicativo, mas conforme mais e mais telas e navegadores vão sendo aninhados a coisa começa a ficar complexa e as dúvidas começam a aparecer:
- Em que lugar devo colocar essa nova tela?
- Preciso adicionar outro navegador?
- Como faço para o meu aplicativo possuir apenas um header?
- Como faço para o montar telas sem que o Bottom Tabs Menu desapareça?
- ...
Pois bem, é exatamente isso que vamos tratar neste artigo! Veremos como criar um aplicativo que possui telas públicas e privadas, utilizando diversos navegadores combinados e com configurações diferentes para cada cenário. Um conceito importante que precisamos estar cientes é: "telas públicas" e "telas privadas", que resume-se basicamente em:
Telas públicas podem ser acessadas exclusivamente por usuários não autenticados, enquanto telas privadas são o oposto, podendo ser acessadas exclusivamente por usuários autenticados.
Note o "exclusivamente", isso quer dizer que um usuário autenticado não pode acessar uma tela pública. Porém, existem exceções, como por exemplo a tela ResetPassword que veremos a seguir, que pode ser acessada tanto por usuários autenticados quanto por usuários não autenticados. Gosto de chamar esse tipo de tela de "híbrida" pois ela precisa se adequar a cada cenário.
Definindo a estrutura do aplicativo
Agora vamos definir de forma clara e objetiva quais telas nosso aplicativo deve ter e como elas devem ser comportar:
SignIn,SignUpeResetPassword: Devem ser públicas, porém, a telaResetPassworddeve estar acessível também quando o usuário estiver autenticado, permitindo que seja acessada através de um botão na telaProfile.SignUpComplement: Deve ser privada e acessível exclusivamente caso o usuário não tenha completado o seu cadastro. Ao finalizar o processo com sucesso, o usuário é considerado "validado".FeedeGroups: Devem ser privadas e acessíveis para usuários validados através do Bottom Tabs Menu.AllPostseFavoritePosts: Devem ser privadas e acessíveis para usuários validados através do Top Tabs Menu que deve ficar localizado na telaFeed.PostForm: Deve ser privada e acessível para usuários validados através de um botão disponível na telaAllPosts, porém, deve cobrir toda a tela, ocultando todos os outros navegadores e o header.ProfileeSettings: Devem ser privadas e acessíveis para usuários validados através do Drawer Menu, porém, temos um requisito importante aqui, queremos que o Bottom Tabs Menu permaneça visível quando alguma dessas telas for acessada.
Observe os textos em itálico, a descrição das telas cita os três navegadores que utilizaremos: Bottom Tabs Menu (BottomTabNavigator), Top Tabs Menu (MaterialTopTabNavigator) e Drawer Menu (DrawerNavigator). Todo o resto da estrutura utiliza o "navegador base" Native Stack (NativeStackNavigator).
Muito bem, vamos ao código! 🔥
Criando o navegador raiz e definindo as regras de negócio
const RootStack = createNativeStackNavigator();
function RootNavigator() {
const {
state: { isAuthenticated, isSignUpValidated },
} = useAuth();
return (
<NavigationContainer>
<RootStack.Navigator>
{isAuthenticated ? (
isSignUpValidated ? (
<RootStack.Screen name="Private" component={Screen} />
) : (
<RootStack.Screen name="SignUpComplement" component={Screen} />
)
) : (
<RootStack.Screen name="Public" component={Screen} />
)}
</RootStack.Navigator>
</NavigationContainer>
);
}
Por hora não utilizamos os nomes das telas definidos anteriormente para facilitar o entendimento.
Criamos um NativeStackNavigator com o nome RootStack e definimos as regras de negócio:
- Se o usuário esta autenticado, verifique se ele é um usuário validado, caso contrário, monte a tela
Public. - Se o usuário está validado, monte a tela
Private, caso contrário, monte a telaSignUpComplement.
Desta forma garantimos que apenas as telas que devem estar acessíveis em cada cenário estarão montadas e disponíveis para navegação.
Dica: Se você ficou confuso com o
useAuthrecomendo a leitura do artigo How to use React Context Effectively.
Criando o Drawer Menu, a tela PostForm e as telas públicas
const RootStack = createNativeStackNavigator();
const Drawer = createDrawerNavigator();
function DrawerNavigator() {
return (
<Drawer.Navigator>
<Drawer.Screen name="Profile" component={Screen} />
<Drawer.Screen name="Settings" component={Screen} />
</Drawer.Navigator>
);
}
function RootNavigator() {
const {
state: { isAuthenticated, isSignUpValidated },
} = useAuth();
return (
<NavigationContainer>
<RootStack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
isSignUpValidated ? (
<>
<RootStack.Screen
name="DrawerNavigator"
component={DrawerNavigator}
/>
<RootStack.Group
screenOptions={{ presentation: 'fullScreenModal' }}>
<RootStack.Screen name="PostForm" component={Screen} />
</RootStack.Group>
</>
) : (
<RootStack.Screen name="SignUpComplement" component={Screen} />
)
) : (
<>
<RootStack.Screen name="SignIn" component={Screen} />
<RootStack.Screen name="SignUp" component={Screen} />
<RootStack.Screen name="ResetPassword" component={Screen} />
</>
)}
</RootStack.Navigator>
</NavigationContainer>
);
}
É criado o DrawerNavigator, a tela PostForm e as telas públicas definidas anteriormente:
- Cada navegador possui seu próprio header, com a criação do
DrawerNavigatorficamos agora com dois headers, sendo assim, desativamos o header doRootStack. - A tela
Privateé substituída peloDrawerNavigator. - Criamos um grupo (
RootStack.Group) para as telas que devem se comportar como modais e adicionamos a telaPostForm. É importante ressaltar que a telaPostFormprecisa ser montada noRootStackpara conseguir cobrir toda a tela, caso fosse montada dentro doDrawerNavigator, por exemplo, o header e o Drawer Menu continuariam visiveis. - A tela
Publicé substituída pelas três telas públicas:SignIn,SignUpeResetPassword.
Criando o Bottom Tabs Menu e seus navegadores internos
const Drawer = createDrawerNavigator();
const BottomTab = createBottomTabNavigator();
const FeedStack = createNativeStackNavigator();
const GroupsStack = createNativeStackNavigator();
function GroupsNavigator() {
return (
<GroupsStack.Navigator>
<GroupsStack.Screen name="Groups" component={Screen} />
</GroupsStack.Navigator>
);
}
function FeedNavigator() {
return (
<FeedStack.Navigator>
<FeedStack.Screen name="Feed" component={Screen} />
</FeedStack.Navigator>
);
}
function BottomTabNavigator() {
return (
<BottomTab.Navigator screenOptions={{ headerShown: false }}>
<BottomTab.Screen name="FeedNavigator" component={FeedNavigator} />
<BottomTab.Screen name="GroupsNavigator" component={GroupsNavigator} />
</BottomTab.Navigator>
);
}
function DrawerNavigator() {
return (
<Drawer.Navigator screenOptions={{ headerShown: false }}>
<Drawer.Screen name="BottomTabNavigator" component={BottomTabNavigator} />
</Drawer.Navigator>
);
}
Por ora deixaremos de lado o RootNavigator. Aqui realizamos uma etapa que pode ser bem confusa, vamos ponto a ponto:
- Cada navegador possui seu próprio header e agora estamos com três headers visíveis simultaneamente:
DrawerNavigator,BottomTabNavigatoreFeedNavigator/GroupsNavigator. Como discutiremos a seguir, o "header principal" sempre será o dos navegadores internos doBottomTabNavigator, neste casoFeedNavigatoreGroupsNavigator, sendo assim, desativamos o header dos navegadoresDrawerNavigatoreBottomTabNavigator. - As telas
ProfileeSettingsdoDrawerNavigatorsão substituídas pelo navegadorBottomTabNavigator. - O
BottomTabNavigatormonta dois navegadores:FeedNavigatoreGroupsNavigator. - Os navegadores
FeedNavigatoreGroupsNavigatormontam as telasFeedeGroups.
O maior ponto de dúvida que comumente surge aqui é, "Por que criamos os navegadores FeedNavigator e GroupsNavigator, ao invés de simplesmente passar as telas Feed e Groups para o BottomTabNavigator?"
Sempre que utilizamos o navegador do tipo BottomTab é importante criarmos um NativeStackNavigator para cada aba, fazendo com que tenhamos estados de navegação independentes, ou seja, cada aba possui seu próprio header e histórico de navegação.
Exemplificando, quando o usuário navega para uma tela montada em FeedNavigator nada acontece com o GroupsNavigator, enquanto o header do FeedNavigator é atualizado de acordo, exibindo o botão voltar e o título correto.
Se você estava atento deve estar se perguntando, "Para onde as telas Profile e Settings foram?", não se preocupe, vamos abordar isso na próxima etapa.
Configurando o Drawer Menu
Com as mudanças feitas anteriormente temos dois problemas:
- Os headers dos navegadores
FeedNavigatoreGroupsNavigatornão exibem o botão de abrir o Drawer Menu. - As telas
ProfileeSettingsdevem ser montadas em algum navegador, caso contrário não há como navegar para elas.
function getDefaultHeaderOptions({ navigation: { openDrawer, goBack } }) {
return {
headerLeft: ({ canGoBack }) => {
if (canGoBack) {
if (Platform.OS === 'web') {
return <Button title="Voltar" onPress={goBack} />;
} else {
return undefined;
}
}
return <Button title="Drawer" onPress={openDrawer} />;
},
};
}
function FeedNavigator() {
return (
<FeedStack.Navigator
screenOptions={(props) => getDefaultHeaderOptions(props)}>
<FeedStack.Screen name="Feed" component={Screen} />
<FeedStack.Screen name="Profile" component={Screen} />
<FeedStack.Screen name="Settings" component={Screen} />
<FeedStack.Screen name="ResetPassword" component={Screen} />
</FeedStack.Navigator>
);
}
function GroupsNavigator() {
return (
<GroupsStack.Navigator
screenOptions={(props) => getDefaultHeaderOptions(props)}>
<GroupsStack.Screen name="Groups" component={Screen} />
</GroupsStack.Navigator>
);
}
Vamos entender como ambos os problemas foram solucionados:
- A função
getDefaultHeaderOptionsretorna um objeto com a propriedadeheaderLeft, que é responsável por definir qual componente será exibido no lado esquerdo do header. Se o parâmetrocanGoBackfor verdadeiro é verificado se a plataforma é Web, caso for, retornamos um botão que chama a funçãogoBack, caso contrário, é retornadoundefined, fazendo com que o header retorne o valor padrão, neste caso o "botão voltar" específico de cada plataforma. SecanGoBackfor falso é retornado um botão que chama o métodoopenDrawer, que como como o nome já diz "abre" o Drawer Menu. É importante ressaltar quegetDefaultHeaderOptionsprecisa ser chamado em todos os navegadores doBottomTabNavigatorpara que o header funcione corretamente. - Aqui é um ponto que pode "bugar sua cabeça". As telas
ProfileeSettingsagora são montadas noFeedNavigator, ou seja, sempre que você navegar para uma tela do Drawer Menu ela será montada na primeira opção do Bottom Tabs Menu. A melhor forma de entender o comportamento disso tudo é testando, e se você esta se perguntando, "Essa é a melhor abordagem?", eu realmente não sei, mas apps como o LinkedIn a utilizam. - Por fim você deve se lembrar que a tela
ResetPassworddeve estar disponível através de um botão visível na telaProfile, então além deProfileeSettingsmontamos tambémResetPassworddentro doFeedNavigator. Um ponto de atenção aqui, em um cenário real onde não passaríamosScreencomo componente mas sim algo comoResetPasswordScreen, devemos utilizar o mesmo componente tanto para a tela privada quanto para a tela pública, fazendo com que o componente seja responsável por se adequar a cada cenário.
Resolvidos esses dois problemas agora temos um novo, o Drawer Menu exibe o item "BottomTabNavigator" ao invés dos itens "Profile" e "Settings", isso acontece por que por padrão o Drawer Menu lista as telas/navegadores montados no DrawerNavigator. Para resolver isso é necessário passar um componente que mapeia os itens que precisam ser exibidos através da opção drawerContent do DrawerNavigator.
function DrawerContent(props) {
const routes = ['Profile', 'Settings'];
return (
<DrawerContentScrollView {...props}>
{routes.map((screen) => (
<DrawerItem
key={screen}
label={screen}
onPress={() => props.navigation.navigate(screen)}
/>
))}
</DrawerContentScrollView>
);
}
function DrawerNavigator() {
return (
<Drawer.Navigator
screenOptions={{ headerShown: false }}
drawerContent={(props) => <DrawerContent {...props} />}>
<Drawer.Screen name="BottomTabNavigator" component={BottomTabNavigator} />
</Drawer.Navigator>
);
}
Aqui você pode ter notado um novo problema, por padrão quando se navega para uma tela do Drawer Menu a opção selecionada fica "focada" para dar contexto ao usuário, porém, o componente DrawerContent não implementa esse comportamento. Esse não é um problema simples de resolver e implica em alguns cenários, mas para exemplificar temos que acessar o estado de navegação do FeedNavigator para saber qual tela esta sendo exibida no momento.
function DrawerContent(props) {
const routes = ['Profile', 'Settings'];
const bottomTabNavigator = props.state.routes.find(
({ name }) => name === 'BottomTabNavigator'
);
const feedNavigator = bottomTabNavigator.state?.routes.find(
({ name }) => name === 'FeedNavigator'
);
const currentScreen =
feedNavigator?.state?.routes[feedNavigator.state.index].name;
return (
<DrawerContentScrollView {...props}>
{routes.map((screen) => (
<DrawerItem
focused={screen === currentScreen}
key={screen}
label={screen}
onPress={() => props.navigation.navigate(screen)}
/>
))}
</DrawerContentScrollView>
);
}
Criando o Top Tabs Menu
const FeedStack = createNativeStackNavigator();
const TopTabFeedStack = createMaterialTopTabNavigator();
function TopTabFeedNavigator() {
return (
<TopTabFeedStack.Navigator>
<TopTabFeedStack.Screen name="AllPosts" component={Screen} />
<TopTabFeedStack.Screen name="FavoritePosts" component={Screen} />
</TopTabFeedStack.Navigator>
);
}
function FeedNavigator() {
return (
<FeedStack.Navigator
screenOptions={(props) => getDefaultHeaderOptions(props)}>
<FeedStack.Screen name="Feed" component={TopTabFeedNavigator} />
<FeedStack.Screen name="Profile" component={Screen} />
<FeedStack.Screen name="Settings" component={Screen} />
<FeedStack.Screen name="ResetPassword" component={Screen} />
</FeedStack.Navigator>
);
}
É criado o navegador TopTabFeedNavigator onde montamos as telas AllPosts e FavoritePosts, e depois substituímos o componente da tela Feed do FeedNavigator por TopTabFeedNavigator, desta forma sempre que a tela Feed for montada todas as telas montadas em TopTabFeedNavigator serão exibidas, seguindo a mesma lógica utilizada na seção "Criando o Bottom Tabs Menu e seus navegadores internos".
Resultado final!
Veja o resultado final no Expo Snack!
Certos trechos de código foram omitidos no artigo a fim de simplificar a explicação.
Peço também a ajuda de vocês para duas coisas!
- Eu não tive tempo de fazer uma revisão gramatical, e na verdade nem sou uma boa pessoa para fazer isso hahaha, então se você ver algo errado ou difícil de entender por favor diga nos comentários.
- Se você manja de React Native/React Navigation e viu algo errado por favor me diga, muito do que escrevi aqui é baseado na minha experiência.
Vlw!!!