useSelection is a hook that manages multi-select functionality via checkboxes in list pages. It provides features like Shift+click range selection and select all/deselect all.
Core Features
Multi-select Manage individual/all selection Set-based efficient state
Range Selection Shift+click support Select consecutive items at once
Auto Validation Auto cleanup on page change Keep only existing items
Type Safety Generic type support Use any type as key
Basic Usage
Import and Initialization
import { useSelection } from "@sonamu-kit/react-components" ;
export function UserListPage () {
const { data } = UserService . useUsers ( "A" , listParams );
const allIds = data ?. rows . map ( row => row . id ) ?? [];
// Initialize useSelection
const {
selectedKeys , // Array of selected keys
getSelected , // Check if specific key is selected
toggle , // Toggle individual item selection
selectAll , // Select all
deselectAll , // Deselect all
isAllSelected , // Check if all selected
handleCheckboxClick , // Shift+click range selection
} = useSelection ( allIds , []); // allIds: all keys, []: initial selection
// ...
}
Parameter explanation :
allIds: Array of all keys on current page (e.g., [1, 2, 3, 4, 5])
defaultSelectedKeys: Initial selection state (optional, default: [])
Individual Item Selection
import { Checkbox } from "@sonamu-kit/react-components/components" ;
const { getSelected , toggle } = useSelection ( allIds , []);
< Checkbox
checked = { getSelected (row.id)}
onValueChange = {() => toggle (row.id)}
/>
How it works :
getSelected(key): Returns true if key is selected, false otherwise
toggle(key): Toggle selection state
Select All/Deselect All
const { isAllSelected , selectAll , deselectAll } = useSelection ( allIds , []);
< Checkbox
checked = { isAllSelected }
onValueChange = {(checked) => {
if ( checked ) {
selectAll ();
} else {
deselectAll ();
}
}}
/>
How it works :
isAllSelected: Whether all items on current page are selected
selectAll(): Select all items on current page
deselectAll(): Deselect all selections
Shift+Click Range Selection
const { handleCheckboxClick } = useSelection ( allIds , []);
{ rows . map (( row , index ) => (
< tr key = {row. id } >
< td onClick = {(e) => handleCheckboxClick ( e , index )} >
< Checkbox
checked = { getSelected (row.id)}
onValueChange = {() => toggle (row.id)}
/>
</ td >
{ /* ... */ }
</ tr >
))}
How it works :
Normal click: Toggle individual item selection
Shift+click: Select all items from last selection to current position
How Shift+click works handleCheckboxClick remembers the last click index and selects all items in between when Shift+clicking.Items: [A, B, C, D, E]
1. Click A → A selected
2. Shift+click D → A, B, C, D all selected
Real-world Examples
Complete Table Implementation
import { useSelection } from "@sonamu-kit/react-components" ;
import { Checkbox , Button , Table } from "@sonamu-kit/react-components/components" ;
import { UserService } from "@/services/services.generated" ;
export function UserListPage () {
const { data } = UserService . useUsers ( "A" , listParams );
const { rows } = data ?? {};
const allIds = rows ?. map ( row => row . id ) ?? [];
const {
selectedKeys ,
getSelected ,
toggle ,
selectAll ,
deselectAll ,
isAllSelected ,
handleCheckboxClick ,
} = useSelection ( allIds , []);
// Bulk delete
const handleBulkDelete = async () => {
if ( selectedKeys . length === 0 ) {
alert ( "Select items to delete" );
return ;
}
if ( confirm ( `Delete ${ selectedKeys . length } items?` )) {
await UserService . del ( selectedKeys );
deselectAll (); // Deselect after deletion
}
};
return (
< div >
{ /* Bulk action buttons */ }
{ selectedKeys . length > 0 && (
< div className = "mb-4 flex gap-2" >
< span >{ selectedKeys . length } selected </ span >
< Button variant = "red" onClick = { handleBulkDelete } >
Delete Selected
</ Button >
</ div >
)}
< Table >
< thead >
< tr >
{ /* Select all checkbox */ }
< th className = "w-[40px]" >
< Checkbox
checked = { isAllSelected }
onValueChange = {(checked) => {
if ( checked ) {
selectAll ();
} else {
deselectAll ();
}
}}
/>
</ th >
< th > ID </ th >
< th > Name </ th >
< th > Email </ th >
</ tr >
</ thead >
< tbody >
{ rows ?. map (( row , index ) => (
< tr key = {row. id } >
{ /* Individual checkbox (with Shift+click support) */ }
< td onClick = {(e) => handleCheckboxClick ( e , index )} >
< Checkbox
checked = { getSelected (row.id)}
onValueChange = {() => toggle (row.id)}
/>
</ td >
< td >{row. id } </ td >
< td >{row. username } </ td >
< td >{row. email } </ td >
</ tr >
))}
</ tbody >
</ Table >
</ div >
);
}
Auto Validation on Page Change
useSelection automatically validates selection state when allIds changes.
// Page 1: rows = [1, 2, 3]
const selection1 = useSelection ([ 1 , 2 , 3 ], []);
selection1 . toggle ( 1 ); // Select 1
selection1 . toggle ( 2 ); // Select 2
// selectedKeys = [1, 2]
// Move to page 2: rows = [4, 5, 6]
const selection2 = useSelection ([ 4 , 5 , 6 ], []);
// selectedKeys automatically cleaned to [] (1, 2 not in allIds)
Validation logic :
// Inside useSelection (list-helpers.ts:84-91)
useEffect (() => {
const selectionKeys = Array . from ( selection . keys ());
const validKeys = intersection ( allKeys , selectionKeys );
// Update state only when changes are needed
if ( validKeys . length !== selectionKeys . length ) {
setSelection ( new Map ( validKeys . map ( key => [ key , true ])));
}
}, [ allKeys , selection ]);
Why is auto validation needed? When navigating pages or changing filters, rows changes. Items selected on the previous page may not exist on the new page, so invalid selections are automatically removed.
Operations with Selected Items
const { selectedKeys } = useSelection ( allIds , []);
// Get information of selected items
const selectedRows = rows ?. filter ( row => selectedKeys . includes ( row . id )) ?? [];
// Bulk update
const handleBulkUpdate = async () => {
await UserService . save ({
params: selectedRows . map ( row => ({
id: row . id ,
status: "active" ,
}))
});
};
// Bulk export
const handleBulkExport = () => {
const csv = selectedRows . map ( row =>
` ${ row . id } , ${ row . username } , ${ row . email } `
). join ( " \n " );
// Download CSV
const blob = new Blob ([ csv ], { type: "text/csv" });
const url = URL . createObjectURL ( blob );
const a = document . createElement ( "a" );
a . href = url ;
a . download = "users.csv" ;
a . click ();
};
Advanced Usage
Initial Selection State
// Restore selection state from URL parameters
const searchParams = new URLSearchParams ( window . location . search );
const preselectedIds = searchParams . get ( "selected" )?. split ( "," ). map ( Number ) ?? [];
const { selectedKeys } = useSelection ( allIds , preselectedIds );
Saving Selection State
const { selectedKeys } = useSelection ( allIds , []);
// Save selection state to localStorage
useEffect (() => {
localStorage . setItem ( "selectedUserIds" , JSON . stringify ( selectedKeys ));
}, [ selectedKeys ]);
// Restore on next visit
const savedIds = JSON . parse ( localStorage . getItem ( "selectedUserIds" ) ?? "[]" );
const { selectedKeys } = useSelection ( allIds , savedIds );
Conditional Selection Restrictions
const { toggle } = useSelection ( allIds , []);
// Only allow selection of items meeting certain conditions
const handleToggle = ( row : UserRow ) => {
if ( row . role === "admin" ) {
alert ( "Cannot select admin users" );
return ;
}
toggle ( row . id );
};
< Checkbox
checked = { getSelected (row.id)}
onValueChange = {() => handleToggle ( row )}
disabled = {row. role === "admin" }
/>
Internal Structure
useSelection manages selection state with a Map<T, boolean> structure.
// Internal state (simplified)
const [ selection , setSelection ] = useState < Map < number , boolean >>(
new Map ([
[ 1 , true ], // ID 1 selected
[ 2 , false ], // ID 2 not selected
[ 3 , true ], // ID 3 selected
])
);
// Extract selectedKeys
const selectedKeys = Array . from ( selection )
. filter (([ key , value ]) => value === true )
. map (([ key ]) => key );
// Result: [1, 3]
Why use Map :
O(1) time complexity for fast lookups
No type constraints on keys (number, string, object all possible)
More extensible than Set (can store additional metadata)
Type Safety
useSelection can specify key type with generics.
// number type
const selection1 = useSelection < number >([ 1 , 2 , 3 ], []);
selection1 . toggle ( 1 ); // ✅ OK
selection1 . toggle ( "1" ); // ❌ Type error
// string type
const selection2 = useSelection < string >([ "a" , "b" , "c" ], []);
selection2 . toggle ( "a" ); // ✅ OK
selection2 . toggle ( 1 ); // ❌ Type error
// Complex object type
type UserKey = { id : number ; email : string };
const keys : UserKey [] = [
{ id: 1 , email: "[email protected] " },
{ id: 2 , email: "[email protected] " },
];
const selection3 = useSelection < UserKey >( keys , []);
Cautions
1. Always provide allIds
// ❌ Wrong usage
const { selectedKeys } = useSelection ([], []); // Empty array
// ✅ Correct usage
const allIds = rows ?. map ( row => row . id ) ?? [];
const { selectedKeys } = useSelection ( allIds , []);
2. To maintain selection across page changes
By default, selection is reset when navigating pages. Separate management is needed to maintain it.
// Global selection state (managed separately with useState)
const [ globalSelectedIds , setGlobalSelectedIds ] = useState < Set < number >>( new Set ());
// Sync with current page selection
useEffect (() => {
const currentPageSelected = allIds . filter ( id => globalSelectedIds . has ( id ));
// Sync with useSelection state
}, [ allIds , globalSelectedIds ]);
3. When using handleCheckboxClick
handleCheckboxClick should be connected to <td> or checkbox container.
// ✅ Correct usage
< td onClick = {(e) => handleCheckboxClick ( e , index )} >
< Checkbox ... />
</ td >
// ❌ Wrong usage (click event not propagated)
< Checkbox
onClick = {(e) => handleCheckboxClick ( e , index )}
...
/>