@rmnddesign/multi-select
v1.0.1
Published
A tiny type-safe React hook for managing multi-select checkbox state
Downloads
179
Maintainers
Readme
@rmnddesign/multi-select
A tiny type-safe React hook for managing multi-select checkbox state.
The Problem
When you have a list of checkboxes where users can select multiple items, you end up writing the same boilerplate code over and over:
const [selected, setSelected] = useState<string[]>([])
const toggleItem = (item: string) => {
setSelected(prev =>
prev.includes(item)
? prev.filter(x => x !== item) // remove if already selected
: [...prev, item] // add if not selected
)
}
const isSelected = (item: string) => selected.includes(item)This hook does all of that for you.
Installation
npm install @rmnddesign/multi-selectQuick Start
import { useMultiSelect } from "@rmnddesign/multi-select"
function FruitPicker() {
const { selected, toggle, isSelected } = useMultiSelect<string>()
const fruits = ["apple", "banana", "cherry", "mango"]
return (
<div>
<h3>Pick your favorite fruits:</h3>
{fruits.map((fruit) => (
<label key={fruit} style={{ display: "block" }}>
<input
type="checkbox"
checked={isSelected(fruit)}
onChange={() => toggle(fruit)}
/>
{fruit}
</label>
))}
<p>You selected: {selected.length === 0 ? "nothing yet" : selected.join(", ")}</p>
</div>
)
}What's happening here:
useMultiSelect<string>()creates a new multi-select state for string valuesisSelected(fruit)returnstrueif that fruit is currently selectedtoggle(fruit)adds the fruit if not selected, or removes it if already selectedselectedis the array of all currently selected fruits
Common Examples
Pre-select some items
Pass an array of initially selected items:
// "banana" and "mango" will be checked by default
const { selected, toggle, isSelected } = useMultiSelect(["banana", "mango"])Limit how many items can be selected
Use the max option to set a maximum:
// Users can only select up to 3 items
const { selected, toggle, isSelected } = useMultiSelect<string>([], {
max: 3
})
// When 3 items are selected, toggle() won't add more items
// (but it will still remove items if you click a selected one)Require at least one selection
Use the min option to prevent deselecting below a threshold:
// Users must keep at least 1 item selected
const { selected, toggle, isSelected } = useMultiSelect(["apple"], {
min: 1
})
// If only 1 item is selected, clicking it won't deselect itDo something when selection changes
Use the onChange callback:
const { selected, toggle, isSelected } = useMultiSelect<string>([], {
onChange: (newSelected) => {
console.log("Selection changed:", newSelected)
// Maybe save to localStorage, send to server, etc.
}
})Select All / Clear All buttons
function MyComponent() {
const { selected, toggle, isSelected, selectAll, clear } = useMultiSelect<string>()
const allOptions = ["apple", "banana", "cherry", "mango"]
return (
<div>
<button onClick={() => selectAll(allOptions)}>Select All</button>
<button onClick={() => clear()}>Clear All</button>
{allOptions.map((option) => (
<label key={option}>
<input
type="checkbox"
checked={isSelected(option)}
onChange={() => toggle(option)}
/>
{option}
</label>
))}
</div>
)
}Working with objects (not just strings)
You can use any type, not just strings. For objects, make sure you're using the same reference:
type User = { id: number; name: string }
function UserPicker() {
// Define users outside or useMemo to keep stable references
const users: User[] = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
]
const { selected, toggle, isSelected } = useMultiSelect<User>()
return (
<div>
{users.map((user) => (
<label key={user.id}>
<input
type="checkbox"
checked={isSelected(user)}
onChange={() => toggle(user)}
/>
{user.name}
</label>
))}
<p>Selected IDs: {selected.map(u => u.id).join(", ")}</p>
</div>
)
}Important: For objects, isSelected() uses reference equality (===). This means the exact same object reference must be used. If you're fetching data from an API, consider using IDs (strings/numbers) instead of full objects.
Using IDs instead of objects (recommended for API data)
type User = { id: number; name: string }
function UserPicker() {
const [users, setUsers] = useState<User[]>([])
// Store just the IDs, not the full objects
const { selected, toggle, isSelected } = useMultiSelect<number>()
useEffect(() => {
fetch("/api/users")
.then(res => res.json())
.then(setUsers)
}, [])
return (
<div>
{users.map((user) => (
<label key={user.id}>
<input
type="checkbox"
checked={isSelected(user.id)} // check by ID
onChange={() => toggle(user.id)} // toggle by ID
/>
{user.name}
</label>
))}
<p>Selected IDs: {selected.join(", ")}</p>
{/* If you need the full objects: */}
<p>Selected users: {users.filter(u => selected.includes(u.id)).map(u => u.name).join(", ")}</p>
</div>
)
}Full API Reference
The hook
const result = useMultiSelect<T>(initialSelected?, options?)Parameters:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| initialSelected | T[] | [] | Items that should be selected when the component first renders |
| options | object | {} | Configuration options (see below) |
Options:
| Option | Type | Description |
|--------|------|-------------|
| max | number | Maximum number of items that can be selected. When reached, toggle() and select() won't add more. |
| min | number | Minimum number of items that must stay selected. When reached, toggle() and deselect() won't remove more. |
| onChange | (selected: T[]) => void | Function called whenever the selection changes. Receives the new array of selected items. |
What you get back
| Property | Type | Description |
|----------|------|-------------|
| selected | T[] | Array of currently selected items |
| toggle(item) | (item: T) => void | Add item if not selected, remove if selected |
| isSelected(item) | (item: T) => boolean | Check if an item is currently selected |
| select(item) | (item: T) => void | Add an item (does nothing if already selected) |
| deselect(item) | (item: T) => void | Remove an item (does nothing if not selected) |
| selectAll(items) | (items: T[]) => void | Add multiple items at once |
| clear() | () => void | Remove all selected items |
| setSelected(items) | (items: T[]) => void | Replace the entire selection with a new array |
TypeScript
The hook is fully type-safe. Specify your type in angle brackets:
// For strings
const { selected } = useMultiSelect<string>()
// selected is string[]
// For numbers
const { selected } = useMultiSelect<number>()
// selected is number[]
// For custom types
type Tag = { id: string; label: string }
const { selected } = useMultiSelect<Tag>()
// selected is Tag[]If you pass initial values, TypeScript will infer the type automatically:
// TypeScript knows this is useMultiSelect<string>
const { selected } = useMultiSelect(["apple", "banana"])License
MIT
