Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions frontend/src/plugins/impl/DataTablePlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
.nullable()
.default(null),
showDownload: z.boolean().default(false),
defaultSort: z.string().optional(),
showFilters: z.boolean().default(false),
showColumnSummaries: z
.union([z.boolean(), z.enum(["stats", "chart"])])
Expand Down Expand Up @@ -500,8 +501,15 @@ export const LoadingDataTableComponent = memo(

const search = props.search;
const setValue = props.setValue;
const initialSorting = useMemo<SortingState>(
() =>
props.defaultSort
? [{ id: props.defaultSort, desc: false }]
: Arrays.EMPTY,
[props.defaultSort],
);
// Sorting/searching state
const [sorting, setSorting] = useState<SortingState>([]);
const [sorting, setSorting] = useState<SortingState>(initialSorting);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initializing sorting state from defaultSort makes sorting.length !== 0 on mount, which forces canShowInitialPage to be false and causes the first render to await search() instead of displaying the precomputed initial page. This can add avoidable latency/loader flashes even though the backend already sends sorted initial data. Consider updating the initial-page fast-path to treat the default sort as part of the initial state (or keep sorting empty while still showing the UI sort indicator).

Suggested change
const [sorting, setSorting] = useState<SortingState>(initialSorting);
const [sorting, setSorting] = useState<SortingState>(Arrays.EMPTY);

Copilot uses AI. Check for mistakes.
const [paginationState, setPaginationState] =
React.useState<PaginationState>({
pageSize: props.pageSize,
Expand Down Expand Up @@ -563,7 +571,11 @@ export const LoadingDataTableComponent = memo(
searchQuery === "" &&
paginationState.pageIndex === 0 &&
filters.length === 0 &&
sorting.length === 0 &&
(sorting.length === 0 ||
(sorting.length === 1 &&
Boolean(props.defaultSort) &&
sorting[0]?.id === props.defaultSort &&
sorting[0]?.desc === false)) &&
!props.lazy &&
!pageSizeChanged;

Expand Down
19 changes: 18 additions & 1 deletion marimo/_plugins/ui/_impl/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ def hover_cell(rowId, columnName, value):
Defaults to True.
show_download (bool, optional): Whether to show the download button.
Defaults to True for dataframes, False otherwise.
default_sort (str, optional): Column name to sort by on initial render.
Sorting is ascending by default.
format_mapping (Dict[str, Union[str, Callable[..., Any]]], optional): A mapping from
column names to formatting strings or functions.
freeze_columns_left (Sequence[str], optional): List of column names to freeze on the left.
Expand Down Expand Up @@ -469,6 +471,7 @@ def __init__(
show_download: bool = True,
max_columns: MaxColumnsType = MAX_COLUMNS_NOT_PROVIDED,
*,
default_sort: Optional[str] = None,
label: str = "",
on_change: Optional[
Callable[
Expand Down Expand Up @@ -673,14 +676,27 @@ def __init__(
field_types: Optional[FieldTypes] = None
num_columns = 0

if default_sort is not None:
existing_columns = set(self._manager.get_column_names())
if default_sort not in existing_columns:
raise ValueError(
f"default_sort column '{default_sort}' not found in table columns"
)

if not _internal_lazy:
default_sort_args = (
[SortArgs(by=default_sort, descending=False)]
if default_sort is not None
else None
)

# Search first page
search_result = self._search(
SearchTableArgs(
page_size=page_size,
page_number=0,
query=None,
sort=None,
sort=default_sort_args,
filters=None,
)
)
Expand Down Expand Up @@ -722,6 +738,7 @@ def __init__(
"show-filters": self._manager.supports_filters(),
"show-download": show_download
and self._manager.supports_download(),
"default-sort": default_sort,
"show-column-summaries": show_column_summaries,
"show-data-types": show_data_types,
"show-page-size-selector": show_page_size_selector,
Expand Down
28 changes: 28 additions & 0 deletions tests/_plugins/ui/_impl/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -2231,6 +2231,34 @@ def test_max_columns_not_provided_with_sort():
assert len(result_data[0].keys()) == 100


def test_default_sort_applies_on_initial_render() -> None:
data = {"name": ["charlie", "alice", "bob"], "value": [3, 1, 2]}
table = ui.table(data, selection=None, default_sort="name")

result_data = json.loads(table._component_args["data"])
assert [row["name"] for row in result_data] == ["alice", "bob", "charlie"]
assert table._component_args["default-sort"] == "name"


def test_default_sort_invalid_column_raises() -> None:
data = {"name": ["charlie", "alice", "bob"], "value": [3, 1, 2]}

with pytest.raises(ValueError, match="default_sort column 'missing'"):
ui.table(data, selection=None, default_sort="missing")


def test_default_sort_invalid_column_raises_in_lazy_mode() -> None:
data = {"name": ["charlie", "alice", "bob"], "value": [3, 1, 2]}

with pytest.raises(ValueError, match="default_sort column 'missing'"):
ui.table(
data,
selection=None,
default_sort="missing",
_internal_lazy=True,
)


@pytest.mark.parametrize(
"df",
create_dataframes(
Expand Down
Loading