Server-Side Data
Why server-side data?
By default, DataGrid() sends all rows
to the browser at once. Pagination, sorting, and filtering are handled
entirely in JavaScript on the client side. This works well for small
datasets, but becomes a problem with large ones:
- Performance: sending thousands of rows as JSON slows down the initial page load.
- Memory: the browser must hold the entire dataset in memory.
- Security: all data is accessible in the browser, even rows not currently displayed.
With server-side data, only the rows for the current page are sent to the browser. Pagination, sorting, and filtering are handled in R on the server. This keeps the app fast regardless of dataset size.
Client-side vs server-side
DataGrid() (client-side) |
DataGridServer() (server-side) |
|
|---|---|---|
| Data sent to browser | All rows at once | Current page only |
| Pagination | JavaScript | R server |
| Sorting | JavaScript | R server |
| Filtering | JavaScript | R server |
| Requires Shiny | No | Yes |
How it works
DataGridServer() is a Shiny-only component. It uses a
custom React component that:
- Manages pagination, sort, and filter state internally in the browser.
- Sends the current state to R via
input$<inputId>whenever the user changes page, sorts a column, or applies a filter. - R processes the data and sends back only the matching rows for the current page.
Automatic vs manual mode
DataGridServer() supports two modes:
-
Automatic mode (default): pass the full dataset via
rows. Pagination, sorting, and filtering are handled internally byprocessGridParams(). This is the simplest option and works well when the data fits in R memory. -
Manual mode: pass pre-sliced
rowstogether with an explicitrowCount. Use this when you need full control — for example, to query a database or apply custom filtering logic.
Basic usage
A minimal server-side DataGrid requires only
reactOutput() in the UI and DataGridServer()
in the server. Pass the full dataset via rows — exactly
like DataGrid() — and pagination, sorting, and filtering
are handled automatically:
library(shiny)
# 100'000 rows
all_data <- data.frame(
id = 1:100000,
label = paste("Row", 1:100000),
value = 1:100000,
group = rep(c("A", "B", "C", "D"), each = 25000)
)
# Very slow with big data
# DataGrid(
# rows = all_data,
# initialState = list(
# pagination = list(
# paginationModel = list(pageSize = 5)
# )
# )
# )
ui <- div(
reactOutput("grid")
)
server <- function(input, output, session) {
output$grid <- renderReact({
DataGridServer(
inputId = "grid_params",
rows = all_data,
initialPageSize = 5L,
pageSizeOptions = c(5L, 10L, 25L)
)
})
}
shinyApp(ui, server)Note:
initialPageSizemust be included inpageSizeOptions— an error is raised if not. It also sets the page size for the very first render, before the grid has sent any state to R.
Key functions
DataGridServer()
Renders a DataGrid with server-side pagination, sorting, and filtering.
| Parameter | Description |
|---|---|
inputId |
Shiny input ID. Grid state is available as
input$<inputId>. |
rows |
Full data frame (automatic mode) or pre-sliced page
with explicit rowCount (manual mode). |
columns |
Column definitions (auto-generated from rows if
NULL). |
rowCount |
When provided, rows is treated as already paginated and
rowCount is the total. When NULL (default), pagination is
handled automatically from the full rows dataset. |
initialPageSize |
Initial rows per page. Must be included in
pageSizeOptions. Defaults to MUI’s default (100) if
NULL. |
pageSizeOptions |
Available page size choices. Defaults to MUI’s default
c(25, 50, 100) if NULL. |
filterDebounce |
Milliseconds to wait after typing before sending filter state to R. Defaults to 300 ms if NULL. |
loading |
Show a loading indicator (default FALSE). |
... |
Additional props passed to the MUI DataGrid. |
processGridParams()
Applies pagination, sorting, and filtering to a data frame based on
input$<inputId>. Use this directly when you need
custom server-side logic (e.g. database queries) that
DataGridServer() cannot handle automatically.
| Parameter | Description |
|---|---|
data |
The full data frame. |
params |
input$<inputId> from DataGridServer
(NULL on first render). |
pageSize |
Page size used on first render before the grid sends state. Should
match initialPageSize. Defaults to 100. |
Returns a list with:
-
rows: the data frame for the current page -
rowCount: total number of matching rows (after filtering, before pagination)
Supported filter operators
processGridParams() handles the following MUI filter
operators:
| Column type | Operators |
|---|---|
| String |
contains, equals, startsWith,
endsWith, not / !=,
isEmpty, isNotEmpty, isAnyOf
|
| Number |
=, !=, >,
>=, <, <=,
isEmpty, isNotEmpty, isAnyOf
|
| Date |
is, not, after,
onOrAfter, before, onOrBefore,
isEmpty, isNotEmpty
|
Unrecognized operators are ignored (all rows pass). If you need operators not listed here, use manual mode and handle filtering yourself.
Note: String operators (
contains,equals,startsWith,endsWith,not) are case-insensitive. Theisoperator is case-sensitive, matching MUI’s default behavior.
Custom columns
You can define custom columns the same way as with
DataGrid():
library(dplyr)
server <- function(input, output, session) {
output$grid <- renderReact({
result <- processGridParams(dplyr::starwars, input$grid_params, pageSize = 5L)
DataGridServer(
inputId = "grid_params",
rows = result$rows,
rowCount = result$rowCount,
initialPageSize = 5L,
columns = list(
list(field = "name", headerName = "Name", flex = 1),
list(field = "height", headerName = "Height (cm)", type = "number", width = 120),
list(field = "mass", headerName = "Mass (kg)", type = "number", width = 120),
list(field = "gender", headerName = "Gender", width = 120)
)
)
})
}Loading indicator
For manual mode, the grid can show a loading overlay while data is
being fetched. Use a reactiveVal to track the loading
state:
server <- function(input, output, session) {
loading <- reactiveVal(FALSE)
grid_data <- reactive({
loading(TRUE)
on.exit(loading(FALSE))
result <- processGridParams(all_data, input$grid_params, pageSize = 50L)
result
})
output$grid <- renderReact({
result <- grid_data()
DataGridServer(
inputId = "grid_params",
rows = result$rows,
rowCount = result$rowCount,
loading = loading(),
initialPageSize = 50L,
pageSizeOptions = c(50L, 100L)
)
})
}This is most useful when the data source is slow (e.g. a database query or API call).
Reading grid state
The grid state is available in input$<inputId> as
a list with three elements:
-
pagination_model: list withpage(0-indexed) andpageSize -
sort_model: list of sort items, each withfieldandsort(“asc” or “desc”) -
filter_model: list withitems, each containingfield,operator, andvalue; andlogicOperator(“and” or “or”)
You can use this to display diagnostics or trigger other reactive logic:
server <- function(input, output, session) {
output$grid <- renderReact({
result <- processGridParams(all_data, input$grid_params)
DataGridServer("grid_params", rows = result$rows, rowCount = result$rowCount)
})
observe({
params <- input$grid_params
if (!is.null(params)) {
cat("Page:", params$pagination_model$page, "\n")
cat("Page size:", params$pagination_model$pageSize, "\n")
}
})
}Custom server-side logic
processGridParams() covers common sorting and filtering
operators. If you need custom logic (e.g. querying a database), you can
skip it and handle input$<inputId> directly:
server <- function(input, output, session) {
output$grid <- renderReact({
params <- input$grid_params
page <- if (!is.null(params)) params$pagination_model$page else 0
page_size <- if (!is.null(params)) params$pagination_model$pageSize else 25
# Custom database query
result <- DBI::dbGetQuery(con, sprintf(
"SELECT * FROM my_table ORDER BY id LIMIT %d OFFSET %d",
page_size, page * page_size
))
total <- DBI::dbGetQuery(con, "SELECT COUNT(*) FROM my_table")[[1]]
DataGridServer(
inputId = "grid_params",
rows = result,
rowCount = total,
initialPageSize = 25L,
pageSizeOptions = c(25L, 50L, 100L)
)
})
}