Paginação com Supabase e React: guia com range()

Paginação com Supabase e React: como fazer do jeito certo
A paginação com Supabase e React se resolve com o método range(from, to) do supabase-js, que retorna apenas um intervalo de linhas da tabela. No React, você controla o número da página em um estado (ou usa o useInfiniteQuery do TanStack Query) e calcula o intervalo a cada mudança. Assim o app carrega só o necessário, fica mais rápido e não estoura o limite padrão de linhas por requisição.
Por que paginar os dados do Supabase
Buscar uma tabela inteira com select('*') funciona em desenvolvimento, mas vira problema em produção. O Supabase usa o PostgREST por trás, que aplica um limite máximo de linhas por resposta (o padrão em projetos Supabase é 1000). Se sua tabela passar disso, os dados simplesmente vêm cortados.
Além do limite, paginar traz ganhos práticos:
- Menos dados trafegando na rede, o que melhora o carregamento em celulares.
- Menos renderização no React, com listas leves e interface fluida.
- Menor consumo de banda e de leituras no banco.
- Melhor experiência para o usuário, que vê conteúdo rápido.
Como usar o range() do supabase-js
O range(from, to) usa índices que começam em zero e incluem o último valor. Ou seja, range(0, 9) retorna 10 linhas. Um helper simples calcula o intervalo a partir da página e do tamanho:
const PAGE_SIZE = 10
async function buscarProdutos(pagina) {
const from = pagina * PAGE_SIZE
const to = from + PAGE_SIZE - 1
const { data, error, count } = await supabase
.from('produtos')
.select('*', { count: 'exact' })
.order('created_at', { ascending: false })
.range(from, to)
if (error) throw error
return { data, count }
}
Dois detalhes importam aqui. Primeiro, sempre use order(): sem uma ordenação estável, a mesma linha pode aparecer em duas páginas ou sumir entre elas. Segundo, a opção count: 'exact' devolve o total de registros, útil para calcular o número de páginas, mas ela tem custo em tabelas muito grandes. Nesses casos, considere count: 'estimated'.
Paginação numerada no React
Para uma lista com botões "anterior" e "próxima", guarde a página em um estado e use o TanStack Query para cachear cada página:
const [pagina, setPagina] = useState(0)
const { data, isPending } = useQuery({
queryKey: ['produtos', pagina],
queryFn: () => buscarProdutos(pagina),
placeholderData: keepPreviousData,
})
const totalPaginas = Math.ceil((data?.count ?? 0) / PAGE_SIZE)
O keepPreviousData mantém a página anterior na tela enquanto a nova carrega, evitando que a lista pisque. A queryKey com o número da página faz cada página ser cacheada separadamente.
Scroll infinito com useInfiniteQuery
Em feeds e catálogos, o scroll infinito costuma funcionar melhor que botões. A documentação do TanStack Query v5 pede que o useInfiniteQuery receba initialPageParam e getNextPageParam:
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteQuery({
queryKey: ['produtos-infinito'],
queryFn: ({ pageParam }) => buscarProdutos(pageParam),
initialPageParam: 0,
getNextPageParam: (ultimaPagina, todasPaginas) =>
ultimaPagina.data.length === PAGE_SIZE ? todasPaginas.length : undefined,
})
Quando getNextPageParam retorna undefined, o hasNextPage vira falso e o carregamento para. Para disparar fetchNextPage, use um IntersectionObserver em um elemento no fim da lista, ou o botão "carregar mais".
Offset ou cursor: qual escolher
O range() usa offset, que é simples e ótimo para a maioria dos sites. O problema aparece em tabelas enormes: quanto maior o offset, mais o Postgres precisa ler e descartar linhas, e a consulta desacelera.
A alternativa é a paginação por cursor (keyset): você pede "os 10 registros com created_at menor que o último que eu vi", usando .lt('created_at', ultimoValor).order('created_at', { ascending: false }).limit(10). Ela mantém o desempenho estável e evita duplicatas quando novos itens entram durante a navegação. Crie um índice na coluna usada como cursor.
Regra prática: até algumas dezenas de milhares de linhas, range() resolve. Acima disso, ou em feeds que mudam o tempo todo, prefira cursor.
Perguntas Frequentes
O range() do Supabase é inclusivo?
Sim. Os dois valores contam e o índice começa em zero. range(0, 9) retorna as 10 primeiras linhas.
Qual o limite de linhas por consulta no Supabase? O padrão em projetos Supabase é de 1000 linhas por requisição, configurável nas configurações da API. Por isso paginar é necessário em tabelas grandes.
Como saber o total de páginas?
Use select('*', { count: 'exact' }) e divida o count pelo tamanho da página com Math.ceil.
Posso paginar sem TanStack Query?
Pode. Um useState com useEffect funciona, mas você perde cache, estados de carregamento e a revalidação automática que a biblioteca entrega.
Paginação por offset ou cursor, qual é mais rápida? O cursor mantém desempenho constante em tabelas grandes, já o offset desacelera conforme a página avança.
Conclusão
Para implementar a paginação com Supabase e React, comece com range() e order(), adicione o TanStack Query para cache e evolua para cursor se a tabela crescer muito. Na Stormcore, criamos sites e sistemas em React e Supabase pensando em desempenho desde o primeiro dia, e uma boa paginação é parte disso.
Matérias relacionadas

IA para criar apresentações: guia prático para empresas

Canal de Transmissão do Instagram para Empresas: Guia

Gerenciador de Senhas para Empresas: Como Escolher

Dark Mode com Tailwind CSS no React: Guia Passo a Passo

Stories do Instagram para empresas: guia prático

IA para Criar Propostas Comerciais: Guia Prático
Pronto para tirar sua ideia do papel?
Fale agora com nosso time de desenvolvimento. Sites, apps, automações, o que você precisar.
Chamar no WhatsAppResposta em até 2 horas