Skip to content

Commit c1acf48

Browse files
committed
9C: Row selection support for DuckDB and server backends
- Add _reactable_rowid (0-based) column to data before serialization for stable row identification across pages - Extract _reactable_rowid into __state = { id, index } for flat rows in DuckDB, data frame, and V8 backends - Add __state with group IDs for grouped rows in DuckDB backend - Extend getRowId in Reactable.js to check useDuckDB in addition to useServerData - Remove selectedRowIds from V8 server useEffect deps to prevent unnecessary re-fetches on selection click - Remove row selection warning for DuckDB backend - Delete backendDt and remove data.table from DESCRIPTION - Fix S3 method param order (searchMethod) to match generic - Add .claude to .Rbuildignore
1 parent 14f847a commit c1acf48

17 files changed

+311
-177
lines changed

.Rbuildignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
^srcjs$
2121
^vignettes$
2222
^webpack\.config.*\.js$
23+
^\.claude$

DESCRIPTION

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ Suggests:
3939
arrow,
4040
covr,
4141
crosstalk,
42-
data.table,
4342
DBI,
4443
duckdb,
4544
dplyr,

NAMESPACE

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
S3method(as.tags,reactable)
44
S3method(reactableServerData,default)
55
S3method(reactableServerData,reactable_backendDf)
6-
S3method(reactableServerData,reactable_backendDt)
76
S3method(reactableServerData,reactable_backendDuckdb)
87
S3method(reactableServerData,reactable_backendV8)
98
S3method(reactableServerInit,default)
10-
S3method(reactableServerInit,reactable_backendDt)
119
S3method(reactableServerInit,reactable_backendDuckdb)
1210
S3method(reactableServerInit,reactable_backendV8)
1311
export(JS)
1412
export(backendDf)
15-
export(backendDt)
1613
export(backendDuckDB)
1714
export(backendV8)
1815
export(colDef)

R/backend-df.R

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,19 @@ reactableServerData.reactable_backendDf <- function(
2525
sortBy = NULL,
2626
filters = NULL,
2727
searchValue = NULL,
28+
searchMethod = NULL,
2829
groupBy = NULL,
2930
pagination = NULL,
3031
paginateSubRows = NULL,
3132
# Unused/unimplemented props
3233
selectedRowIds = NULL,
3334
expanded = NULL,
34-
searchMethod = NULL,
3535
...
3636
) {
3737

38+
# Add 0-based row IDs for stable row identification across pages
39+
data[["_reactable_rowid"]] <- seq_len(nrow(data)) - 1L
40+
3841
# Column filters - simple text match for now
3942
if (length(filters) > 0) {
4043
data <- dfFilter(data, filters)
@@ -191,6 +194,15 @@ dataFrame <- function(...) {
191194
}
192195

193196
dfPaginate <- function(df, pageIndex = 0, pageSize = NULL) {
197+
# Extract _reactable_rowid into __state for stable row identification (flat rows only)
198+
if ("_reactable_rowid" %in% colnames(df) && !"__state" %in% colnames(df)) {
199+
rowids <- df[["_reactable_rowid"]]
200+
df[["__state"]] <- lapply(rowids, function(rid) {
201+
list(id = as.character(rid), index = as.integer(rid))
202+
})
203+
df[["_reactable_rowid"]] <- NULL
204+
}
205+
194206
if (is.null(pageSize)) {
195207
return(resolvedData(df, rowCount = nrow(df)))
196208
}

R/backend-dt.R

Lines changed: 0 additions & 73 deletions
This file was deleted.

R/backend-duckdb.R

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ reactableServerInit.reactable_backendDuckdb <- function(x, data = NULL, columns
104104
}
105105

106106
con <- DBI::dbConnect(duckdb::duckdb())
107+
# Add 0-based row IDs for stable row identification across pages
108+
data[["_reactable_rowid"]] <- seq_len(nrow(data)) - 1L
107109
# Zero-copy registration - DuckDB reads directly from R data frame memory
108110
duckdb::duckdb_register(con, "reactable_data", data)
109111
x$private$con <- con
@@ -120,13 +122,13 @@ reactableServerData.reactable_backendDuckdb <- function(
120122
sortBy = NULL,
121123
filters = NULL,
122124
searchValue = NULL,
125+
searchMethod = NULL,
123126
groupBy = NULL,
124127
pagination = NULL,
125128
paginateSubRows = NULL,
126129
# Unused/unimplemented props
127130
selectedRowIds = NULL,
128131
expanded = NULL,
129-
searchMethod = NULL,
130132
...
131133
) {
132134
con <- x$private$con
@@ -152,6 +154,15 @@ reactableServerData.reactable_backendDuckdb <- function(
152154
countResult <- DBI::dbGetQuery(con, query$countSql, params = query$params)
153155
rowCount <- countResult$n
154156

157+
# Extract _reactable_rowid into __state for stable row identification
158+
if ("_reactable_rowid" %in% colnames(page)) {
159+
rowids <- page[["_reactable_rowid"]]
160+
page[["__state"]] <- lapply(rowids, function(rid) {
161+
list(id = as.character(rid), index = as.integer(rid))
162+
})
163+
page[["_reactable_rowid"]] <- NULL
164+
}
165+
155166
resolvedData(page, rowCount = rowCount)
156167
}
157168

@@ -241,9 +252,18 @@ duckdbGroupedQuery <- function(con, columns, filters, searchValue, sortBy,
241252
subRowsList[[i]] <- subResult
242253
}
243254
} else {
244-
# Leaf level fetch individual rows
255+
# Leaf level -- fetch individual rows
245256
subQuery <- buildDuckdbSubRowSql("reactable_data", baseWhere, childFilters, sortBy)
246-
subRowsList[[i]] <- DBI::dbGetQuery(con, subQuery$sql, params = subQuery$params)
257+
subRows <- DBI::dbGetQuery(con, subQuery$sql, params = subQuery$params)
258+
# Extract _reactable_rowid into __state for stable row identification
259+
if ("_reactable_rowid" %in% colnames(subRows)) {
260+
rowids <- subRows[["_reactable_rowid"]]
261+
subRows[["__state"]] <- lapply(rowids, function(rid) {
262+
list(id = as.character(rid), index = as.integer(rid))
263+
})
264+
subRows[["_reactable_rowid"]] <- NULL
265+
}
266+
subRowsList[[i]] <- subRows
247267
}
248268

249269
# Post-compute aggregates from sub-row data (e.g. frequency)
@@ -257,6 +277,11 @@ duckdbGroupedQuery <- function(con, columns, filters, searchValue, sortBy,
257277

258278
groupData[[".subRows"]] <- subRowsList
259279

280+
# Add __state with group ID for group header rows
281+
groupData[["__state"]] <- lapply(groupValues, function(val) {
282+
list(id = paste0(groupCol, ":", val), grouped = TRUE)
283+
})
284+
260285
if (depth == 0) {
261286
resolvedData(groupData, rowCount = rowCount)
262287
} else {

R/reactable.R

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,14 @@
130130
#' Static rendering is **experimental**, and is not supported for tables
131131
#' rendered via [reactableOutput()] in Shiny.
132132
#' @param server **Deprecated.** Use `backend` instead. Accepts `TRUE` (equivalent
133-
#' to `backendV8()`), or a backend string like `"df"`, `"dt"`, `"duckdb"`.
133+
#' to `backendV8()`), or a backend string like `"df"`, `"duckdb"`.
134134
#' @param backend A backend object for data processing. Options:
135135
#' \describe{
136136
#' \item{[backendDuckDB()]}{DuckDB-powered sorting, filtering, and pagination.
137137
#' In static documents (R Markdown, Quarto), DuckDB-WASM runs in the browser.
138138
#' In Shiny, the DuckDB R package runs on the server.}
139139
#' \item{[backendV8()]}{V8 server-side processing (Shiny only).}
140140
#' \item{[backendDf()]}{Pure R data frame operations (Shiny only).}
141-
#' \item{[backendDt()]}{data.table operations (Shiny only). Requires the data.table package.}
142141
#' }
143142
#' @param selectionId **Deprecated**. Use [getReactableState()] to get the selected rows
144143
#' in Shiny.
@@ -272,7 +271,7 @@ reactable <- function(
272271

273272
if (!is.null(backend)) {
274273
if (!is.object(backend)) {
275-
stop("`backend` must be a backend object created by `backendDuckDB()`, `backendV8()`, `backendDf()`, or `backendDt()`")
274+
stop("`backend` must be a backend object created by `backendDuckDB()`, `backendV8()`, or `backendDf()`")
276275
}
277276
if (!isFALSE(server)) {
278277
stop("`backend` and `server` cannot both be specified. Use `backend` instead of `server`.")
@@ -287,7 +286,6 @@ reactable <- function(
287286
"The `server` argument is deprecated. Use `backend` instead:\n",
288287
' backend = backendV8() # instead of server = TRUE\n',
289288
' backend = backendDf() # instead of server = "df"\n',
290-
' backend = backendDt() # instead of server = "dt"\n',
291289
' backend = backendDuckDB() # instead of server = "duckdb"',
292290
call. = FALSE
293291
)
@@ -297,8 +295,6 @@ reactable <- function(
297295
backend <- backendV8()
298296
} else if (identical(server, "df")) {
299297
backend <- backendDf()
300-
} else if (identical(server, "dt")) {
301-
backend <- backendDt()
302298
} else if (identical(server, "duckdb")) {
303299
backend <- backendDuckDB(mode = "server")
304300
} else if (is.object(server)) {
@@ -867,10 +863,10 @@ reactable <- function(
867863
"Use a JS() function instead: reactable(rowStyle = JS(\"function(rowInfo) { ... }\")).",
868864
call. = FALSE)
869865
}
870-
if (!is.null(selection)) {
871-
warning('Row `selection` is not supported with `backendDuckDB()` and will not work correctly.',
872-
call. = FALSE)
873-
}
866+
# Add 0-based row IDs for stable row identification across pages. DuckDB queries
867+
# return different rows per page, so page-relative indices (0, 1, 2...) would collide.
868+
# The JS side extracts this column and uses it for __state.id and __state.index.
869+
data[["_reactable_rowid"]] <- seq_len(nrow(data)) - 1L
874870

875871
arrowData <- serializeArrowIPC(data)
876872
totalRowCount <- nrow(data)
@@ -908,6 +904,7 @@ reactable <- function(
908904
firstPageData <- firstPageData[do.call(order, orderArgs), , drop = FALSE]
909905
}
910906
firstPageData <- utils::head(firstPageData, defaultPageSize)
907+
firstPageData[["_reactable_rowid"]] <- NULL
911908
data <- toJSON(firstPageData)
912909
serverRowCount <- totalRowCount
913910
serverMaxRowCount <- totalRowCount

R/shiny.R

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,6 @@ getServerBackend <- function(backend = NULL) {
499499
backends <- list(
500500
v8 = backendV8,
501501
df = backendDf,
502-
dt = backendDt,
503502
duckdb = backendDuckdbServer
504503
)
505504

man/backendDt.Rd

Lines changed: 0 additions & 21 deletions
This file was deleted.

man/reactable.Rd

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)