Skip to main content
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 selectionSet-based efficient state

Range Selection

Shift+click supportSelect consecutive items at once

Auto Validation

Auto cleanup on page changeKeep only existing items

Type Safety

Generic type supportUse 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:
  1. Normal click: Toggle individual item selection
  2. Shift+click: Select all items from last selection to current position
How Shift+click workshandleCheckboxClick 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)}
  ...
/>