Tablas interactivas con DT y reactable

r
shiny
Las dos opciones modernas para tablas interactivas en Shiny. DT como veterano basado en DataTables.js, reactable como sucesor moderno. Cuándo cada uno, paginación, filtrado, formato condicional y edición.

DT vs reactable: las dos opciones

Para tablas interactivas en Shiny hay dos paquetes que vale la pena conocer:

DT, interfaz a la librería JavaScript DataTables. Lleva una década siendo el estándar. Madurísimo, muchísimo control, muchas opciones.

reactable, más reciente (2019, por Greg Lin), basado en React Table. Defaults mucho mejores, sintaxis más limpia, integración nativa con bslib.

Aspecto DT reactable
Defaults Razonables pero datados Modernos out-of-the-box
API Compleja (mucho JSON debajo) Limpia, idiomática R
Estética Genérica de DataTables Moderna, integra con bslib
Edición de celdas Sí, con editable Sí, con cellInfo y JS
Sparklines en celdas Posible con plugins Nativo con reactablefmtr
Comunidad y documentación Enorme Buena, en crecimiento
Cuándo usar Apps existentes, control fino, exportar a Excel/PDF Apps nuevas, estética moderna, dashboards

Si empiezas una app nueva, reactable. Si heredas código con DT, déjalo, son ambos buenos.

DT básico

library(shiny)
library(DT)

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

server <- function(input, output, session) {
  output$tabla <- renderDT({
    datatable(mtcars,
              options = list(pageLength = 10, searchHighlight = TRUE),
              rownames = FALSE)
  })
}

shinyApp(ui, server)

Por defecto, datatable() da paginación, ordenación por columna y búsqueda. Sin tocar nada más, ya es funcional.

Personalización frecuente

datatable(
  mtcars,
  options = list(
    pageLength    = 25,           # filas por página
    lengthMenu    = c(10, 25, 50, 100),
    dom           = 'tipr',       # qué controles mostrar (t=table, i=info, p=pag, r=processing)
    scrollX       = TRUE,         # scroll horizontal si hay muchas columnas
    autoWidth     = TRUE,
    columnDefs    = list(
      list(targets = "_all", className = "dt-center")
    )
  ),
  filter   = "top",               # filtro por columna en cabecera
  selection = "single",           # permitir seleccionar filas
  rownames  = FALSE,
  class     = "stripe hover"      # clases CSS de DataTables
)

El argumento dom es la cosa más críptica de DT, combina letras (cada una un componente: l length, f filter, t table, i info, p paginación, r processing). dom = 'tipr' significa “tabla + info + paginación + processing, sin la barra de búsqueda global ni el selector de filas por página”.

Capturar selección en server

ui <- fluidPage(
  DTOutput("tabla"),
  verbatimTextOutput("seleccionada")
)

server <- function(input, output, session) {
  output$tabla <- renderDT({
    datatable(mtcars, selection = "single", rownames = FALSE)
  })

  output$seleccionada <- renderPrint({
    fila <- input$tabla_rows_selected
    req(fila)
    mtcars[fila, ]
  })
}

El <outputId>_rows_selected te da el índice de la fila seleccionada. Con selection = "multiple", devuelve un vector.

Otros endpoints que DT expone al server:

  • input$tabla_rows_all: todas las filas tras filtrado.
  • input$tabla_rows_current: filas visibles en la página actual.
  • input$tabla_cell_clicked: celda concreta clicada (lista con row, col, value).

reactable básico

library(reactable)

ui <- fluidPage(
  reactableOutput("tabla")
)

server <- function(input, output, session) {
  output$tabla <- renderReactable({
    reactable(mtcars,
              searchable = TRUE,
              filterable = TRUE,
              pagination = TRUE,
              defaultPageSize = 10)
  })
}

Más legible que DT desde el primer vistazo. Los argumentos son funciones de R, no listas anidadas de JSON.

Personalización por columna

reactable(
  mtcars,
  columns = list(
    mpg  = colDef(name = "Millas/Galón", format = colFormat(digits = 1)),
    cyl  = colDef(name = "Cilindros", align = "center"),
    wt   = colDef(name = "Peso (1000 lb)", format = colFormat(digits = 2)),
    qsec = colDef(show = FALSE)   # ocultar columna
  ),
  defaultSorted = list(mpg = "desc"),
  highlight = TRUE,
  striped   = TRUE
)

colDef() agrupa todas las propiedades de una columna (nombre visible, formato, alineación, ancho, función de renderizado). Mucho más limpio que la API JSON de DataTables.

Formato condicional

Algo que en DT requiere JavaScript, en reactable es directo:

reactable(mtcars,
  columns = list(
    mpg = colDef(
      style = function(value) {
        if (value > 25) list(color = "green", fontWeight = "bold")
        else if (value < 15) list(color = "red")
      },
      format = colFormat(digits = 1)
    )
  )
)

Una función R que recibe el valor y devuelve estilo CSS como lista. Muy potente sin salir de R.

Para barras de progreso, sparklines, iconos, hay reactablefmtr:

library(reactablefmtr)

reactable(mtcars,
  columns = list(
    mpg = colDef(cell = data_bars(mtcars, fill_color = "#5F8575"))
  )
)

Una barra horizontal en cada celda proporcional al valor, el efecto de “tabla-gráfico” tan común en dashboards corporativos.

Selección en server

output$tabla <- renderReactable({
  reactable(mtcars, selection = "single", onClick = "select")
})

output$seleccionada <- renderPrint({
  fila <- getReactableState("tabla", "selected")
  req(fila)
  mtcars[fila, ]
})

getReactableState() accede al estado interno de la tabla. Más explícito que el patrón de DT con input$id_rows_selected.

Edición de celdas

Con DT:

output$tabla <- renderDT({
  datatable(mtcars, editable = "cell")
})

observeEvent(input$tabla_cell_edit, {
  info <- input$tabla_cell_edit
  message("Celda editada: fila ", info$row, " col ", info$col, " → ", info$value)
  # Actualizar el dataset reactivo aquí
})

editable = "cell" permite editar valores. input$tabla_cell_edit captura el cambio.

Con reactable, la edición es más limitada en la API básica. Para casos serios de edición masiva considera rhandsontable (spreadsheet-like) o DT que está más maduro en esta área.

Cuál elegir para tu caso

Necesidad Recomendación
Tabla de presentación, pocas filas, sin edición reactable
Dashboard moderno con KPIs y formato condicional reactable + reactablefmtr
Tabla con millones de filas (server-side processing) DT
Edición masiva de celdas DT con editable o rhandsontable
Exportar a CSV/Excel/PDF desde el navegador DT (más extensiones disponibles)
App nueva, sin migraciones reactable
Heredar código existente Mantén lo que esté

Trampas habituales

  • Pasar un dataset enorme a la tabla sin server-side processing. Con > 10 000 filas, el navegador se ralentiza. DT soporta server-side processing nativo. reactable lo soporta también desde 0.4. Léelo antes de pasar millones de filas.
  • input$tabla_rows_selected cuando no hay selección. Devuelve NULL, si no lo manejas con req(), el código que depende de él falla. Patrón: req(input$tabla_rows_selected) al inicio del observer.
  • Cambiar la tabla completa en cada render. Si renderDT() o renderReactable() regeneran toda la tabla con cada cambio de un filtro, pierdes la posición de scroll y la selección del usuario. Para actualizaciones incrementales, mira DT::replaceData() o reactable::updateReactable().
  • Confundir el id de la tabla con el id del output. En input$tabla_rows_selected, tabla es el outputId que pasaste a DTOutput("tabla"). Si lo cambias, todos los input$tabla_* cambian su prefijo.

En la siguiente entrega

Has aprendido tablas. La otra pieza imprescindible son los gráficos interactivos. plotly convierte cualquier ggplot en interactivo y permite capturar clicks, hovers y selecciones desde el server. Lo siguiente.