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
,SignUp
eResetPassword
: Devem ser públicas, porém, a telaResetPassword
deve 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".Feed
eGroups
: Devem ser privadas e acessíveis para usuários validados através do Bottom Tabs Menu.AllPosts
eFavoritePosts
: 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.Profile
eSettings
: 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
useAuth
recomendo 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
DrawerNavigator
ficamos 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 telaPostForm
precisa ser montada noRootStack
para 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
,SignUp
eResetPassword
.
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
,BottomTabNavigator
eFeedNavigator
/GroupsNavigator
. Como discutiremos a seguir, o "header principal" sempre será o dos navegadores internos doBottomTabNavigator
, neste casoFeedNavigator
eGroupsNavigator
, sendo assim, desativamos o header dos navegadoresDrawerNavigator
eBottomTabNavigator
. - As telas
Profile
eSettings
doDrawerNavigator
são substituídas pelo navegadorBottomTabNavigator
. - O
BottomTabNavigator
monta dois navegadores:FeedNavigator
eGroupsNavigator
. - Os navegadores
FeedNavigator
eGroupsNavigator
montam as telasFeed
eGroups
.
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
FeedNavigator
eGroupsNavigator
não exibem o botão de abrir o Drawer Menu. - As telas
Profile
eSettings
devem 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
getDefaultHeaderOptions
retorna um objeto com a propriedadeheaderLeft
, que é responsável por definir qual componente será exibido no lado esquerdo do header. Se o parâmetrocanGoBack
for 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. SecanGoBack
for falso é retornado um botão que chama o métodoopenDrawer
, que como como o nome já diz "abre" o Drawer Menu. É importante ressaltar quegetDefaultHeaderOptions
precisa ser chamado em todos os navegadores doBottomTabNavigator
para que o header funcione corretamente. - Aqui é um ponto que pode "bugar sua cabeça". As telas
Profile
eSettings
agora 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
ResetPassword
deve estar disponível através de um botão visível na telaProfile
, então além deProfile
eSettings
montamos tambémResetPassword
dentro doFeedNavigator
. Um ponto de atenção aqui, em um cenário real onde não passaríamosScreen
como 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!!!