A tabela é o widget mais subestimado do Shiny. Todo dashboard tem uma, e a maioria é feita mal: renderTable cuspindo um HTML estático que trava com dez mil linhas, sem busca, sem ordenação, sem paginação. Uma boa tabela é interativa, rápida com grandes volumes, formatada com significado (cores, barras, ícones), editável quando preciso, e conectada ao resto do app (selecionar uma linha filtra um gráfico). Este módulo cobre os três grandes pacotes — DT, reactable e gt — e quando usar cada um.

Dominar tabelas interativas de produção: DT para grandes volumes com processamento no servidor, seleção e edição; reactable para tabelas modernas com formatação rica e sub-linhas; gt para tabelas de publicação. Aprender a atualizar tabelas sem recriá-las (dataTableProxy) e a conectá-las ao grafo reativo.

Escolhendo o pacote certo

Os três pacotes resolvem problemas parcialmente diferentes. Escolher errado gera retrabalho:

  • DT (wrapper do DataTables.js): o cavalo de batalha interativo. Busca, ordenação, paginação, seleção de linhas, edição de células, e — crucial — processamento no servidor para milhões de linhas. É o padrão para tabelas que interagem com o app.
  • reactable (wrapper do React Table): tabelas modernas e bonitas, com formatação por célula (barras, cores, ícones), linhas expansíveis (detalhe aninhado), agrupamento e sparklines. Ótimo quando a estética e a formatação rica importam.
  • gt (grammar of tables): tabelas de publicação — relatórios, PDFs, artigos. Cabeçalhos agrupados, rótulos, notas de rodapé, formatação tipográfica impecável. Menos interativo; é para exibir, não para interagir.

Regra rápida: interação e volume → DT; formatação rica e visual → reactable; relatório estático elegante → gt.

DT: o essencial interativo

library(DT)
library(shiny)

ui <- fluidPage(DTOutput("tab"))

server <- function(input, output, session) {
  output$tab <- renderDT({
    datatable(
      iris,
      filter = "top",                      # caixa de filtro por coluna
      selection = "single",                # 'single' | 'multiple' | 'none'
      rownames = FALSE,
      options = list(
        pageLength = 10,                    # linhas por página
        lengthMenu = c(10, 25, 50),
        order = list(list(0, "desc")),      # ordena pela 1ª coluna, desc
        scrollX = TRUE                      # rolagem horizontal
      )
    )
  })
}
shinyApp(ui, server)

Seleção de linha conectada ao app

O recurso que transforma uma tabela em ferramenta: ler qual linha o usuário selecionou via input$TABID_rows_selected.

server <- function(input, output, session) {
  output$tab <- renderDT(
    datatable(mtcars, selection = "single"), server = TRUE
  )

  # input$tab_rows_selected traz o(s) índice(s) da(s) linha(s) selecionada(s)
  output$detalhe <- renderPrint({
    sel <- input$tab_rows_selected
    req(sel)                               # nada selecionado → para
    mtcars[sel, ]
  })
}

Os "inputs mágicos" do DT que você mais usará: input$tab_rows_selected (linhas selecionadas), input$tab_rows_all (todas as linhas após filtro), input$tab_cell_clicked (célula clicada), input$tab_search (texto da busca). São a ponte entre a tabela e o resto da reatividade.

Processamento no servidor: milhões de linhas

Por padrão, renderDT(..., server = TRUE) mantém os dados no servidor e envia ao navegador só a página visível. Isso é o que permite paginar, ordenar e buscar em datasets enormes sem travar o browser — o DataTables pede ao R apenas as ~10 linhas de cada página.

# server = TRUE (padrão em renderDT) → só a página atual trafega ao cliente
output$tab <- renderDT(
  server = TRUE,
  {
    datatable(dados_gigantes,              # milhões de linhas: sem problema
              filter = "top",
              options = list(pageLength = 25))
  }
)

Contraste: server = FALSE manda tudo ao navegador de uma vez — aceitável para poucos milhares de linhas, desastroso para milhões. Regra: em dúvida, deixe server = TRUE.

Formatação e edição

output$tab <- renderDT({
  datatable(vendas, editable = "cell") |>   # dá para editar clicando na célula
    formatCurrency("faturamento", currency = "R$ ", dec.mark = ",", mark = ".") |>
    formatPercentage("margem", digits = 1) |>
    formatStyle(
      "faturamento",
      background = styleColorBar(vendas$faturamento, "lightblue"),  # mini-barra
      color = styleInterval(1000, c("red", "green"))                # cor por faixa
    )
})

# Capturar edições: input$tab_cell_edit traz row, col e o novo value
observeEvent(input$tab_cell_edit, {
  info <- input$tab_cell_edit
  vendas[info$row, info$col] <<- DT::coerceValue(info$value, vendas[info$row, info$col])
})

formatStyle + styleColorBar/styleInterval dão as "mini-barras" e cores condicionais diretamente na célula, sem sair do R.

Atualização sem recriar: dataTableProxy

Como nos mapas e gráficos, recriar a tabela toda perde o estado (página atual, busca, ordenação). Para atualizar só os dados, use dataTableProxy + replaceData.

output$tab <- renderDT(datatable(dados_iniciais), server = TRUE)

proxy <- dataTableProxy("tab")

observeEvent(input$atualizar, {
  replaceData(proxy, novos_dados, resetPaging = FALSE)  # mantém a página atual
})

reactable: formatação rica e linhas expansíveis

Quando o objetivo é uma tabela visualmente rica — barras dentro das células, cores condicionais, ícones, e sobretudo linhas que expandem para mostrar detalhe — reactable brilha.

library(reactable)

reactable(
  vendas,
  searchable = TRUE, filterable = TRUE, defaultPageSize = 10,
  striped = TRUE, highlight = TRUE,
  columns = list(
    faturamento = colDef(
      name = "Faturamento",
      cell = function(value) sprintf("R$ %.2f", value),
      style = function(value) {
        cor <- if (value > 1000) "#1a9850" else "#d73027"
        list(color = cor, fontWeight = "bold")
      }
    ),
    margem = colDef(
      name = "Margem",
      cell = reactablefmt::data_bars(vendas, fill_color = "#5b9bd5")  # barra na célula
    )
  ),
  # Linha expansível: detalhe aninhado
  details = function(index) {
    sub <- detalhes[detalhes$id == vendas$id[index], ]
    reactable(sub, outlined = TRUE)      # uma tabela dentro da linha!
  }
)

No Shiny, reactable tem renderReactable/reactableOutput e um proxy próprio (updateReactable) para atualizar dados, seleção e expansão sem recriar. A seleção de linhas chega via getReactableState("tab", "selected").

output$tab <- renderReactable({
  reactable(dados, selection = "multiple", onClick = "select")
})

observe({
  sel <- getReactableState("tab", "selected")   # índices selecionados
  # ... usar 'sel' no resto do app
})

gt: tabelas de publicação

Para relatórios e documentos, gt produz tabelas com qualidade tipográfica de revista: título e subtítulo, grupos de colunas (spanners), notas de rodapé, formatação de números e destaque de células.

library(gt)

vendas |>
  gt() |>
  tab_header(
    title = "Desempenho Trimestral",
    subtitle = "Por região — 2024"
  ) |>
  fmt_currency(columns = faturamento, currency = "BRL") |>
  fmt_percent(columns = margem, decimals = 1) |>
  tab_spanner(label = "Financeiro", columns = c(faturamento, margem)) |>
  data_color(
    columns = margem,
    fn = scales::col_numeric("RdYlGn", domain = c(0, 1))   # heatmap na coluna
  ) |>
  tab_footnote("Margem líquida após impostos.", locations = cells_column_labels(margem)) |>
  tab_source_note("Fonte: sistema interno de vendas.")

No Shiny há render_gt/gt_output. gt é menos interativo (sem paginação/seleção nativas), então use-o para o painel de resumo de um dashboard, não para a tabela principal de interação.

Padrão de decisão e integração

Um dashboard maduro frequentemente usa os três: um gt de KPIs no topo (resumo estático elegante), um reactable para uma visão rica com detalhe expansível, e um DT como tabela-mestra que dirige o app (selecionar linha → filtra gráficos). Todos se conectam ao grafo reativo pela mesma lógica: a seleção vira um input, o resto do app lê esse input.

# DT como mestre: selecionar cliente filtra todos os gráficos
output$clientes <- renderDT(datatable(clientes, selection = "single"), server = TRUE)

cliente_sel <- reactive({
  sel <- input$clientes_rows_selected
  req(sel)
  clientes$id[sel]
})

output$grafico  <- renderPlotly({ plot_cliente(cliente_sel()) })
output$historico <- renderReactable({ reactable(hist_do(cliente_sel())) })

São três pacotes com três propósitos distintos: o DT para interação e volume, o reactable para formatação rica e linhas expansíveis, e o gt para publicação. O DT expõe o que se pode chamar de inputs mágicos — _rows_selected, _rows_all, _cell_clicked, _cell_edit —, e é por eles que a tabela conversa com a reatividade. O server = TRUE, que é o padrão no renderDT, processa no servidor e trafega só a página visível: é o que torna milhões de linhas viáveis. Para formatar, formatStyle com styleColorBar ou styleInterval dá barras e cores condicionais, e editable habilita a edição, que chega em _cell_edit. Atualizar sem recriar preserva página, busca e ordenação, e se faz com dataTableProxy e replaceData no DT, ou updateReactable no reactable — que por sua vez é quem entrega linhas expansíveis com tabelas aninhadas e barras na célula, com a seleção lida por getReactableState. O gt fica para o resumo estático elegante, não para a tabela de interação principal. E as três se integram pelo mesmo princípio: a seleção vira input e o app reage.

Exercícios

  1. Você precisa exibir uma tabela de 3 milhões de linhas com busca e paginação sem travar o navegador. Qual pacote e qual configuração?

    ✓ Resposta:

    DT com renderDT(..., server = TRUE) (que é o padrão). No modo servidor, o DataTables mantém os dados no R e requisita ao servidor apenas a página visível (~10–25 linhas) por vez, junto com o resultado de busca/ordenação. Assim o navegador nunca recebe os 3 milhões de registros de uma vez. Usar server = FALSE aqui enviaria tudo ao cliente e travaria — é o erro clássico.

  2. Qual a diferença de propósito entre reactable e gt? Dê um caso de uso para cada.

    ✓ Resposta:

    reactable é para tabelas interativas e visualmente ricas — busca, filtro, ordenação, barras/cores por célula e, sobretudo, linhas expansíveis com detalhe aninhado. Caso de uso: uma tabela de vendas onde clicar numa linha expande os itens daquele pedido. gt é para tabelas de publicação estática — qualidade tipográfica, cabeçalhos agrupados, notas de rodapé. Caso de uso: a tabela-resumo de KPIs de um relatório que vira PDF. Interação → reactable; documento elegante → gt.

  3. Como você lê, no servidor, qual linha o usuário selecionou numa tabela DT chamada "clientes"? E como evita erro antes de qualquer seleção?

    ✓ Resposta:

    Lê-se pelo input mágico input$clientes_rows_selected, que traz o(s) índice(s) da(s) linha(s) selecionada(s) (respeitando selection = "single"/"multiple"). Para evitar erro antes da primeira seleção — quando o valor é NULL — usa-se req(input$clientes_rows_selected) no início do reactive/render, que aborta silenciosamente até haver seleção.

  4. Por que atualizar dados com dataTableProxy/replaceData é melhor que redesenhar a tabela dentro de renderDT?

    ✓ Resposta:

    Redesenhar dentro de renderDT recria a tabela do zero, perdendo o estado da interface: a página atual volta à primeira, a busca digitada é apagada e a ordenação escolhida se perde. dataTableProxy + replaceData(proxy, novos, resetPaging = FALSE) substitui apenas os dados mantendo página, busca e ordenação — atualização mais suave e sem "pulos" visuais, o mesmo princípio de leafletProxy/plotlyProxy.

  5. Você quer uma tabela onde cada célula da coluna "margem" mostre uma mini-barra proporcional ao valor e fique verde se > 20% e vermelha caso contrário. Qual pacote e quais funções?

    ✓ Resposta:

    Com DT, use formatStyle("margem", background = styleColorBar(dados$margem, "lightgreen"), color = styleInterval(0.20, c("red", "green"))) — styleColorBar desenha a mini-barra e styleInterval define a cor por faixa. Com reactable, use um colDef(style = function(value) list(color = if (value > 0.20) "green" else "red")) combinado com reactablefmt::data_bars() para a barra. Ambos resolvem; a escolha depende do resto da tabela (interação/volume → DT; estética → reactable).

Fontes e leituras recomendadas