Falaaaa Dev!!! 👊

Introdução

Vamos construir juntos a funcionalidade completa de autenticação dentro do React Native, mostrando como que a gente faz a chamada API, como pega o token, usuário, como armazena os dados no Async Storage, como criar um contexto para deixar essas informações disponibilizadas em toda aplicação, como  criar um custom hook, como controlar o estado de loading das informações que ainda não foram recuperadas do Storage, aonde que a gente faz autenticação, onde que a gente faz logout,  o que a gente faz com Async Storage no momento que a gente fizer SignIn, SignOut ou quando a tela principal é carregada, como determinar qual stack de rotas que vamos utilizar de acordo com estado do usuário logado ou deslogado. Uauu, fluxo completo!

Estou muito empolgado com o conteúdo, vamos falar sobre tecnologias bem legais como React, React Native, a fantástica Context API, Hooks (Effect/State), Typescript, e ainda vamos criar o nosso Custom Hook!

Vai ser um passo a passo bem mastigadinho e completo!

Então, bora codar! 👨‍💻👩‍💻

Se o seu ambiente de desenvolvimento mobile com React Native não estiver pronto, você pode fazer configurar seguindo os passos doc de ambiente React Native

Criando o Projeto em React Native com Typescript

Vamos começar criando projeto React Native com Typescript, a melhor maneira de criar o projeto é executar o comando com npx:

npx react-native init authrn --template react-native-template-typescript

Com npx ele busca o pacote na web instala na sua máquina na versão mais atualizada, executa o comando react-native, deixa em cache por um tempo e depois desinstala, dessa forma você não precisa ficar com o react-native cli na node_modules principal da sua máquina.

Vamos utilizar typescript por isso passei a flag: --template react-native-template-typescript

Eu vou utilizar o simulador do iOS aqui, você pode utilizar o do Android ou até mesmo criar um projeto com Expo, isso não vai impedir de você seguir o tutorial.

Então depois de criar o projeto, posso executar:

cd authrn && yarn ios

Esse comando vai abrir a pasta do projeto e rodar o comando que faz o build no simulador do iOS, por baixo dos panos seria um npx react-native run-ios ou react-native run-ios.

Quando projeto estiver instalado e rodando você vai ver na tela o metro bundler e o simulador.

Metro bundler é o webpack do react native, responsável pela conversão do código typescript -> javascript otimizado, para ficar legível pelo react native. Se a janela não abrir, basta você rodar o comando yarn start que provavelmente já vai estar funcionando.

Agora só abrir o projeto com vscode ou seu editor preferido!

Codando e estruturando o projeto

Deleto o arquivo App.tsx da raiz do projeto e crio uma pasta src/ e o arquivo App.tsx dentro da pasta.

Dentro do arquivo App.tsx coloco:

import React from 'react';
import {View} from 'react-native';

const App: React.FC = () => {
 return <View />;
};

export default App;

E no arquivo index.js na raiz do projeto altero o import do App:

import App from './src/App';

Se der algum erro nessa parte, fecha o metro bundler e tenta executar novamente:

yarn  start  --reset-cache

O app vai ficar em branco no emulador ou simulador, mas agora vamos instalar algumas libs (bibliotecas) para continuar o app.

Vamos instalar React Navigation  que é o módulo de navegação no React Native.

yarn add @react-navigation/native

E depois instalamos algumas libs auxiliares:

yarn add react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view

Se você estiver no macbook e estiver rodando no simulador do iOS precisa rodar:

cd ios && pod install

No Android já está tudo certo! 🙂

Agora falta adicionar o import 'react-native-gesture-handler'; no comecinho do arquivo principal, para deixar a tela touchable (tocável):

src/App.tsx:

import 'react-native-gesture-handler';
// ... restante do código omitido

E por fim, nosso App tem que ser envolvido pelo NavigationContainer conforme a documentação.

Na tela não surge nenhum efeito e o nosso código fica assim:

src/App.tsx:

import 'react-native-gesture-handler';
import React from "react";
import { View } from "react-native";
import { NavigationContainer } from "@react-navigation/native";

const App: React.FC = () => {
  return (
    <NavigationContainer>
      <View />
    </NavigationContainer>
  );
};

export default App;

Depois devemos executar o comando npx react-native run-ios ou npx react-native run-android devido a instalação de bibliotecas nativas que fizemos.

Na seção hello-react-navigation, a documentação nos ensina como configurar cada tipo de navegação.

A mais utilizada é a Stack Navigation, que para cada clique ou uma ação de navegação, ela vai sendo salva em uma pilha, e as rotas são empilheiradas, você pode voltar sempre para o estado anterior. Você pode ler um post da Rockeseat sobre navegação.

Vamos instalar a Stack Navigation:

npm install @react-navigation/stack

Agora vamos criar nossas páginas e as rotas:

Dentro da pasta src crio as pastas pages e routes, dentro de pages vai ficar a pasta SignIn com o arquivo index.tsx e dentro de Dashboard o arquivo index.tsx.

Estrutura de pastas do projeto

O Dashboard vai ser acessado após o login (autenticação).

Por enquanto os arquivos vão ser bem simples mesmo.

src/SignIn/index.tsx:

import React from "react";
import { View } from "react-native";

const SignIn: React.FC = () => <View />;

export default SignIn;

src/Dashboard/index.tsx:

import React from "react";
import { View } from "react-native";

const Dashboard: React.FC = () => <View />;

export default Dashboard;

Na pasta routes, vamos criar dois arquivos: app.routes.tsx e auth.routes.tsx.

No arquivo auth.routes.tsx ficarão as rotas não autenticadas, ou seja, as rotas que o usuário pode acessar sem precisar estar logado, o no app.routes.tsx as rotas autenticadas, que só após o logon será possível acessa-las.

import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';

import SignIn from '../pages/SignIn';

const AuthStack = createStackNavigator();

const AuthRoutes: React.FC = () => (
  <AuthStack.Navigator>
    <AuthStack.Screen name="SignIn" component={SignIn} />
  </AuthStack.Navigator>
);

export default AuthRoutes;

createStackNavigator é a função que cria a navegação em pilha, eu invoco essa função passando o retorno dela para constante AuthStack. AuthStack se torna um componente que possuí um Navigator e Screen.

Sempre o Navigator vai envolver a Screen, e a Screen recebe duas propriedades: name e component, o name pode ser qualquer nome, geralmente uso o mesmo nome do componente e a propriedade component recebe o componente em si,  o name vai ser referenciado sempre que quisermos navegar para o componente.

AuthRoutes é o nome que dei para o componente de rotas da aplicação, e exporto esse componente.

E o arquivo Dashboard.tsx vai ficar quase que do mesmo jeito:

import React from 'react';
import {createStackNavigator} from '@react-navigation/stack';

import Dashboard from '../pages/Dashboard';

const AppStack = createStackNavigator();

const AppRoutes: React.FC = () => (
  <AppStack.Navigator>
    <AppStack.Screen name="Dashboard" component={Dashboard} />
  </AppStack.Navigator>
);

export default AppRoutes;

Pronto, agora criamos as duas stacks de navegação, uma para usuário logado e outra para deslogado.

Função de Autenticação

Por hora, vamos criar uma função fake para retornar um token de autenticação, simulando o logon da aplicação.

Criei a pasta services e um arquivo auth.ts:

src/services/auth.ts:

export function signIn() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        token: 'jk12h3j21h3jk212h3jk12h3jkh12j3kh12k123hh21g3f12f3',
        name: 'Thiago',
        email: 'thiagomarinho@rockeseat.com.br',
      });
    }, 2000);
  });
}

Quando eu chamo essa função (sem parâmetro mesmo), ela devolve uma promise (promessa) que é resolvida depois de dois segundo, trazendo um objeto com atributos token, name e email.  Ficou simulando o login mesmo, certo!

Controle de Rotas da aplicação

Agora crio um arquivo index.tsx dentro da pasta routes que vai controlar qual a rota ficará disponível, ou seja, quando o usuário estiver logado ele o roteador deve disponibilizar as rotas de dentro do app.routes.tsx quando o usuário estiver deslogado o roteador deverá disponibilizar as rotas  de dentro de auth.routes.tsx.

src/routes/index.tsx:

import React from 'react';

import AuthRoutes from '../routes/auth.routes';
// import AppRoutes from '../routes/app.routes';

const Routes: React.FC = () => {
  return <AuthRoutes />;
};

export default Routes;

Por enquanto, estou criando apenas um componente Routes que retorna apenas o componente AuthRoutes, logo mais iremos validar qual será retornado, de acordo com usuário logado ou não.

Por hora, vamos apenas exibir na tela o conteúdo de dentro de SignIn, mas antes vou colocar um botão para exibir na tela:

src/pages/SignIn/index.tsx:

import React from 'react';
import {View, Button, StyleSheet} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
});

const SignIn: React.FC = () => (
  <View style={styles.container}>
    <Button title="Sign In" onPress={() => {}} />
  </View>
);

export default SignIn;

E dentro do arquivo src/App.tsx eu importo o Routes e utilizo-o.

import React from "react";
import { NavigationContainer } from "@react-navigation/native";

import Routes from "./routes";

const App: React.FC = () => {
  return (
    <NavigationContainer>
      <Routes />
    </NavigationContainer>
  );
};

export default App;

Dessa forma agora quando eu acessar a aplicação, o Routes vai ser chamado e dentro dele vai ser retornado o AuthRoutes  o qual retorna a navegação para o componente SignIn. Por enquanto esse é o fluxo.

Agora você já consegue ver um botão Sign In na tela, que ainda não faz nada porque não coloquei nenhuma função no onPress do botão.

Ah, veja que tem um header ali escrito SignIn em negrito, isso é automático feito pelo StackNavigator, podemos remover isso ou alterar o estilo, ele é bem customizável. 👌

Agora vamos precisamos dar vida ao clique do botão no Sign In.

Então vamos mudar um pouco o arquivo SignIn/index.tsx, vamos precisar invocar a função de signIn que criamos no arquivo src/services/auth.ts.

import React from "react";
import { View, Button, StyleSheet } from "react-native";

import { signIn } from "../../services/auth";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
});

const SignIn: React.FC = () => {
  async function handleSign() {
    // email, password (formulário omitido)
    const response = await signIn();
    console.log(response);
  }

  return (
    <View style={styles.container}>
      <Button title="Sign In" onPress={handleSign} />
    </View>
  );
};

export default SignIn;

Criei uma função handleSign que invoca a função signIn(), que retorna depois de dois segundos os dados do usuário logado e imprime no console do terminal do metro bundler. Como ela retorna uma promise, ela tem que ser assíncrona, depois eu coloquei a referência da função no onPress={handleSign}.

Pronto, agora o log do usuário aparece no terminal do metro bundler.

{"email": "thiagomarinho@rocketseat.com.br", "name": "Thiago", "token": "olkl12h3g12y2214kl123jh21gg"}

Tenho os dados do usuário, preciso armazenar em algum lugar para que sempre que ele navegar em alguma tela que requer autenticação ele possa entrar direto na tela ao invés de ser redirecionado para o login toda vez, vamos fazer isso mais para frente.

O Poder da Context API

Agora entra em ação o contexto, usaremos a Context API do React, poderia utilizar um Redux também, mas tem casos e casos para qual utilizar um ou outro, nessa aplicação pequena vale a pena manter o estado na Context API, é uma dependência a menos, já que ela vem com React e também ela está bem mais interessante de utilizar com os hooks, código fica mais coeso e menos verboso, você vai ver.

Vamos criar um pasta dentro de src chamada contexts para armazenar nossos hooks de contexto de autenticação. Dentro da pasta criaremos o  arquivo: auth.tsx

src/contexts/auth.tsx:

import { createContext } from "react";

const AuthContext = createContext({ signed: true });

export default AuthContext;

Crio o contexto usando a função createContext, atribuo para a AuthContext, a função createContext recebe um objeto, mesmo que o valor inicial seja vazio: {}, mas nesse momento vamos colocar signed: true,  só para testar, conforme a aplicação ir crescendo vamos mudando.

E no arquivo App.tsx importamos o AuthContext lá do nosso arquivo de auth.tsx, e definimos o Provider e o valor que queremos disponibilizar para todos os filhos abaixo do Provider de AuthContext.

<AuthContext.Provider value={{ signed: true }}>
//...
</AuthContext.Provider>

Por enquanto o signed:true está estático, logo iremos deixar dinâmico:

src/App.tsx:


import React from "react";
import { NavigationContainer } from "@react-navigation/native";

import AuthContext from "./contexts/auth";

import Routes from "./routes";

const App: React.FC = () => {
    
  return (
    <NavigationContainer>
      <AuthContext.Provider value={{ signed: true }}>
        <Routes />
      </AuthContext.Provider>
    </NavigationContainer>
  );
};

export default App;

Vamos deixar o AuthContext abaixo do NavigationContainer pois ele não precisa da informação que AuthContext provê, agora todos os filhos podem necessitar por exemplo do nome ou email do usuário logado e terão o acesso, não via props, mas de outra maneira mais escalável e elegante que o React nos proporciona. A informação agora fica em um contexto, esse contexto da aplicação tem várias informações que podem ser utilizadas simplesmente invocando uma função que fica global dentro do contexto, abaixo do Provider, e os valores que tem acesso é o que é informado no value do Provider. Todos os componentes abaixo do AuthContext.Provider agora podem acessar o signed: true.

Agora o estado não fica mais sendo controlado no estado local. Leia novamente o código acima para entender bem. 🧐

Quando faz o logon no SignIn, poderia armazenar o estado em um useState e deixar salvo no AsyncStorage (React Native) ou LocalStorage (Web), mas essa não é a função dos Storages, e ai que entra a Context API, os estados ficam em um contexto onde os componentes filhos podem acessar de forma escalável, e claro que poderíamos criar outros contextos também sem problema nenhum.

Agora vamos acessar o context o no SignIn.tsx. Veja como fica fácil e sem o problema de PropsHell ou Prop Drilling.

src/pages/SignIn/index.tsx:


import React, { useContext } from "react";
import { View, Button, StyleSheet } from "react-native";
import { signIn } from "../../services/auth";
import AuthContext from "../../contexts/auth";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
});

const SignIn: React.FC = () => {
  const { signed } = useContext(AuthContext);

  console.log(signed);

  async function handleSign() {
   
    const response = await signIn();
    console.log(response);
  }

  return (
    <View style={styles.container}>
      <Button title="Sign In" onPress={handleSign} />
    </View>
  );
};

export default SignIn;

Importamos o AuthContext do nosso contexts/auth.tsx, e utilizamos o useContext() que é o hook do React para nos devolver um valor que o Provider nos fornece, que no caso é o signed por enquanto, passando o AuthContext dentro do useContext.

E agora quando a tela SignIn, é acessada  será impresso no console: true.

Se lá no App.tsx eu mudar:

<AuthContext.Provider value={{ signed: false }}>

Será impresso: false no console quando acessar a página de SignIn.

Eu achei isso fantástico e rápido de implementar essa parte com a Context API, vamos prosseguir no exemplo, deixando dinâmico.

Agora quando o usuário clicar no botão SignIn a  função handleSign deverá ser disparada e executar a  função  signIn que altera o contexto.

Dentro do arquivo contexts/auth.tsx,  iremos implementar um componente AuthProvider, que será exportado.

src/contexts/auth.tsx


import React, { createContext } from "react";

const AuthContext = createContext({ signed: true });

export const AuthProvider: React.FC = ({ children }) => (
  <AuthContext.Provider value={{ signed: false }}>
    {children}
  </AuthContext.Provider>
);

export default AuthContext;

AuthProvider é um componente funcional que contém o conteúdo que estava no App.tsx, e passamos o children, ou seja, tudo que fica dentro desse provider será repassado no children (filhos).

E agora refatoramos o App.tsx.


import React from "react";
import { NavigationContainer } from "@react-navigation/native";

import { AuthProvider } from "./contexts/auth";

import Routes from "./routes";

const App: React.FC = () => {
  return (
    <NavigationContainer>
      <AuthProvider>
        <Routes />
      </AuthProvider>
    </NavigationContainer>
  );
};

export default App;

Ao invés de importar o AuthContext e usar o Provider dentro dele, apenas importamos o componente AuthProvider e envolvemos as rotas (Routes) nele, Routes será o children que está no componente AuthProvider no arquivo contexts/auth.tsx.

Veja a semelhança com NavigationContainer, esse componente utiliza a mesma  estratégia de implementação que o AuthProvider utilizando Context API do React.

Agora o nosso store (estados de contexto) vai ficar no arquivo de contexto e não mais lá no App.tsx onde passamos o value para o Provider, e agora podemos deixar mais dinâmico e menos desacoplado as responsabilidades, tornando o App.tsx coeso.

Vamos melhorar ainda mais nosos arquivo de contexto (auth.tsx), e vamos ver mais um pouco o poder do Typescript.

Typescript, o Javascript que escala

Nosso AuthContext vai ter os estados: logado ou deslogado com a atributo: signed recebendo true ou false, e vai armazenar um token (se a nossa estratégia for JWT por exemplo) com o atributo token,  e os dados do usuário com atributo user.

Então vamos criar uma interface do typescript, com os atributos que mencionei acima:

interface AuthContextData {
  signed: boolean;
  token: string;
  user: object;
}

signed é um boolean, token recebe uma string e user é do tipo object, pode ser um objeto qualquer independente dos seus atributos (não sei quais dados estão vindo do backend, e também dessa forma o frontend não fica tão acoplado ao backend, faz muito sentido isso).

Agora quando utilizo o createContext devo informar essa interface e passar um objeto como parâmetro forçando que o objeto seja do tipo AuthContextData.

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

Se eu passase apenas um createContext<AuthContextData>({}); o vscode iria me mostrar um erro de typescript:

Argument of type '{}' is not assignable to parameter of type 'AuthContextData'.

O argumento do tipo objeto vazio não é atribuível ao tipo de parâmetro AuthContextData. Então, preciso fazer o hack: {} as AuthContextData falando que {} é representado como um AuthContextData.

Outra maneira de contornar o problema seria assim:

const AuthContext = createContext<AuthContextData | null>(null);

AuthContext recebe um AuthContextData ou null e início a constante com null como parâmetro da função. Mas eu não acho tão elegante. Do outro jeito é o mais recomendável fazer e comumente utilizado pela comunidade.

Agora o value acusou um problema, e isso é ótimo, porque agora o typescript com vscode nos ajuda a corrigir o código em tempo de desenvolvimento e não mais em tempo de execução do projeto, porque agora nosso código está sendo compilado e o typescript já nos acusa os futuros possíveis problemas que iremos ter, e o vscode mostra isso pra gente, então já podemos corrigir antes de colocar o código em produção. 🙌

O que o value está reclamando é que estou apenas enviando a informação value={{signed: false}} mas AuthContext fornece signed, token e user como obrigatórios.

Type '{ signed: boolean; }' is missing the following properties from type 'AuthContextData': token, userts(2739)

Como ficou o código até o momento para entender todo o contexto explicado acima:

src/contexts/auth.tsx:

import React, { createContext } from "react";

interface AuthContextData {
  signed: boolean;
  token: string;
  user: object;
}

const AuthContext = createContext<AuthContextData>({} as  AuthContextData);

export const AuthProvider: React.FC = ({ children }) => (
  <AuthContext.Provider value={{ signed: false }}>
    {children}
  </AuthContext.Provider>
);

export default AuthContext;

Então vamos passar o restante dos atributos de AuthContextData para o value.

<AuthContext.Provider  value={{signed:  false, token:  '', user: {}}}>

Agora o vscode para de reclamar,  e agora esses dados já estão disponiveis em todo o contexto, claro que nesse momento é inútil isso, mas já podemos ter acesso, exemplo:

src/pages/SignIn/index.tsx:

const { signed, user, token } = useContext(AuthContext);

Context API, seu job é ser responsável pelo contexto de autenticação 🚔

Agora vamos deixar mais legal ainda, toda a parte de autenticação vai ficar sobre responsabilidade do contexto de autenticação, antes tínhamos apenas acesso ao estado, agora iremos poder manipular o estado do contexto e implementar a autenticação. Vamos por partes:

Primeiro eu importo agora o serviço de autenticação para dentro do contexto auth.tsx, utilizo o * as auth para colocar todos os métodos dentro da variável auth, para eu poder criar uma função com nome signIn, isso não é obrigatório, mas dá mais semântica no código.

E crio uma função com nome de signIn que irá fazer a autenticação. Nesse momento apenas imprimirá no console os dados do usuário:

 async function signIn() {
    const response = await auth.signIn();
    console.log(response);
  }

Mudei a interface também para que ela forneça apenas o que é necessário, não precisa informar o token para os componentes, e incluí o método signIn(): Promise<void> que não recebe nenhum parâmetro e o retorno é uma promise que não tem retorno (void), forneço essa função no value do Provider, e agora essa função fica disponível para quem precisar ter acesso, e adivinha quem vai precisar dela? O componente SignIn.

Veja o código completo:

import React, { createContext } from "react";
import * as auth from "../services/auth";

interface AuthContextData {
  signed: boolean;
  user: object;
  signIn(): Promise<void>;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({ children }) => {
  async function signIn() {
    const response = await auth.signIn();
    console.log(response);
  }

  return (
    <AuthContext.Provider value={{ signed: false, user: {}, signIn }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

Agora vamos implementar o src/pages/SignIn/index.tsx para fazer uso dessa função.

Temos acesso a função signIn fazendo apenas isso:

const {signed, signIn} =  useContext(AuthContext);

E agora na função handleSign(), apenas invovamos o signIn() dentro dela:

function  handleSign() {
  signIn();
}

E o restante não mudou nada, veja como o código ficou lindo 💜, sem Props Drilling, sem acoplamento, com as responsabilidades separadas e bem nomeadas, lá no contexto posso mudar a implementação do Logon que não vai impactar no componente SignIn.

Veja o código completo 😍

import React, { useContext } from "react";
import { View, Button, StyleSheet } from "react-native";
import AuthContext from "../../contexts/auth";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
});

const SignIn: React.FC = () => {
  const { signed, signIn } = useContext(AuthContext);

  console.log(signed);

  function handleSign() {
    signIn();
  }

  return (
    <View style={styles.container}>
      <Button title="Sign In" onPress={handleSign} />
    </View>
  );
};

export default SignIn;

Agora quando eu clico no botão Sign In tudo funciona da mesma forma, e vai imprimir pra mim o resultado da função signIn que está implementado no contexts/auth.tsx.

Faz muito sentido por que agora os dados do usuário logado, estarão disponíveis para outros componentes. tipo um Header que contém o nome e o avatar do usuário logado por exemplo. Então é interessante realmente que o Login da aplicação esteja no contexto separado.

Agora, preciso pegar os dados do response que estava apenas imprimindo na tela e passar para dentro do contexto.

Antes vou fazer uma alteração no services de autenticação para falar qual o tipo de dados será retornado no response, criando uma interface do typescript, e informando o tipo de retorno da função signIn como Promise<void>.

interface Response {
  token: string;
  user: {
    name: string,
    email: string,
  };
}

export function signIn(): Promise<Response> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        token: "jk12h3j21h3jk212h3jk12h3jkh12j3kh12k123hh21g3f12f3",
        user: {
          name: "Thiago",
          email: "thiagomarinho@rockeseat.com.br",
        },
      });
    }, 2000);
  });
}

Agora lá no contexts/auth.tsx posso ter acesso ao tipo de retorno do response e com isso até mesmo utilizar o autocomplete que a intellisense do vscode fornece.

const {token, user} = response;

OBS: nesse momento o vscode vai continuar reclamando, mas agora é porque criei essas duas varíaveis acima, mas não estou utilizando ainda.

Pronto, agora que tenho o token e o usuário, preciso armazenar esses dados no contexto, e vamos utilizar o useState do React Hooks para isso.

Aliás, o dado mais relevante é o usuário (user), porque se tenho um usuário quer dizer que está autenticado, certo? se user !== null então na hora de fazer login deu certo, usuário veio preenchido.

Portanto, vou alterar aqui um pouco src/contexts/auth.tsx:

import React, { createContext, useState } from "react";
import * as auth from "../services/auth";

interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({ children }) => {
  const [user, setUser] = useState<object | null>(null);

  async function signIn() {
    const response = await auth.signIn();
    setUser(response.user);
  }

  return (
    <AuthContext.Provider value={{ signed: !!user, user, signIn }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

Por padão o user será inicializado com null, no primeiro momento vai ser nulo porque foi o valor que passei como parâmetro para o useState(null), mas declarei que ele pode ser um object ou null, então ele pode assumir esses dois valores, então quando logar e der tudo certo ele recebe um objeto com os dados do usuário (user).

Isso tudo para satisfazer as condições que o typescript estabeleceu, experimenta deixar:

const [user, setUser] =  useState({});

Você vai deparar com alguns erros de typescript, e você vai ver a segurança que o TS te dá em tempo de desenvolvimento, faz todo sentido, estou forçando o meu código trabalhar de forma segura eliminando um pouco da dinâmica dos tipos do javascript para o bem da aplicação em produção, esse super set (super conjunto) do typescript adicionado ao javascript torna a linguagem ainda mais poderosa.

Perceba que passo o user que está dentro de response.user para o useState user com o método setUser que é responsável por mudar um estado.

No value passo agora o user, como shorthand (maneira abreviada) uma vez que a chave é a mesma que o valor.

E o signed  vai ser true se !!user  for Truthy ou Falsy se user estiver nulo(null) ou indefinido(undefined),  isto é, se o estado do user não for preenchido com os dados do usuário logado, o que vai ocorrer se os dados não forem válidos.

Poderia ser assim também: value={{signed: Boolean(user), user: {}, signIn}}

E agora já tenho acesso ao user e signed de forma dinâmica de acordo com o usuário autenticado, claro que nesse momento o usuário está sempre chumbado ali no serviço de autenticação, mas já deu pra entender o recado.

Agora podemos testar novamente a aplicação, e quando inicia o valor de signed será false, porque não clicamos ainda na função signIn para fazer o login certo, e quando clicar no botão Sign In vai levar dois segundos para imprimir no console: true.

Esse console.log(signed) está lá no componente Sign In, isso quer dizer que toda vez que o contexto altera, todos os filhos que utilizam a dependência de useContext para o contexto alterado irá alterar também, e com isso o componente será reenderizado na tela, isto é, o componente SignIn irá ser atualizado, e o console.log(signed) impresso na tela. Podemos passar o user também:

src/pages/SignIn/index.tsx:

// omitido ...
 const {signed, signIn, user} = useContext(AuthContext);
 console.log(signed);
 console.log(user);
// omitido...

Que vai gerar o seguinte resultado:

[Wed Jun 10 2020 11:06:14.770] **LOG** true
[Wed Jun 10 2020 11:06:14.772] **LOG** {"email": "thiagomarinho@rocketseat.com.br", "name": "Thiago"}

E temos um problema, depois que fizermos um refresh na tela precisamos manter o usuário logado e fazer redirecionamento para rota Dashboard uma vez que está logado, são os próximos passos.

Então, agora como temos o signed true/false vamos no arquivo routes/index.tsx e de acordo com esse estado do contexto da autenticação vamos saber qual tela renderizar, isso é muito legal, porque iremos utilizar o AuthContext em mais um arquivo da nossa aplicação, além do SignIn agora no routes, veja:

Uso o hook useContext, e dentro do componente pego o signed de dentro do AuthContext com o custom hook useContext.

Retorno a rota fazendo um if ternário, se estiver logado retorno o componente AppRoutes para ver o dashboard senão o AuthRoutes para fazer login.

import React, { useContext } from "react";

import AuthContext from "../contexts/auth";

import AuthRoutes from "../routes/auth.routes";
import AppRoutes from "../routes/app.routes";

const Routes: React.FC = () => {
  const { signed } = useContext(AuthContext);
  return signed ? <AppRoutes /> : <AuthRoutes />;
};

export default Routes;

E como eu já estava logado, porque cliquei no botão SignIn o fast refresh do React Native já atualizou a tela pra mim de acordo com o estado atual do contexto e me redirecionou para a rota da Dashboard, com fast refresh atualiza o código mas não faz reload de toda a aplicação, por isso manteve o estado. 😎

Se eu der F5 atualiza tudo, ai volta para tela de login.

E agora quando clico em SignIn depois de dois segundos vai para a tela de Dashboard.

Como eu disse acima, o contexto do AuthContext foi alterado, e como o arquivo routes/index.tsx tem uma dependência com o useContext(AuthContext) então esse componente será renderizado na tela, e como o signed foi alterado então a rota será alterada também, e agora sim ficou dinâmico essa parte de autenticação.

Agora vamos implementar dentro do Dashboard o signOut para ver o efeito contrário, quando o usuário estiver logado e clicar em um botão que faz logout que vai alterar o estado de signed para false, possa voltar para a tela de SignIn para se autenticar novamente.

E é bem fácil, então primeira coisa que faço é criar o método de signOut no contexto de autenticação, que irá setar o valor null no usuário: setUser(null), e vou disponibilizar essa nova função no value do Provider, mas passando também no AuthContextData, signOut não precisa ser assíncrona por que não retorna nada e o estado é local no contexto. Veja:

src/contexts/auth.tsx:

import React, { createContext, useState } from "react";
import * as auth from "../services/auth";

interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
  signOut(): void;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({ children }) => {
  const [user, setUser] = useState<object | null>(null);

  async function signIn() {
    const response = await auth.signIn();
    setUser(response.user);
  }

  function signOut() {
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ signed: !!user, user, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

E no arquivo src/pages/Dashboard/index.tsx, utilizo essa função:

Deixei o arquivo bem parecido com o SignIn, só mudei os nomes e utilizo apenas o signOut que o contexto AuthContext fornece.

import React, { useContext } from "react";
import { View, Button, StyleSheet } from "react-native";
import AuthContext from "../../contexts/auth";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
});

const Dashboard: React.FC = () => {
  const { signOut } = useContext(AuthContext);

  function handleSignOut() {
    signOut();
  }

  return (
    <View style={styles.container}>
      <Button title="Sign Out" onPress={handleSignOut} />
    </View>
  );
};

export default Dashboard;

Muito legal, fica bem dinâmico, existe o acoplamento com o componente, mas é bem leve e muito fácil de lidar se tivéssemos utilizando apenas props. Mas lá no final do artigo esse código vai melhorar mais ainda com o custom hook que iremos criar. 🎁

Pronto, agora a parte de Sign In/Sign Out e redirecionamento de acordo com estado do signed já está funcionando, agora falta manter o estado após um F5 (refresh na tela).

Legal demais essa integração do React com React Native, você pode reutilizar o AuthContext dentro do frontend React sem quebrar nada, então a lógica de autenticação é toda reaproveitada, única coisa que muda é escrita dos componentes da tela do Sign In e Dashboard e também as rotas porque são APIs diferentes, mas dá pra seguir a mesma lógica! Show d+ Frontend e Mobile com essa tecnologia agiliza muito a nossa vida, a base de conhecimento é totalmente compartilhada entre esses dois mundos!

Armazenando no Storage

Vamos lidar com a parte de Storage (armazenamento), agora vai mudar um pouco a implementação da web com mobile, pois na web usamos LocalStorage e no mobile com RN utilizamos AsyncStorage.

Então vamos instalar esse pacote no react native.

yarn add @react-native-community/async-storage

Se estiver usando o iOS precisa fazer um pod install na pasta ios.

cd ios && pod install

E tem que reiniciar o simulador:

cd.. && npx react-native run-ios

Pronto, depois de inicializado podemos voltar a codar!

Processo é bem fácil, semelhante ao da web se você já usou LocalStorage. Só importar o AsyncStorage e após o login eu armazeno no storage, confira:

src/contexts/auth.tsx:

import AsyncStorage from  '@react-native-community/async-storage';

// codigo acima omitido
  async function signIn() {
    const response = await auth.signIn();
    setUser(response.user);

    await AsyncStorage.setItem('@RNAuth:user', JSON.stringify(response.user));
    await AsyncStorage.setItem('@RNAuth:token', response.token);
  }
// codigo abaixo omitido 

Os métodos do AsyncStorage são assíncronos então preciso utilizar o async/await, e uso o método setItem que recebe dois parâmetros, primeiro a chave e o segundo o valor, é imprescindível que o nome da chave seja único, por isso uso o @RNAuth que é o nome do aplicativo, e o Storage armazena strings, por isso utilizei o JSON.stringify para converter o objeto user em string, e o token já é string então não precisei fazer isso.

Pronto, agora já estamos salvando o usuário logado, agora, falta obter o usuário logado quando faz um refresh no aplicativo.

Utilizando o useEffect após que a tela é montada

Agora vou usar mais um hook do React, o useEffect que vai ser disparado assim que o AuthProvider for construído em tela.

Dentro do useEffect crio uma função chamada loadStorageData, que faz um getItem naquelas chaves que fiz o setItem, ela irá trazer o usuário e o token, depois verifico se realmente tem um valor dentro de storagedUser e storagedToken, se tiver coloco o user no estado fazendo um setUser(JSON.parse(storagedUser), como o objeto veio em formato de string, devo converter em objeto com JSON.parse. E no final do useEffect invoco a função que criei. No useEffect não é possível usar async/await em sua função, por isso precisei criar uma função e invocar logo em seguida, isso é um padrão da implementação do React. Veja como ficou o useEffect:

src/contexts/auth.tsx:

// código acima omitido
  useEffect(() => {
    async function loadStorageData() {
      const storagedUser = await AsyncStorage.getItem('@RNAuth:user');
      const storagedToken = await AsyncStorage.getItem('@RNAuth:token');

      if (storagedUser && storagedToken) {
        setUser(JSON.parse(storagedUser));
      }
    }

    loadStorageData();
  });
// codigo abaixo omitido

Por enquanto não faço nada com o token.

Pronto, até agora, fazemos o login e salvamos o user no AsyncStorage, e depois quando o AuthProvider é construído (montado) em tela buscamos o user do AsyncStorage e definimos no estado do user.

Pronto, o login continua funcionando, mas agora quando atualizo a tela fazendo um reload, ela nos mantém na rota Dashboard porque o login continua ativo.

Agora no signOut precisamos remover o usuário e token do AsyncStorage para que o logout realmente seja efetivado.

Isso é bem fácil, basta eu chamar a função clear do AsyncStorage:

src/contexts/auth.tsx:

// código acima omitido
async function signOut() {
  await AsyncStorage.clear();
  setUser(null);
}
// código abaixo omitido

Agora o signOut vai estar funcionando também, e quando atualiza a tela volta para a tela de SignIn.

Veja o código completo até aqui:

src/contexts/auth.tsx:

import React, { createContext, useState, useEffect } from "react";
import * as auth from "../services/auth";

import AsyncStorage from "@react-native-community/async-storage";

interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
  signOut(): void;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({ children }) => {
  const [user, setUser] = useState<object | null>(null);

  useEffect(() => {
    async function loadStorageData() {
      const storagedUser = await AsyncStorage.getItem("@RNAuth:user");
      const storagedToken = await AsyncStorage.getItem("@RNAuth:token");

      if (storagedUser && storagedToken) {
        setUser(JSON.parse(storagedUser));
      }
    }

    loadStorageData();
  });

  async function signIn() {
    const response = await auth.signIn();
    setUser(response.user);

    await AsyncStorage.setItem("@RNAuth:user", JSON.stringify(response.user));
    await AsyncStorage.setItem("@RNAuth:token", response.token);
  }

  async function signOut() {
    await AsyncStorage.clear();
    setUser(null);
  }

  return (
    <AuthContext.Provider value={{ signed: !!user, user, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

Melhorando a usabilidade com um Loading

Mas se você reparar na tela, vai ver que quando faço reload, ele mostra a tela Sign In pisca e vai para tela de Dashboard, isso acontence porque o processo é assíncrono, então, para melhorar isso, podemos adicionar um loading ⌛.

Crio então um novo estado, usando useState chamado loading:

const [loading, setLoading] = useState(true);

E depois de carregar, defino o valor false, sempre que ele iniciar começo com valor true, carregou altero o estado  de loading para false.

Agora só utilizar o estado de loading, vamos usar no próprio contexto, poderia ser em outro lugar ali antes das rotas também, mas acho que no AuthProvider fica bem interessante, poderia criar uma pasta componentes e importar o componente Loading também, mas pra facilitar aqui vou deixar no mesmo código.

src/contexts/auth.tsx:

import React, { createContext, useState, useEffect } from "react";
import * as auth from "../services/auth";
import { View, ActivityIndicator } from "react-native";
import AsyncStorage from "@react-native-community/async-storage";

interface AuthContextData {
  signed: boolean;
  user: object | null;
  signIn(): Promise<void>;
  signOut(): void;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({ children }) => {
  const [user, setUser] = useState(object | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadStorageData() {
      const storagedUser = await AsyncStorage.getItem("@RNAuth:user");
      const storagedToken = await AsyncStorage.getItem("@RNAuth:token");

      if (storagedUser && storagedToken) {
        setUser(JSON.parse(storagedUser));
      }
	 setLoading(false);
    }

    loadStorageData();
  });

  async function signIn() {
    const response = await auth.signIn();
    setUser(response.user);

    await AsyncStorage.setItem("@RNAuth:user", JSON.stringify(response.user));
    await AsyncStorage.setItem("@RNAuth:token", response.token);
  }

  async function signOut() {
    await AsyncStorage.clear();
    setUser(null);
  }

  if (loading) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <ActivityIndicator size="large" color="#666" />
      </View>
    );
  }

  return (
    <AuthContext.Provider value={{ signed: !!user, user, signIn, signOut }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

Pronto, agora na hora de entrar na aplicação e fizer reload já vai dar pra ver um loading. 🥂

Se você não conseguir ver o loading basta colocar esse código abaixo para simular uma lentidão com uma promise que será resolvida depois de dois segundos:

useEffect(() => {
  async function loadStorageData() {
    const storagedUser = await AsyncStorage.getItem("@RNAuth:user");
    const storagedToken = await AsyncStorage.getItem("@RNAuth:token");
	// simular uma lentidão para mostar o loading.
    await new Promise((resolve) => setTimeout(resolve, 2000));

    if (storagedUser && storagedToken) {
      setUser(JSON.parse(storagedUser));
    }
    setLoading(false);
  }

  loadStorageData();
});

Refatorando o Loading da aplicacão

Vamos refatorar um pouco mais o código, vamos passar o loading para o value do Provider, e vamos separar a verificação e exibição do loading em outro lugar.

O melhor lugar esse loading ficar é no arquivo src/routes/index.tsx, aqui vou manter o mesmo código de Loading, mas poderia utilizar react-native-splash-screen e deixar a tela de splash screen exibindo até terminar a verificação de login e senha, ou poderia usar um  lottie-react-native para ficar bem mais bonito o app.

Para não extender mais esse tutorial, vamos apenas refatorar a rota:

src/routes/index.tsx

import React, { useContext } from "react";
import { View, ActivityIndicator } from "react-native";

import AuthContext from "../contexts/auth";

import AuthRoutes from "../routes/auth.routes";
import AppRoutes from "../routes/app.routes";

const Routes: React.FC = () => {
  const { signed, loading } = useContext(AuthContext);

  if (loading) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <ActivityIndicator size="large" color="#666" />
      </View>
    );
  }

  return signed ? <AppRoutes /> : <AuthRoutes />;
};

export default Routes;

E agora o nosso arquivo src/context/auth.tsx disponibilizará o loading, até aqui você já deve ter pego o jeito, coloco o loading no AuthContextData e disponibilizo o loading no value do AuthProvider. Check it out 👇

import React, { createContext, useState, useEffect } from "react";
import * as auth from "../services/auth";
import AsyncStorage from "@react-native-community/async-storage";

interface AuthContextData {
  signed: boolean;
  user: object | null;
  loading: boolean;
  signIn(): Promise<void>;
  signOut(): void;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({ children }) => {
  const [user, setUser] = useState<object | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadStorageData() {
      const storagedUser = await AsyncStorage.getItem("@RNAuth:user");
      const storagedToken = await AsyncStorage.getItem("@RNAuth:token");

      if (storagedUser && storagedToken) {
        setUser(JSON.parse(storagedUser));
      }
	  setLoading(false);
    }

    loadStorageData();
  });

  async function signIn() {
    const response = await auth.signIn();
    setUser(response.user);

    await AsyncStorage.setItem("@RNAuth:user", JSON.stringify(response.user));
    await AsyncStorage.setItem("@RNAuth:token", response.token);
  }

  async function signOut() {
    await AsyncStorage.clear();
    setUser(null);
  }

  return (
    <AuthContext.Provider
      value={{ signed: !!user, user, loading, signIn, signOut }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

Agora pra colocar a cerejinha do bolo 🍰, vamos finalizar com a parte do token.

Como disponibilizar o token para cada requisição da aplicação?

Geralmente usamos o axios para fazer requisições http (get, post, delete, put, patch) e para cada requisição precisamos fornecer o token para rotas autenticadas no backend.

Vamos instalar o axios:

yarn add axios

Na pasta services vou criar um arquivo api.ts com a seguinte configuração:

import axios from 'axios';

const api = axios.create({
  baseURL: 'http://localhost:3333',
});

export default api;

Claro, nesse momento isso é apenas fake, no lugar ali do http... devemos colocar a url do servidor backend. 🔙🔚

E agora eu utilizo a api no arquivo src/contexts/auth.tsx:

import React, { createContext, useState, useEffect } from "react";
import AsyncStorage from "@react-native-community/async-storage";
import * as auth from "../services/auth";
import api from "../services/api";

interface AuthContextData {
  signed: boolean;
  user: object | null;
  loading: boolean;
  signIn(): Promise<void>;
  signOut(): void;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export const AuthProvider: React.FC = ({ children }) => {
  const [user, setUser] = useState<object | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadStorageData() {
      const storagedUser = await AsyncStorage.getItem("@RNAuth:user");
      const storagedToken = await AsyncStorage.getItem("@RNAuth:token");

      if (storagedUser && storagedToken) {
        setUser(JSON.parse(storagedUser));
        setLoading(false);
        api.defaults.headers.Authorization = `Baerer ${storagedToken}`;
      }
    }

    loadStorageData();
  });

  async function signIn() {
    const response = await auth.signIn();
    setUser(response.user);

    api.defaults.headers.Authorization = `Baerer ${response.token}`;

    await AsyncStorage.setItem("@RNAuth:user", JSON.stringify(response.user));
    await AsyncStorage.setItem("@RNAuth:token", response.token);
  }

  async function signOut() {
    await AsyncStorage.clear();
    setUser(null);
  }

  return (
    <AuthContext.Provider
      value={{ signed: !!user, user, loading, signIn, signOut }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export default AuthContext;

Pronto, agora cada vez que eu fizer autenticação, o authorization será definido no header da requisição e lá no backend esse token será validado para continuar a requisição.

Repare que uso o trecho de código: api.defaults.headers.Authorization... ; em dois lugares, tanto no login quando no reload da página, assim sempre o token ficará disponível nas requisições.

Agora sim, finalizando com chave de ouro 🔑

Vamos criar um custom hook 🎣 de autenticação 🔒!

Vai ficar top! aguenta ai! Hold on!!!

Vamos mostrar no Dashboard o nome do usuário logado:

// ...
const {user, signOut} =  useContext(AuthContext);
// ...
<Text>{user?.name}</Text>
// ...

O usuário pode ser nulo, então, uso ?.name que é um operador ternário de segurança que o typescript nos fornece, ele só acessa a propriedade name se o usuário não for nulo. 😌

Mas para usar dessa forma o vscode requer que eu crie um interface  User para acessar a propriedade name.

src/contexts/auth.tsx:

//...
interface User {
  name: string;
  email: string;
}

interface AuthContextData {
  signed: boolean;
  user: User | null;
  loading: boolean;
  signIn(): Promise<void>;
  signOut(): void;
}
//...

Em todo lugar que precisar do user e antes estava object eu troco para User.

const [user, setUser] =  useState<User | null>(null);

Pronto agora já tenho o nome e está sendo exibindo em tela.

Podemos diminuir um pouco nosso código, desacoplar mais ainda dos componente criando o nosso custom hook.

Toda vez que precisamos de uma informação do contexto de autenticação, precisamos importar o useContext do React, importar o AuthContext e passar como parâmetro.

Vamos abstrair mais o código para que isso não seja necessário e vamos remover esses dois imports e melhorar verbosidade.

Lá no contexs/auth.tsx iremos exportar uma função chamada useAuth que iremos criar, ela por si só vai utilizar o useContext do React e receber como parâmetro o AuthContext.

src/contexts/auth.tsx

export function useAuth() {
  const context = useContext(AuthContext);
  return context;
}

A constante context agora tem todos os atributos de AuthContextData, e retorna esses valores para quem invocar essa função useAuth.

Agora substituímos todos os lugares que usa o contexto de autenticação AuthContext para usar a função useAuth().

Últimas listagens de código, o nosso auth.tsx vai ficar assim:

import React, {createContext, useState, useEffect, useContext} from 'react';
import AsyncStorage from '@react-native-community/async-storage';
import * as auth from '../services/auth';
import api from '../services/api';

interface User {
  name: string;
  email: string;
}

interface AuthContextData {
  signed: boolean;
  user: User | null;
  loading: boolean;
  signIn(): Promise<void>;
  signOut(): void;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

const AuthProvider: React.FC = ({children}) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadStorageData() {
      const storagedUser = await AsyncStorage.getItem('@RNAuth:user');
      const storagedToken = await AsyncStorage.getItem('@RNAuth:token');

      if (storagedUser && storagedToken) {
        setUser(JSON.parse(storagedUser));
        api.defaults.headers.Authorization = `Baerer ${storagedToken}`;
      }

      setLoading(false);
    }

    loadStorageData();
  });

  async function signIn() {
    const response = await auth.signIn();
    setUser(response.user);

    api.defaults.headers.Authorization = `Baerer ${response.token}`;

    await AsyncStorage.setItem('@RNAuth:user', JSON.stringify(response.user));
    await AsyncStorage.setItem('@RNAuth:token', response.token);
  }

  async function signOut() {
    await AsyncStorage.clear();
    setUser(null);
  }

  return (
    <AuthContext.Provider
      value={{signed: !!user, user, loading, signIn, signOut}}>
      {children}
    </AuthContext.Provider>
  );
};

function useAuth() {
  const context = useContext(AuthContext);

  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider.');
  }

  return context;
}

export {AuthProvider, useAuth};

No arquivo acima dei uma leve refatorada, primeiro coloquei a exportação no final, onde exporto o AuthProvider que o App.tsx utiliza e a função useAuth() que fornece o contexto.

Coloquei uma verificação que se o contexto estiver nulo ou indefinido é porque esse hook está sendo chamado fora do Provider, pois o Provider sempre nos entrega um value com alguma coisa, se isso não estiver acontecendo é que a função não está dentro de um Provider.

Nosso Dashboard no final vai ficar assim, já utilizando o hook useAuth:

import React from "react";
import { View, Text, Button, StyleSheet } from "react-native";
import { useAuth } from "../../contexts/auth";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
});

const Dashboard: React.FC = () => {
  const { user, signOut } = useAuth();

  function handleSignOut() {
    signOut();
  }

  return (
    <View style={styles.container}>
      <Text>{user?.name}</Text>
      <Button title="Sign Out" onPress={handleSignOut} />
    </View>
  );
};

export default Dashboard;

E agora o Sign In fica assim:

import React from "react";
import { View, Button, StyleSheet } from "react-native";
import { useAuth } from "../../contexts/auth";

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
  },
});

const SignIn: React.FC = () => {
  const { signIn } = useAuth();

  function handleSign() {
    signIn();
  }

  return (
    <View style={styles.container}>
      <Button title="Sign In" onPress={handleSign} />
    </View>
  );
};

export default SignIn;

src/routes/tsx:

import React from "react";
import { View, ActivityIndicator } from "react-native";

import { useAuth } from "../contexts/auth";

import AuthRoutes from "../routes/auth.routes";
import AppRoutes from "../routes/app.routes";

const Routes: React.FC = () => {
  const { signed, loading } = useAuth();

  if (loading) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <ActivityIndicator size="large" color="#666" />
      </View>
    );
  }

  return signed ? <AppRoutes /> : <AuthRoutes />;
};

export default Routes;

Que legal, 🥳 os códigos dos componentes ficaram mais limpos, apenas renderizando dados e recebendo as referências de funções que executam algo.

Uffa, pronto acabamos a aplicação, e criamos o nosso custom hook, e a aplicação está funcionando normalmente! Show de código né!

Lembrando que o mesmo fluxo pode ser migrado para web sem problemas nenhum e ai fica o desafio, criar um projeto web e utilizando a mesma estrutura e lógica desse projeto, só que agora na web, dica vai mudar ali a parte do Async Storage, mas fica mais fácil porque você não precisa instalar uma lib, o LocalStorage fica disponível de forma global para você e é síncrono.

Segue o código da aplicação: https://github.com/tgmarinho/authrn

O post ficou enorme, mas agregou muito conhecimento!

Se você chegou até aqui, alcançou mais um nível na sua jornada de dev.

Espero que tenham curtido, deixe um comentário para eu saber o que você achou, e se tiver alguma dúvida comente também, ou leve lá para o canal #ajuda da comunidade no Discord.

Se quiserem assistir a Master Class que o Diego Fernandes mostra o passo a passo dessa aplicação:

Até a próxima, porque o conhecimento é continuo, e sempre tem um próximo nível!