diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3ebed3f..245013a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
A summary of notable changes per release. For the full commit history see the [repository on GitHub](https://github.com/jbetancur/react-data-table-component/commits/master).
+## 8.8.1
+
+### Bug fixes
+
+- Controlled `selectedRows` is no longer dropped when a row is toggled in-table. Selection deltas are computed against the `selectedRows` prop instead of stale internal state, so a selection set from outside survives the next checkbox click and `onSelectedRowsChange` emits the correct set. → [Row selection](/docs/selection) ([#1374](https://github.com/jbetancur/react-data-table-component/issues/1374))
+
+---
+
## 8.8.0
### New features
diff --git a/apps/docs/src/components/demos/ControlledSelectionDemo.tsx b/apps/docs/src/components/demos/ControlledSelectionDemo.tsx
new file mode 100644
index 00000000..86337f06
--- /dev/null
+++ b/apps/docs/src/components/demos/ControlledSelectionDemo.tsx
@@ -0,0 +1,114 @@
+import React, { useState } from 'react';
+import DataTable from '../ThemedDataTable';
+import { type TableColumn } from 'react-data-table-component';
+
+interface Row {
+ id: number;
+ name: string;
+ role: string;
+ status: 'Active' | 'On Leave';
+}
+
+const data: Row[] = [
+ { id: 1, name: 'Aria Chen', role: 'Engineering Lead', status: 'Active' },
+ { id: 2, name: 'Marcus Webb', role: 'Product Manager', status: 'Active' },
+ { id: 3, name: 'Priya Kapoor', role: 'Senior Designer', status: 'On Leave' },
+ { id: 4, name: 'Jordan Ellis', role: 'Data Scientist', status: 'Active' },
+ { id: 5, name: 'Sam Rivera', role: 'DevOps Engineer', status: 'On Leave' },
+];
+
+const columns: TableColumn[] = [
+ { name: 'Name', selector: r => r.name, sortable: true },
+ { name: 'Role', selector: r => r.role },
+ { name: 'Status', selector: r => r.status },
+];
+
+export default function ControlledSelectionDemo() {
+ const [selected, setSelected] = useState([]);
+ const [events, setEvents] = useState([]);
+
+ const setExternally = (rows: Row[], label: string) => {
+ setSelected(rows);
+ setEvents(prev => [`external: ${label}`, ...prev].slice(0, 6));
+ };
+
+ const selectWhere = (label: string, predicate: (row: Row) => boolean) =>
+ setExternally(data.filter(predicate), label);
+
+ const invert = () =>
+ setExternally(
+ data.filter(row => !selected.some(s => s.id === row.id)),
+ 'invert selection',
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ The buttons write straight to the selectedRows state. Checking a box in the table
+ calls onSelectedRowsChange, which writes back to that same state. Both paths stay in sync.
+
- New in 8.1.0 — useTableExport ships as part of the library's
- headless surface alongside useTableState, useTableData, and friends.
+ useTableExport ships as part of the library's headless surface
+ alongside useTableState, useTableData, and friends.
-
- New in 8.1.0 — number, date, checkbox, and custom editors plus the
- validate hook. The text and select editors are unchanged.
-
-
@@ -15,11 +18,6 @@ import SelectionDemo from '../../components/demos/SelectionDemo.tsx';
selection from outside, or use the imperative ref API to clear it.
-
- New in 8.1.0 — Shift-click range selection (on by default) and a
- controlled selectedRows prop.
-
-
⚠ keyField is required for reliable selection
@@ -104,14 +102,6 @@ export default function App() {
-
Custom selection toolbar
-
- v8 removed the built-in contextMessage and contextActions props.
- Use onSelectedRowsChange to drive your own toolbar rendered outside the table —
- you get full control over layout, copy, and actions. See the
- bulk-action toolbar recipe for a complete example.
-
-
Range selection (Shift-click)
Click one row's checkbox, then Shift-click another to toggle every row in between to match
@@ -128,6 +118,27 @@ export default function App() {
// Disable range selection
`} />
+
Uncontrolled selection (default)
+
+ Omit selectedRows and the table owns selection itself. Read it via
+ onSelectedRowsChange, and clear it imperatively through the ref. This is the
+ demo at the top of this page, and it is what you want unless something outside the table
+ needs to set the selection.
+
+ (null);
+
+ setCount(selectedRows.length)}
+/>
+
+// clear it from outside
+ref.current?.clearSelectedRows();`} />
+
Controlled selection
Pass selectedRows to drive selection from your own state. The table will
@@ -135,25 +146,53 @@ export default function App() {
toggles. Match rows by keyField, so the entries you pass in must include the
key field.
-
+ Reach for this when something other than the table needs to set the selection: a "select all
+ matching" action, selection restored from URL or saved state, or a second view that has to
+ stay in sync. The demo below drives it from filter predicates.
+
+ ([]);
+ const selectWhere = (predicate) => setSelected(data.filter(predicate));
+
return (
- setSelected(selectedRows)}
- />
+ <>
+
+
+
+
+
+ setSelected(selectedRows)}
+ />
+ >
);
-}`} />
+}`}
+ >
+
+
+
Controlled selection is useful when selection lives in URL state, a Redux/Zustand store,
or needs to survive remounts. Omit selectedRows to fall back to the table's
internal state.
+
+ Write the emitted rows back to the same state you pass to selectedRows. The
+ table computes each toggle against the prop, so a value you set from outside is preserved
+ when the user checks another box.
+
Single select
Pass selectableRowsSingle to restrict to one row at a time. Shift-click range selection is automatically disabled in single-select mode.
@@ -170,6 +209,38 @@ export default function App() {
Pre-select rows
row.status === 'Active'} />;`} />
+
Select only visible rows
+
+ When pagination is enabled, selectableRowsVisibleOnly makes the "select all"
+ checkbox operate only on the current page rather than the full dataset.
+
+
+ It also changes what happens when the page changes: the selection is cleared. That keeps
+ "select all" honest — it always refers to the rows in front of you — but it means a
+ selection cannot be accumulated across pages. Leave the prop off if you need that.
+
+ ;`} />
+
+
+
+// Page-scoped: select all takes the current page, paging clears the selection
+`}
+ >
+
+
+
onSelectedRowsChange
The callback receives {`{ allSelected, selectedCount, selectedRows }`}.
+ v8 removed the built-in contextMessage and contextActions props.
+ Use onSelectedRowsChange to drive your own toolbar rendered outside the table —
+ you get full control over layout, copy, and actions. See the
+ bulk-action toolbar recipe for a complete example.
+
+
Highlight selected rows
Add selectableRowsHighlight to apply the theme's selected-row background
@@ -201,13 +280,6 @@ tableRef.current?.clearSelectedRows();`} />
;`} />
-
Select only visible rows
-
- When pagination is enabled, selectableRowsVisibleOnly makes the "select all"
- checkbox operate only on the current page rather than the full dataset.
-
- ;`} />
-
Custom checkbox component
Replace the built-in checkbox with your own component via selectableRowsComponent.
@@ -216,6 +288,49 @@ tableRef.current?.clearSelectedRows();`} />
;`} />
+
+
+ Your component receives checked, disabled, name,
+ onClick, and a ref. Forward that ref to a real
+ <input type="checkbox"> — DataTable sets .indeterminate on the
+ node directly for the partial-selection state on the header checkbox. Keep the input in the
+ DOM and style a sibling element off it, rather than replacing it, so keyboard and screen
+ reader behaviour still work.
+