Guia de Otimização · React Native · Firebase

Otimização do useOuvirCorridas.js

Vamos otimizar o useOuvirCorridas.js do início ao fim, explicando o que acontece em cada etapa, quais problemas podem surgir, como corrigi-los e como validar cada mudança antes de passar para a próxima.

A ideia é preservar as funcionalidades existentes: receber ofertas de corridas e entregas, filtrar por localização e categoria, controlar vagas, exibir o modal, tratar recusas e timeouts e acompanhar as mudanças em tempo real pelo Firebase.

1

Entender o fluxo completo antes de alterar o código

O hook atual executa várias tarefas que dependem umas das outras. A ordem ideal para revisar o sistema é esta:

1
Inicialização do aplicativo Autenticação, ID do motorista, estado online e histórico local.
2
Preparação do monitoramento Categorias, GPS, listeners e índices do Firebase.
3
Recepção e validação das ofertas Status, idade, coordenadas, raio, categoria e recusas.
4
Reserva de vaga Transação atômica, limite de motoristas e validade da reserva.
5
Fila e apresentação da oferta Ordenação, modal, timeout e atualização do status.
6
Aceitação ou recusa Confirmação no servidor, liberação de recursos e auditoria.
7
Encerramento e recuperação Cancelar listeners e timers, persistir dados e recuperar conexão.
💡 Vamos seguir essa sequência porque corrigir somente o modal, por exemplo, não resolve uma reserva duplicada causada anteriormente no Firebase.
2

Etapa inicial — preparar o motorista corretamente

No arquivo, a inicialização está distribuída por vários useEffect: recuperação do histórico, restauração do momento de login, atualização da posição, monitoramento do status e configuração das categorias.

O risco é começar a ouvir ofertas antes de todas as informações necessárias estarem prontas.

Passo 2.1 — definir as condições para começar

Antes de instalar listeners, o sistema precisa saber:

Nem todas essas condições precisam impedir a instalação de todo listener. Por exemplo, o monitoramento do status do motorista pode começar antes do GPS. Entretanto, o processamento de uma oferta que depende da localização deve aguardar uma posição válida.

Uma regra de inicialização pode ser representada assim:

const podeMonitorarOfertas =
  Boolean(motoristaId) &&
  isOnline === true &&
  historicoCarregado === true &&
  Boolean(momentoLogin) &&
  categoriasEfetivasRef.current.length > 0;
⚠️ Essa variável é apenas uma referência conceitual. Como a lista de categorias está em um ref, a implementação final deve garantir que uma alteração das categorias também atualize o estado reativo que controla o efeito.

Por que isso melhora o desempenho? Evita processar ofertas que serão descartadas por falta de dados e reduz trabalho desnecessário durante a inicialização.

Passo 2.2 — validar a posição GPS

O código atual atualiza posicaoRef sempre que minhaPosicao muda, mas a validação das coordenadas é feita em outro ponto. Centralize a validação:

function posicaoValida(posicao) {
  const latitude = Number(posicao?.latitude);
  const longitude = Number(posicao?.longitude);

  return (
    Number.isFinite(latitude) &&
    Number.isFinite(longitude) &&
    latitude >= -90 &&
    latitude <= 90 &&
    longitude >= -180 &&
    longitude <= 180
  );
}

E, no processamento:

const posicao = posicaoRef.current;

if (!posicaoValida(posicao)) {
  pendentesSemPosicaoRef.current.set(k, {
    tipo,
    oferta,
    id,
  });

  return;
}
🔴 É importante limitar o tamanho dessa coleção de pendências e eliminar registros expirados. Caso contrário, uma sequência de ofertas recebidas sem GPS pode acumular dados na memória.

Passo 2.3 — corrigir o momento de login

O hook persiste MOMENTO_LOGIN_KEY e usa esse valor em uma consulta startAt(momentoLogin).

Há um detalhe: o momento de login salvo no dispositivo não deve ser tratado como uma prova de que o motorista está autenticado, nem como garantia de que todas as ofertas relevantes serão encontradas.

Precisamos distinguir:

Também é necessário decidir se ofertas criadas antes de o motorista ficar online devem ser ignoradas ou se ele deve receber ofertas ainda válidas. O comportamento atual tende a restringir a busca pelo momento salvo.

3

Etapa de monitoramento — otimizar o Firebase

O código possui duas consultas principais, uma para corridas e outra para entregas, cada uma com onChildAdded e onChildChanged. Também cria listeners adicionais para ofertas aguardando vaga e para a oferta aberta no modal.

Passo 3.1 — manter apenas os listeners necessários

O fluxo recomendado é:

1
Instalar os listeners principais quando o motorista estiver habilitado.
2
Receber uma oferta e validar os dados locais básicos.
3
Evitar criar outro listener se a oferta já estiver sendo acompanhada.
4
Instalar acompanhamento individual somente quando for realmente necessário.
5
Cancelar o acompanhamento quando a oferta expirar, mudar de status, for recusada ou deixar de ser relevante.
⚠️ Não recomendo remover indiscriminadamente os listeners de onValue: o acompanhamento em tempo real do status de uma oferta pode ser importante para impedir que um modal permaneça aberto depois que outro motorista aceita a corrida.

Passo 3.2 — configurar índices e reduzir o volume de dados

A consulta atual utiliza orderByChild('criadoEmMs'). Configure o índice correspondente no Realtime Database e verifique se o campo existe em todas as ofertas consultadas.

O limite de 30 registros é um começo razoável, mas não garante que todas as ofertas relevantes serão consideradas. Se houver mais de 30 ofertas recentes, o resultado dependerá da ordenação e dos filtros posteriores.

O ajuste correto depende de métricas como:

Meta: reduzir tráfego e processamento sem perder ofertas válidas.

Passo 3.3 — impedir processamento concorrente da mesma oferta

A deduplicação atual utiliza uma janela de 30 segundos. Isso ajuda a reduzir eventos repetidos, mas não impede que duas chamadas já iniciadas executem operações assíncronas simultaneamente.

Uma solução é manter uma coleção de ofertas em processamento:

const processandoRef = useRef(new Set());

No início da função que processa a oferta:

const k = `${tipo}:${id}`;

if (processandoRef.current.has(k)) {
  return;
}

processandoRef.current.add(k);

try {
  // Validar a oferta e executar o processamento.
} finally {
  processandoRef.current.delete(k);
}
💡 Esse exemplo precisa ser integrado à função processarOferta() com cuidado. A chave não deve ser liberada antes de terminar toda a operação crítica, e ofertas que precisem ser reprocessadas após uma alteração devem ter uma política explícita de reentrada.

A deduplicação por tempo e o bloqueio de processamento resolvem problemas diferentes: a primeira limita repetições; o segundo evita operações simultâneas na mesma oferta.

4

Etapa de análise da oferta — decidir se deve ser apresentada

No código atual, uma oferta passa por verificações de status, ocupação, localização, idade, raio, categoria, histórico de notificações e reserva de vaga.

A sequência é importante: as validações mais baratas devem acontecer antes de operações que acessam a rede.

Passo 4.1 — usar esta ordem de validação

1
IdentificaçãoConfirmar que o tipo é corrida ou entrega e que o ID é válido.
2
StatusDescartar imediatamente ofertas que não estejam procurando motorista.
3
DisponibilidadeNão processar novas ofertas se o motorista estiver ocupado.
4
Dados obrigatóriosValidar origem, data de criação e categoria.
5
IdadeDescartar ofertas expiradas.
6
DistânciaCalcular o raio permitido e verificar se a origem está dentro dele.
7
Categoria e recusaVerificar as regras de compatibilidade e os períodos de espera.
8
HistóricoLimitar notificações repetidas.
9
ReservaTentar reservar uma vaga somente quando a oferta tiver passado pelas verificações anteriores.

Isso evita executar uma transação no Firebase para uma oferta que já deveria ter sido descartada.

Passo 4.2 — corrigir o cálculo da idade

A função atual retorna idade zero quando não encontra uma data válida, pois utiliza o horário atual como referência:

const base = criadoEmMs || agora;

Isso pode fazer uma oferta sem data ser tratada como recém-criada.

Se o campo de criação for obrigatório, valide-o antes:

function criadoEmValido(valor) {
  return (
    Number.isFinite(valor) &&
    valor > 0 &&
    valor <= Date.now() + 60000
  );
}
💡 A tolerância de um minuto para horários futuros é apenas ilustrativa e deve ser ajustada ao funcionamento real do servidor. O ideal é normalizar a data na criação da oferta e usar uma referência de tempo confiável.

Passo 4.3 — otimizar o cálculo de distância

A fórmula de Haversine utilizada no arquivo é adequada para estimar a distância geográfica entre dois pontos.

Para volumes elevados, você pode primeiro aplicar um filtro aproximado por latitude e longitude e executar Haversine apenas para os candidatos próximos. Isso reduz cálculos trigonométricos.

No entanto, com apenas 30 ofertas por consulta, essa otimização provavelmente será menos importante do que reduzir leituras, listeners e operações concorrentes.

⚠️ Também não confunda distância em linha reta com distância por ruas. Se a regra de negócio usa raio geográfico, Haversine atende; se precisa considerar tempo de deslocamento ou trajetos rodoviários, será necessária outra estratégia.
5

Etapa de reserva — impedir vagas duplicadas

A função reservarVaga() é uma das partes mais sensíveis do sistema.

Atualmente, a reserva é registrada em corridas_meta ou entregas_meta, dentro do nó ofertados, com um limite de três motoristas e uma validade de 35 segundos.

Passo 5.1 — definir o significado da reserva

É importante separar três conceitos:

Conceito 1

Oferta enviada

O motorista foi selecionado para visualizar a oportunidade.

Conceito 2

Reserva temporária

O motorista tem uma vaga para responder durante um prazo.

Conceito 3

Corrida aceita

A atribuição foi confirmada e não pode ser aceita por outro motorista.

A reserva temporária não deve, sozinha, significar que a corrida foi aceita.

Passo 5.2 — controlar a expiração

O código considera uma reserva expirada quando agora - em > TTL_OFERTADO_MS. Isso é funcional como mecanismo de limpeza, mas utiliza o relógio do dispositivo que executa a transação.

Para uma implementação mais confiável:

1
Use um identificador estável por motorista.
2
Faça a reserva por transação.
3
Verifique o limite dentro da transação.
4
Registre o momento da reserva.
5
Revalide o prazo ao tentar aceitar.
6
Faça uma limpeza controlada das reservas expiradas.
7
Libere a vaga ao recusar ou concluir o fluxo.
🔴 Se o volume de ofertas crescer, a transação sobre o nó inteiro ofertados poderá causar contenção. Uma arquitetura baseada em servidor pode ser necessária para distribuir vagas em escala.

Passo 5.3 — tornar a aceitação atômica

Esta é a mudança arquitetural mais importante.

Quando um motorista toca em "Aceitar", o sistema precisa verificar e alterar o estado da corrida como uma única operação lógica. Dois motoristas não podem aceitar a mesma corrida simultaneamente.

A operação de aceitação deve verificar no servidor:

O servidor deve então confirmar o motorista vencedor e atualizar o estado da corrida atomicamente. O hook deve tratar o resultado confirmado, não assumir que a aceitação ocorreu apenas porque uma gravação foi enviada.

6

Etapa do modal — manter a interface consistente

O hook controla ofertaParaModal, uma fila ordenada por distância e um timeout de 10 segundos. Além disso, acompanha mudanças no status da oferta para fechar o modal quando ela deixa de estar disponível.

O fluxo ideal é:

1
Selecionar a oferta válida mais próxima.
2
Mostrar o modal.
3
Iniciar o prazo de resposta.
4
Atualizar o modal se a oferta mudar.
5
Se ela expirar ou for aceita por outro motorista, removê-la da fila.
6
Se o motorista recusar ou o tempo acabar, liberar a reserva e avaliar a próxima oferta.
7
Evitar que uma resposta atrasada abra novamente um modal antigo.

Problema importante: fechamento e fila

Na implementação atual, setOfertaParaModal(null) pode ocorrer quando a oferta deixa de estar disponível, mas a lógica que seleciona a próxima oferta não é centralizada em todos os caminhos.

Isso pode deixar a fila parada até outro evento acontecer.

Uma melhoria é criar uma única função responsável por avançar a fila e chamá-la de forma consistente quando:

💡 Também recomendo que o timeout seja identificado pela chave da oferta, para evitar que o timeout de um modal antigo recuse acidentalmente uma oferta diferente.
7

Etapa de aceitação, recusa e timeout

O hook possui funções para aceitar, recusar, liberar reservas, registrar eventos e permitir novas tentativas. É importante que todas essas operações sigam uma sequência coerente.

Passo 7.1 — aceitar uma corrida

A sequência recomendada é:

1
O motorista solicita a aceitação.
2
O servidor verifica o status atual e a autorização.
3
A atribuição é confirmada atomicamente.
4
O aplicativo atualiza o estado local com o resultado confirmado.
5
Os listeners e timers relacionados a ofertas incompatíveis são cancelados.
6
A reserva temporária é encerrada conforme a política do sistema.
7
O evento de auditoria é registrado.

Se a aceitação falhar, o aplicativo deve receber o motivo e atualizar a interface sem mostrar a corrida como confirmada.

Passo 7.2 — recusar uma corrida

A recusa precisa ser idempotente: executar a mesma operação duas vezes não deve causar efeitos duplicados.

A função deve verificar se a oferta ainda está ativa para aquele motorista, registrar a recusa uma única vez e liberar a reserva correspondente.

⚠️ Atenção a um detalhe no código atual: a função recusarOferta() usa setTimeout(() => abrirProximaOferta(), 300). Esse atraso pode funcionar visualmente, mas não garante que a fila esteja consistente quando o callback for executado.

É melhor coordenar a remoção da oferta, a liberação da reserva e a escolha da próxima oferta em um fluxo controlado, com tratamento explícito de erros.

Passo 7.3 — controlar as novas tentativas

O arquivo usa um intervalo de 90 segundos e um máximo de duas novas tentativas para recusas. Esses parâmetros devem permanecer configuráveis, mas é importante esclarecer o significado de cada tentativa.

Uma nova tentativa deve ocorrer somente quando:

🔴 Evite apagar uma recusa no Firebase antes de verificar todas essas condições. Caso contrário, a recusa pode ser removida mesmo quando a oferta já não pode ser reapresentada.
8

Etapa final — limpar recursos e recuperar o aplicativo

A limpeza dos recursos é essencial para que o sistema permaneça estável depois de horas de uso.

No código atual, existem listeners, intervalos e timeouts em vários efeitos. Alguns já possuem funções de cancelamento, o que é positivo, mas precisamos garantir que nenhum caminho deixe recursos ativos indevidamente.

Passo 8.1 — fazer uma limpeza centralizada

Ao ficar offline, trocar de motorista ou desmontar o hook, a aplicação deve:

💡 Há uma distinção importante: cancelar o listener não cancela automaticamente uma transação que já foi enviada ao Firebase. Por isso, operações assíncronas precisam verificar se ainda são relevantes depois de cada await.

Passo 8.2 — lidar com reconexão

Quando a conexão é interrompida, o Firebase pode reconectar e entregar novos eventos. O aplicativo deve suportar:

Na reconexão, revalide o estado real da oferta no Firebase antes de apresentá-la. Não confie apenas na cópia que ficou na memória.

Passo 8.3 — testar a estabilidade durante o uso prolongado

O teste não deve terminar depois de abrir e aceitar uma corrida. Execute sessões prolongadas e acompanhe o crescimento de memória, o número de listeners, as gravações e o tempo de resposta.

Uma boa meta é que, depois de aceitar ou recusar uma oferta, os recursos associados sejam liberados e a quantidade de listeners permaneça dentro do esperado.

9

Como testar cada etapa antes de publicar

Use esta lista de verificação durante o desenvolvimento.

1. Inicialização
Abrir o app com GPS disponível
Abrir sem permissão de localização
Restaurar histórico salvo
Trocar de motorista ou sair da conta
2. Monitoramento
Receber uma oferta nova
Receber alteração repetida da mesma oferta
Ficar offline e voltar online
Verificar cancelamento dos listeners
3. Validação
Oferta fora do raio
Oferta expirada
Categoria incompatível
Coordenadas inválidas
Oferta sem data de criação
4. Reservas
Dois motoristas disputando a última vaga
Reserva expirada
Recusa seguida de nova tentativa
Aceitação simultânea por dois motoristas
5. Interface
Abrir e fechar o modal
Expirar o timeout
Oferta removida enquanto o modal está aberto
Duas ofertas simultâneas na fila
6. Estabilidade
Perder conexão durante uma reserva
Voltar do segundo plano
Usar o app por várias horas
Sair da tela durante uma operação assíncrona
10

Ordem definitiva de otimização

Eu implementaria as mudanças nesta ordem:

Fase 1 Inicialização e validações Menos processamento prematuro e dados inválidos.
Fase 2 Listeners e índices Menos tráfego e processamento redundante.
Fase 3 Concorrência e deduplicação Menos ofertas duplicadas e operações simultâneas.
Fase 4 Reserva e aceitação no servidor Maior integridade e prevenção de dupla atribuição.
Fase 5 Fila e modal Fluxo previsível de ofertas.
Fase 6 Recusa, timeout e retry Menos reservas presas e novas tentativas indevidas.
Fase 7 Limpeza e reconexão Maior estabilidade em sessões prolongadas.
Fase 8 Testes e métricas Evidências de que as mudanças realmente melhoraram o sistema.
🔴 Uma ressalva importante: a revisão do arquivo permite identificar riscos no cliente, mas não comprova quais falhas de segurança estão efetivamente exploráveis sem examinar as regras do Realtime Database, o código que cria e aceita corridas e o fluxo de autenticação.
✅ Para otimizar de verdade, o próximo passo é trabalhar sobre o código original em alterações pequenas e verificáveis, começando pela inicialização e pelo controle dos listeners. Depois, avançamos para a reserva atômica e a aceitação no servidor, sem substituir o hook inteiro às cegas.