DEV Community

Bruno Bertolini
Bruno Bertolini

Posted on

Gerenciamento de autenticação no front-end

Existem várias formas de fazer o gerenciamento de autenticação no front. A forma que eu vou mostrar aqui é simples, porém robusta, e pode ser utilizada tanto no React Native quanto no React web.

Faremos isso com React Native, utilizando o Context API pra criar um estado global na nossa aplicação, e AsyncStorage pra persistência de dados.

A tela de login

Precisamos pensar como esse gerenciamento vai funcionar. Eu gosto de começar de cima pra baixo o desenvolvimento, da camada mais alta até a mais baixa, isso me dá uma clareza maior do que precisa ser feito.

Nesse caso, a camada mais alta é a nossa tela de login. Então o que eu preciso fazer quando o usuário apertar o botão Entrar? Basicamente, duas coisas:

  1. Pegar os dados do form e enviar pra uma api, que vai me retornar o usuário logado e um token para as próximas requisições
  2. Pegar os dados retornados da api, e jogar eles em um estado global, pra que seja acessado de qualquer parte da aplicação.

Então, teríamos um componente de login, semelhante a isto:

const Login = () => {
  const formik = useFormik({
    onSubmit = async values => {
      try {
        const { data } = await api.login(values)
        setStore(data)
      } catch (error) { }
    }
  })

  ...
}

Show, agora eu sei que eu preciso criar um setStore, que vai gerenciar meu estado global, que é a próxima etapa.

Global Store

Podemos fazer a global store de várias formas diferentes, seja utilizando redux, mobx, ou qualquer outra ferramenta de gerenciamento de estado. Aqui, vamos utilizar o Context API, que resolve muito bem o problema e serve tanto para aplicações pequenas quanto maiores.

Vamos criar um simples context provider que utilizará como valor, um useState, assim conseguiremos capturar o estado atual da nossa store em qualquer componente, bem como alterá-lo.

Crie um arquivo chamado store.js, faça o seguinte:

import * as React from 'react'
import { createContext, useState } from 'react'

// Aqui criamos o contexto, já com um valor semelhante 
// ao que precisaremos posteriormente
const StoreContext = createContext([{}, () => {}])

// E aqui encapsulamos o provider pra conseguir passar o retorno 
// do `useState` como valor
export const StoreProvider = ({ children }) => {
  // criando um state com um objeto vazio como valor inicial
  const [state, setState] = useState({})

  return (
    <StoreContext.Provider value={[state, setState]}>
      {children}
    </StoreContext.Provider>
  )
}


Nota: se quiser estudar mais sobre context api, dá uma olhada na doc oficial

Certo, criado nosso global store provider, precisamos usar ele no componente principal da aplicação (geralmente index.js, ou melhor ainda src/index.js), para que todos os componentes abaixo dele tenham acesso ao StoreContext e possam recuperar e manipular nossa store.

import { AppRegistry } from 'react-native'
import { name as appName } from './app.json'
import { App } from './src'

const Root = () => (
  <StoreProvider>
    <App />
  </StoreProvider>
)

AppRegistry.registerComponent(appName, () => Root)

Agora, qualquer componente pode acessar o contexto da store, mas, como fazer isso?

Bom, poderíamos fazer exportando nosso StoreContext e utilizando useContext assim:

import { StoreContext } from './store.js'

const Component = () => {
  const [store, setStore] = useContext(StoreContext)
  return '...'
}

Mas eu aconselho criarmos um hook especifico pra acessar a store, assim temos mais flexibilidade de criação e manutenção, podendo estender as funcionalidades do nosso novo hook facilmente. Então no store.js, criamos o hook:

export const useStore = () => {
  const [store, setStore] = useContext(StoreContext)
  return [store, setStore]
}

Agora que temos login jogando na nossa store os dados do usuário, ao iniciar a aplicação precisamos verificar se um usuário está logado, e redirecionar ele para a tela correspondente (home se estiver logado, login se não estiver). Vamos fazer isso no componente que define as rotas principais, porém nesse momento ele vai ser criado dentro do arquivo src/index.js e vamos chamá-lo de Router.

const Router = () => {
  const [store] = useStore()
  return store.token ? <Home /> : <Login />
}

Lembra que criamos nossa store com um objeto vazio como valor inicial? Pois bem, nesse momento, ao verificar se nossa store possui uma prop token, teremos como resultado false, portanto, nossa tela de login será mostrada. Posteriormente, quando o usuário fizer login e nosso componente de login fizer setStore com o token, automaticamente o nosso router vai ser re-renderizado, dessa vez contento store.token, mostrando assim a tela inicial (Home) no lugar de login.

Pronto, já temos nosso gerenciamento de autenticação, certo? Errado! Ainda falta uma etapa importante. Toda vez que o usuário fechar o app e abrir novamente, nós perdemos os dados em memória, portanto, mesmo que ele tenha feito login recentemente, ele será redirecionado para Login. Então, como podemos resolver isso?

Persistência e reidratação de memória

Quando um app é fechado, de um modo geral, ele apaga todas as variáveis da memória, portanto na próxima execução do app não saberemos quem estava logado. Por isso precisamos persistir essas informações em outro local (como um arquivo, banco de dados local ou remoto), e fazer a reidratação para que o app volta ao estado em que estava logo antes de ser fechado.

Para isso, vamos utilizar o async storage pra react native (pra web você pode utilizar o local storage, com a mesma abordagem).

Vamos iniciar importando o useAsyncStorage e persistindo toda alteração da nossa store nele. Dentro do <StoreProvider> vamos fazer o seguinte:

import { useAsyncStorage } from '@react-native-community/async-storage'

export const StoreProvider = ({ children }) => {
  const [state, setState] = useState({})

  // Aqui definimos que a "key" usada no async storage será "store"
  const { setItem } = useAsyncStorage('store')

  // então usamos o useEffect pra escutar as alterações do state,
  // e executar setItem, que vai persistir  nosso estado
  useEffect(() => {
   setItem(JSON.stringify(state))
  }, [state])

  return ...
}


Agora quando executarmos o setStore lá na tela de login, o StoreProvider vai persistir isso no async storage. Porém ainda precisamos reidratar a memoria quando o app for aberto, então pra isso, faremos outro useEffect:

export const StoreProvider = ({ children }) => {
  // ...
  const { setItem, getItem } = useAsyncStorage('store')

  const rehydrate = async () => {
    const data = await getItem()
    data && setState(JSON.parse(data))
  }

  useEffect(() => {
    rehydrate()
  }, [])

  return ...
}

Ou seja, toda vez que o app foi aberto, e o react fizer mount do nosso StoreProvider, a função de rehydrate será executada, pegando todos os dados do async storage e jogando na memoria do nosso state.

Acontece que não sabemos quanto tempo esse processo de rehydrate pode levar, ocasionando um lag na checagem do nosso router, que vai mostrar a tela de login antes de fazer o redirecionamento pra tela Home, pois inicialmente não temos o token na store. Então pra resolver esse problema, precisamos adicionar na nossa store uma prop informando que o processo de rehydrate ainda está em execução, para que um loading seja mostrado na tela antes de fazermos a verificação de usuário logado. Nesse caso nossa store final fica assim:

import * as React from 'react'
import { createContext, useContext, useState, useEffect } from 'react'
import { useAsyncStorage } from '@react-native-community/async-storage'

const StoreContext = createContext([{}, () => {}])

export const useStore = () => {
  const [state, setState] = useContext(StoreContext)
  return [state, setState]
}

export const StoreProvider = ({ children }) => {
  const { getItem, setItem } = useAsyncStorage('store')
  const [state, setState] = useState({
    rehydrated: false,
  })

  const rehydrate = async () => {
    const data = await getItem()
    setState(prev => ({
      ...prev,
      ...(data && JSON.parse(data)),
      rehydrated: true,
    }))
  }

  useEffect(() => {
    rehydrate()
  }, [])

  useEffect(() => {
    setItem(JSON.stringify(state))
  }, [state])

  return (
    <StoreContext.Provider value={[state, setState]}>
      {children}
    </StoreContext.Provider>
  )
}

Verifique que adicionamos um estado inicial com rehydrated: false, e no método de rehydrate, colocamos rehydrated: true pra informar que o processo de reidratação foi concluído.

Ainda temos que alterar nosso login, pra mesclar as informações na store, em vez de substituí-las.

const Login = () => {
  const formik = useFormik({
    onSubmit = async values => {
      try {
        const { data } = await api.login(values)
-        setStore(data)
+        setStore(prevState => ({...prevState, auth: data })
      } catch (error) { }
    }
  })

  ...
}

Perceba que agora nossa store passa a ter os dados de autenticação nomeados pra auth, então nosso componente Router precisa se adaptar a isso, além de verificar se o processo de rehydrate já terminou ou não:

const Router = () => {
  const [{ auth, rehydrated }] = useStore()

  if (!rehydrated) {
    return <Loading />
  }

  return auth && auth.token ? <Home /> : <Login />
}

E pronto, temos um gerenciamento de autenticação utilizando um estado global com context api e persistência!

Você pode ver o vídeo onde eu explico essa implementação mais detalhadamente, e pode acessar o repo com o projeto desenvolvido durante a gravação do vídeo.

Top comments (0)