Tablas interactivas con DT y reactable
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 conrow,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.
DTsoporta server-side processing nativo.reactablelo soporta también desde 0.4. Léelo antes de pasar millones de filas. input$tabla_rows_selectedcuando no hay selección. DevuelveNULL, si no lo manejas conreq(), 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()orenderReactable()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, miraDT::replaceData()oreactable::updateReactable(). - Confundir el
idde la tabla con el id del output. Eninput$tabla_rows_selected,tablaes eloutputIdque pasaste aDTOutput("tabla"). Si lo cambias, todos losinput$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.