Autenticação no React Native / ReactJS com Context API & Hooks
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.
Navegação é com React Navigation no RN.
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
.
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!