docs/enterprise/ag_grid/master-detail.md
Master-detail lets rows expand to show detailed information in a nested grid. Each master row carries its detail rows as a nested list, and an expandable column reveals them.
Three pieces are required:
master_detail=True on the grid."cell_renderer": "agGroupCellRenderer", which renders the expand/collapse arrows.detail_cell_renderer_params describing the detail grid's columns and how to extract the detail rows from the master row.import reflex as rx
import reflex_enterprise as rxe
class MasterDetailState(rx.State):
master_data: list[dict] = [
{
"id": 1,
"name": "Product A",
"category": "Electronics",
"price": 299.99,
"counts": [ # Detail rows for this master row
{"count": 10, "value": "Stock Level"},
{"count": 5, "value": "Orders Today"},
{"count": 25, "value": "Total Sales"},
],
},
{
"id": 2,
"name": "Product B",
"category": "Clothing",
"price": 49.99,
"counts": [
{"count": 50, "value": "Stock Level"},
{"count": 12, "value": "Orders Today"},
{"count": 78, "value": "Total Sales"},
],
},
]
column_defs = [
{
"field": "id",
"header_name": "ID",
"width": 80,
"cell_renderer": "agGroupCellRenderer", # Required for expand/collapse
},
{"field": "name", "header_name": "Product Name", "width": 150},
{"field": "category", "header_name": "Category", "width": 120},
{
"field": "price",
"header_name": "Price",
"width": 100,
"value_formatter": "params.value ? '$' + params.value.toFixed(2) : ''",
},
]
detail_cell_renderer_params = {
"detail_grid_options": {
"column_defs": [
{"field": "count", "header_name": "Count"},
{"field": "value", "header_name": "Description"},
]
},
"get_detail_row_data": lambda params: rx.vars.function.FunctionStringVar(
"params.successCallback"
).call(params.data.counts),
}
def master_detail_grid():
return rxe.ag_grid(
id="master_detail_grid",
row_data=MasterDetailState.master_data,
column_defs=column_defs,
master_detail=True,
detail_cell_renderer_params=detail_cell_renderer_params,
width="100%",
height="500px",
)
get_detail_row_data follows AG Grid's asynchronous convention: the grid passes a params object containing the master row's data and a successCallback to invoke with the detail rows. The lambda above calls params.successCallback with the nested counts list of the expanded row.
The detail grid is a full AG Grid instance with its own column_defs, independent from the master grid's columns.
row_data and column_defs are plain serializable data, so they can live in state and change at runtime:
class MasterDetailState(rx.State):
master_data: list[dict] = [] # fetch/replace at runtime
column_defs: list[dict] = []
detail_cell_renderer_params is different because it holds a callback (get_detail_row_data). A callable cannot be stored in a state var: Reflex serializes state to the client as JSON, and a Python lambda (or FunctionStringVar) has no JSON representation, so syncing that state raises a serialization error at runtime.
Keep the renderer params as a module-level object and pass it to the grid directly — it is compiled into the app once and does not need to change per request:
DETAIL_PARAMS = {
"detail_grid_options": {"column_defs": [{"field": "count"}]},
"get_detail_row_data": lambda params: rx.vars.function.FunctionStringVar(
"params.successCallback"
).call(params.data.counts),
}
Reserve state vars for the serializable pieces (row data, column defs) and leave the callback-bearing renderer params at module level.