Executando verificação de segurança...
8

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 e ResetPassword: Devem ser públicas, porém, a tela ResetPassword deve estar acessível também quando o usuário estiver autenticado, permitindo que seja acessada através de um botão na tela Profile.
  • 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 e Groups: Devem ser privadas e acessíveis para usuários validados através do Bottom Tabs Menu.
  • AllPosts e FavoritePosts: Devem ser privadas e acessíveis para usuários validados através do Top Tabs Menu que deve ficar localizado na tela Feed.
  • PostForm: Deve ser privada e acessível para usuários validados através de um botão disponível na tela AllPosts, porém, deve cobrir toda a tela, ocultando todos os outros navegadores e o header.
  • Profile e Settings: 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 tela SignUpComplement.

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 do RootStack.
  • A tela Private é substituída pelo DrawerNavigator
  • Criamos um grupo (RootStack.Group) para as telas que devem se comportar como modais e adicionamos a tela PostForm. É importante ressaltar que a tela PostForm precisa ser montada no RootStack para conseguir cobrir toda a tela, caso fosse montada dentro do DrawerNavigator, por exemplo, o header e o Drawer Menu continuariam visiveis.
  • A tela Public é substituída pelas três telas públicas: SignIn, SignUp e ResetPassword.

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 e FeedNavigator/GroupsNavigator. Como discutiremos a seguir, o "header principal" sempre será o dos navegadores internos do BottomTabNavigator, neste caso FeedNavigator e GroupsNavigator, sendo assim, desativamos o header dos navegadores DrawerNavigator e BottomTabNavigator.
  • As telas Profile e Settings do DrawerNavigator são substituídas pelo navegador BottomTabNavigator.
  • O BottomTabNavigator monta dois navegadores: FeedNavigator e GroupsNavigator.
  • Os navegadores FeedNavigator e GroupsNavigator montam as telas Feed e Groups.

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:

  1. Os headers dos navegadores FeedNavigator e GroupsNavigator não exibem o botão de abrir o Drawer Menu.
  2. As telas Profile e Settings 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:

  1. A função getDefaultHeaderOptions retorna um objeto com a propriedade headerLeft, que é responsável por definir qual componente será exibido no lado esquerdo do header. Se o parâmetro canGoBack for verdadeiro é verificado se a plataforma é Web, caso for, retornamos um botão que chama a função goBack, caso contrário, é retornado undefined, fazendo com que o header retorne o valor padrão, neste caso o "botão voltar" específico de cada plataforma. Se canGoBack for falso é retornado um botão que chama o método openDrawer, que como como o nome já diz "abre" o Drawer Menu. É importante ressaltar que getDefaultHeaderOptions precisa ser chamado em todos os navegadores do BottomTabNavigator para que o header funcione corretamente.
  2. Aqui é um ponto que pode "bugar sua cabeça". As telas Profile e Settings agora são montadas no FeedNavigator, 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.
  3. Por fim você deve se lembrar que a tela ResetPassword deve estar disponível através de um botão visível na tela Profile, então além de Profile e Settings montamos também ResetPassword dentro do FeedNavigator. Um ponto de atenção aqui, em um cenário real onde não passaríamos Screen como componente mas sim algo como ResetPasswordScreen, 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!

  1. 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.
  2. 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!!!

Carregando publicação patrocinada...
2

Ótimo post!
A única coisa que eu acho que está faltando e deveria ter nos seus exemplos é o Typescript, afinal convenhamos que em pleno 2022, ano da tecnologia, começar um projeto sem Typescript é arrumar sarna pra se coçar
Comecei um novo projeto hibrido (Web, Android, IOS) na empresa e ao invés do React Router que a gente geralmente usava resolvi usar o React Navigation
Infelizmente ele vem com alguns defaults pensados no mobile que dificultam a configuração Web, além da falta de exemplos e a integração meia boca com o Typescript
Mas ainda assim compensou pelo simples fato de poder usar as transições entre páginas mesmo na web

1

Fala Thiago!

Eu deixei de usar o TS para focar na estruturação do Navigation, mas com ctz começar um projeto em JS é loucura hahaha.

É, infelizmente o Navigation ainda é bem capenga para web, oferece os recursos mais básicos, mas acredito que supra as maiores dores, e de quebra o mesmo código funciona bem no mobile. E realmente, a integração com o TS é trash, principalmente quando começa a ficar complexo e surge a necessidade de usar coisas como CompositeScreenProps.