Capa do Ebook gratuito Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Roteamento e Autenticação em React: Construindo SPAs Seguras com React Router e JWT

Novo curso

20 páginas

Modelagem do estado de autenticação com Context, Providers e hooks

Capítulo 6

Tempo estimado de leitura: 11 minutos

+ Exercício
Audio Icon

Ouça em áudio

0:00 / 0:00

Modelar o estado de autenticação em uma SPA React vai além de “guardar um token”. Você precisa representar, de forma consistente e previsível, perguntas como: o usuário está autenticado? estamos carregando a sessão? qual usuário está logado? como lidar com expiração e logout? como evitar que múltiplos componentes reimplementem a mesma lógica? Para isso, uma abordagem comum e robusta é combinar React Context (para disponibilizar o estado global), um Provider (para encapsular regras e efeitos) e hooks (para consumo e operações como login/logout).

O que é “estado de autenticação” (e o que ele deve conter)

Um erro frequente é tratar autenticação como um booleano (isLoggedIn) ou como “token existe/não existe”. Na prática, uma modelagem útil precisa capturar estados intermediários e dados associados. Um modelo mínimo e didático costuma incluir:

  • status: estado atual do fluxo de autenticação. Exemplos: "checking" (validando sessão), "authenticated", "unauthenticated".
  • user: dados do usuário (id, nome, email, roles/permissions), ou null quando não autenticado.
  • accessToken: token de acesso (JWT) usado para chamadas autenticadas. Em muitos cenários, você não expõe o token diretamente para componentes, mas o Provider pode mantê-lo internamente.
  • error: mensagem/objeto de erro do último login/refresh, útil para UI.

Além disso, você precisa de ações (funções) que alteram esse estado: signIn, signOut, refreshSession, updateUser. O Context é um bom lugar para expor essas ações de forma padronizada.

Por que Context + Provider + hooks (em vez de prop drilling)

Sem Context, você tende a passar props de autenticação por muitos níveis (prop drilling), ou a duplicar lógica em páginas e componentes. O Context resolve a distribuição do estado, e o Provider centraliza:

  • Regras de transição de estado (ex.: ao iniciar o app, checar sessão).
  • Persistência (ex.: ler/gravar token em localStorage ou sessionStorage).
  • Integração com camada de API (ex.: configurar header Authorization).
  • Tratamento de expiração e logout automático.

Os hooks entram para oferecer uma API ergonômica: em vez de useContext(AuthContext) espalhado e sem validação, você cria useAuth() com checagens e tipagem.

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

Modelagem recomendada: estado finito (status) e ações explícitas

Pense na autenticação como uma pequena máquina de estados. Um fluxo típico:

  • App inicia: status = "checking".
  • Provider tenta restaurar sessão (ler token, validar/renovar, buscar /me).
  • Se ok: status = "authenticated", user preenchido.
  • Se falhar: status = "unauthenticated", user = null.

Isso evita “piscar” UI (mostrar área logada antes de confirmar) e reduz condições confusas do tipo if (token) ....

Passo a passo prático: criando AuthContext, AuthProvider e useAuth

1) Defina o formato do estado e do contexto

Comece definindo o que o Context vai expor. Mesmo em JavaScript, vale estruturar bem. Em TypeScript, isso fica ainda melhor, mas o exemplo abaixo funciona como referência conceitual.

import React from "react"; const AuthContext = React.createContext(null); export default AuthContext;

Em seguida, planeje o “contrato” do valor do contexto. Por exemplo:

  • status: "checking" | "authenticated" | "unauthenticated"
  • user: objeto ou null
  • signIn(credentials)
  • signOut()
  • refreshSession()

Mesmo sem tipagem formal, você deve manter esse contrato consistente para evitar componentes dependentes de detalhes internos.

2) Implemente um reducer para transições previsíveis

Para autenticação, useReducer costuma ser melhor do que useState, porque você terá múltiplas transições e quer evitar atualizações parciais inconsistentes. Exemplo de reducer:

const initialState = { status: "checking", user: null, accessToken: null, error: null }; function authReducer(state, action) { switch (action.type) { case "RESTORE_START": return { ...state, status: "checking", error: null }; case "RESTORE_SUCCESS": return { ...state, status: "authenticated", user: action.payload.user, accessToken: action.payload.accessToken, error: null }; case "RESTORE_FAILURE": return { ...state, status: "unauthenticated", user: null, accessToken: null, error: null }; case "SIGNIN_SUCCESS": return { ...state, status: "authenticated", user: action.payload.user, accessToken: action.payload.accessToken, error: null }; case "SIGNIN_FAILURE": return { ...state, status: "unauthenticated", user: null, accessToken: null, error: action.payload.error }; case "SIGNOUT": return { ...state, status: "unauthenticated", user: null, accessToken: null, error: null }; case "UPDATE_USER": return { ...state, user: action.payload.user }; default: return state; } }

Note como as ações são explícitas e cada transição define claramente o que acontece com user e accessToken. Isso reduz bugs como “logout mas user ainda aparece” ou “erro de login persistindo após sucesso”.

3) Crie funções de persistência do token (com cuidado)

Você pode persistir o token para restaurar sessão ao recarregar a página. Uma abordagem simples é usar localStorage. Crie funções pequenas para isolar isso:

const TOKEN_KEY = "access_token"; function saveToken(token) { localStorage.setItem(TOKEN_KEY, token); } function loadToken() { return localStorage.getItem(TOKEN_KEY); } function clearToken() { localStorage.removeItem(TOKEN_KEY); }

Importante: persistir token em localStorage é comum, mas tem implicações de segurança (ex.: XSS). Em muitos projetos, prefere-se cookies httpOnly para refresh token e manter access token em memória. Aqui, o foco é a modelagem do estado; a estratégia de armazenamento deve seguir as decisões de segurança do seu backend.

4) Implemente um cliente de API mínimo (ou adaptador) para login e /me

O Provider precisa chamar endpoints. Para manter o capítulo focado, use um adaptador simples com fetch. Exemplo:

async function apiSignIn({ email, password }) { const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }) }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message || "Falha no login"); } return res.json(); } async function apiMe(accessToken) { const res = await fetch("/api/auth/me", { headers: { Authorization: `Bearer ${accessToken}` } }); if (!res.ok) throw new Error("Sessão inválida"); return res.json(); }

Suponha que apiSignIn retorne algo como { accessToken, user }. Se seu backend retorna apenas token, você pode chamar /me em seguida para obter o usuário.

5) Construa o AuthProvider com restauração de sessão

O Provider é o coração do modelo: ele inicializa, restaura sessão e expõe ações. Um padrão útil é executar a restauração em um useEffect na montagem.

import React from "react"; import AuthContext from "./AuthContext"; export function AuthProvider({ children }) { const [state, dispatch] = React.useReducer(authReducer, initialState); const restoreSession = React.useCallback(async () => { dispatch({ type: "RESTORE_START" }); const token = loadToken(); if (!token) { dispatch({ type: "RESTORE_FAILURE" }); return; } try { const user = await apiMe(token); dispatch({ type: "RESTORE_SUCCESS", payload: { user, accessToken: token } }); } catch (e) { clearToken(); dispatch({ type: "RESTORE_FAILURE" }); } }, []); React.useEffect(() => { restoreSession(); }, [restoreSession]); const signIn = React.useCallback(async ({ email, password }) => { try { const data = await apiSignIn({ email, password }); saveToken(data.accessToken); dispatch({ type: "SIGNIN_SUCCESS", payload: { user: data.user, accessToken: data.accessToken } }); return { ok: true }; } catch (e) { dispatch({ type: "SIGNIN_FAILURE", payload: { error: e.message } }); return { ok: false, error: e.message }; } }, []); const signOut = React.useCallback(() => { clearToken(); dispatch({ type: "SIGNOUT" }); }, []); const value = React.useMemo(() => ({ status: state.status, user: state.user, error: state.error, signIn, signOut, refreshSession: restoreSession }), [state.status, state.user, state.error, signIn, signOut, restoreSession]); return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }

Repare em algumas decisões:

  • restoreSession tenta ler token e validar com /me. Se falhar, limpa token e marca como não autenticado.
  • signIn retorna um objeto de resultado ({ok, error}) para a UI decidir o que fazer (mostrar mensagem, redirecionar etc.).
  • useMemo evita recriar o objeto value a cada render sem necessidade, reduzindo renders em cascata.

6) Crie o hook useAuth com validação

Um hook dedicado melhora a ergonomia e evita que componentes usem o Context fora do Provider. Exemplo:

import React from "react"; import AuthContext from "./AuthContext"; export function useAuth() { const ctx = React.useContext(AuthContext); if (!ctx) { throw new Error("useAuth deve ser usado dentro de <AuthProvider>"); } return ctx; }

Agora, em qualquer componente, você usa:

const { status, user, signIn, signOut, error } = useAuth();

Como consumir o estado na UI sem duplicar lógica

Renderização baseada em status (evitando “flash”)

Quando status é "checking", você ainda não sabe se há sessão válida. Em vez de renderizar imediatamente páginas públicas ou privadas, você pode renderizar um placeholder de carregamento em um ponto central (por exemplo, no componente raiz do app).

function AppShell() { const { status } = useAuth(); if (status === "checking") { return <p>Carregando sessão...</p>; } return <YourRoutes />; }

Isso evita que a aplicação mostre a tela de login por um instante e depois “pule” para a área autenticada quando a restauração terminar.

Exemplo de formulário de login usando signIn

Um formulário pode chamar signIn e reagir ao resultado. O componente não precisa saber como token é armazenado nem como o usuário é carregado.

function LoginForm() { const { signIn, error, status } = useAuth(); const [email, setEmail] = React.useState(""); const [password, setPassword] = React.useState(""); const [submitting, setSubmitting] = React.useState(false); async function handleSubmit(e) { e.preventDefault(); setSubmitting(true); const result = await signIn({ email, password }); setSubmitting(false); if (!result.ok) { return; } } return ( <form onSubmit={handleSubmit}> <input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="email" /> <input value={password} onChange={(e) => setPassword(e.target.value)} placeholder="senha" type="password" /> <button disabled={submitting || status === "checking"}>Entrar</button> {error ? <p>{error}</p> : null} </form> ); }

Observe que o componente lida apenas com estado de formulário e feedback. A autenticação em si é responsabilidade do Provider.

Separando “estado” de “operações”: hooks especializados

Conforme o app cresce, você pode querer separar o consumo do estado em hooks menores para reduzir renders e melhorar legibilidade. Exemplos:

  • useAuthUser() retorna apenas user.
  • useIsAuthenticated() retorna um booleano derivado de status.
  • useAuthActions() retorna apenas signIn, signOut, refreshSession.

Isso pode ser feito com um único Context, mas componentes que consomem o Context inteiro re-renderizam quando qualquer parte do value muda. Para otimizar, você pode:

  • Dividir em dois Contexts: AuthStateContext e AuthActionsContext.
  • Ou usar bibliotecas de selector, mas aqui manteremos o padrão nativo.

Exemplo de divisão simples em dois contexts:

const AuthStateContext = React.createContext(null); const AuthActionsContext = React.createContext(null); export function AuthProvider({ children }) { const [state, dispatch] = React.useReducer(authReducer, initialState); const actions = React.useMemo(() => ({ signIn, signOut, refreshSession: restoreSession }), [signIn, signOut, restoreSession]); return ( <AuthStateContext.Provider value={state}> <AuthActionsContext.Provider value={actions}> {children} </AuthActionsContext.Provider> </AuthStateContext.Provider> ); } export function useAuthState() { const s = React.useContext(AuthStateContext); if (!s) throw new Error("useAuthState deve ser usado dentro do provider"); return s; } export function useAuthActions() { const a = React.useContext(AuthActionsContext); if (!a) throw new Error("useAuthActions deve ser usado dentro do provider"); return a; }

Esse padrão reduz renders em componentes que só precisam das ações (por exemplo, um botão de logout) e não precisam re-renderizar quando user muda.

Sincronizando autenticação com chamadas HTTP (sem espalhar token)

Mesmo que você mantenha o token no estado, é melhor evitar que componentes montem headers manualmente. Uma abordagem é centralizar isso em um “API client” que lê o token do Provider. Existem duas estratégias comuns:

  • Injeção por função: o Provider cria um cliente com o token atual e expõe funções de API já autenticadas.
  • Interceptor (quando usando Axios): configurar o header Authorization automaticamente.

Com fetch, você pode expor um helper no contexto:

async function authFetch(url, options = {}) { const token = state.accessToken; const headers = { ...(options.headers || {}), ...(token ? { Authorization: `Bearer ${token}` } : {}) }; const res = await fetch(url, { ...options, headers }); if (res.status === 401) { signOut(); } return res; }

Se você expor authFetch no contexto, componentes chamam authFetch e não precisam conhecer token. Porém, cuidado para não recriar funções a cada render sem useCallback, e para não acoplar demais UI com detalhes de rede. Em muitos casos, é melhor manter serviços de API separados e o Provider apenas gerenciar sessão.

Atualização de usuário e consistência de dados

Após autenticar, o usuário pode atualizar perfil (nome, avatar etc.). Se a UI depende de user no contexto, você precisa de uma forma de atualizar esse dado sem forçar um novo login. Duas opções:

  • Recarregar chamando refreshSession() (que chama /me).
  • Atualizar localmente com uma ação UPDATE_USER após uma resposta de API de atualização de perfil.

Exemplo de atualização local:

function updateUser(user) { dispatch({ type: "UPDATE_USER", payload: { user } }); }

Você pode expor updateUser no contexto para páginas de perfil, mantendo a UI sincronizada sem depender de recarregar toda a sessão.

Tratando expiração de token e logout automático

JWTs expiram. Se você não modelar isso, o usuário pode ficar “aparentemente logado” até a primeira requisição falhar. Há algumas abordagens:

  • Reagir a 401: se uma chamada autenticada retornar 401, executar signOut() e limpar estado.
  • Checar expiração no client: decodificar o JWT (sem validar assinatura) para ler exp e agendar um logout/refresh. Isso melhora UX, mas não substitui validação no servidor.
  • Refresh token: manter um mecanismo de renovação (normalmente via cookie httpOnly) e, no Provider, tentar renovar antes de declarar unauthenticated.

Um exemplo simples de checagem de expiração (sem biblioteca) é decodificar o payload base64. Isso é útil para decidir se vale chamar /me ou se o token já expirou:

function parseJwt(token) { try { const payload = token.split(".")[1]; const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/")); return JSON.parse(json); } catch { return null; } } function isExpired(token) { const data = parseJwt(token); if (!data || !data.exp) return true; return Date.now() >= data.exp * 1000; }

No restoreSession, você poderia fazer:

const token = loadToken(); if (!token || isExpired(token)) { clearToken(); dispatch({ type: "RESTORE_FAILURE" }); return; }

Isso evita chamadas desnecessárias ao backend quando o token já está expirado.

Erros comuns ao modelar autenticação com Context

1) Provider recriando value e causando renders excessivos

Se você cria o objeto value inline sem useMemo, qualquer render do Provider muda a referência e força re-render em todos os consumidores. Use useMemo e useCallback para estabilizar referências.

2) Misturar “carregando login” com “checando sessão”

status pode representar a sessão global, mas o formulário de login pode ter seu próprio submitting. Evite usar um único booleano global para tudo, senão um login em andamento pode bloquear telas desnecessariamente.

3) Expor token para qualquer componente

Quanto mais lugares conhecem o token, mais difícil é controlar segurança e consistência. Prefira expor ações e dados do usuário. Se precisar de token para um serviço, injete via camada de API centralizada.

4) Não limpar estado ao falhar restauração

Se /me falha, limpe token e zere user. Caso contrário, você pode ficar com UI exibindo dados antigos.

Checklist prático do que seu AuthProvider deve garantir

  • Ter um status explícito com pelo menos checking, authenticated, unauthenticated.
  • Restaurar sessão na montagem (ler token, validar, carregar usuário).
  • Centralizar signIn e signOut com transições claras no reducer.
  • Persistir/limpar token de forma consistente.
  • Expor um hook useAuth (ou useAuthState/useAuthActions) com validação de uso dentro do Provider.
  • Evitar renders desnecessários com useMemo/useCallback.

Agora responda o exercício sobre o conteúdo:

Qual modelagem de autenticação ajuda a evitar o flash de UI e torna as transições mais previsíveis em uma SPA React?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Um status finito permite representar estados intermediários como checking, evitando renderizar telas públicas/privadas antes da sessão ser validada e mantendo transições consistentes via Provider e reducer.

Próximo capitúlo

Fluxo de login e logout com persistência de sessão e sincronização de UI

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.