Rooks
HooksState Management

useSelectableList

Manages a single-selection list state with toggle and clear helpers.

About

Easily select a single value from a list of values. very useful for radio buttons, select inputs etc.

Examples

import { useSelectableList } from "rooks";

const colors = ["Red", "Green", "Blue"];

export default function ColorPicker() {
  const [[, selected], { matchSelection, updateSelection }] = useSelectableList(
    colors,
    0
  );

  return (
    <fieldset>
      <legend>Selected: {selected}</legend>
      {colors.map((color) => (
        <label key={color}>
          <input
            type="radio"
            name="color"
            checked={matchSelection({ value: color })}
            onChange={updateSelection({ value: color })}
          />
          {color}
        </label>
      ))}
    </fieldset>
  );
}

Pizza topping selector

import { useEffect, useState } from "react";
import { useSelectableList } from "rooks";
import { createGlobalStyle } from "styled-components";

const GlobalStyles = createGlobalStyle`
  .App {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}

h3 {
  text-align: center;
}

.topping {
  margin-top: 0.3rem;
  vertical-align: text-bottom;
}

.result {
  margin-top: 1rem;
}

.toppings-list,
.total {
  width: 30%;
  margin: 0 auto;
}

.toppings-list {
  list-style: none;
  padding: 0;
}

.toppings-list li {
  margin-bottom: 0.5rem;
}

.toppings-list-item {
  display: flex;
  justify-content: space-between;
}

.toppings-list li:last-child {
  border-top: 1px solid #ccc;
  margin-top: 1rem;
  padding-top: 1rem;
}

.toppings-list-item label {
  vertical-align: text-bottom;
  margin-left: 0.2rem;
}

.total {
  margin-top: 1rem;
}

@media screen and (max-width: 600px) {
  .toppings-list,
  .total {
    width: 90%;
  }
}

`;

export const toppings = [
  {
    name: "Capsicum",
    price: 1.2,
  },
  {
    name: "Paneer",
    price: 2.0,
  },
  {
    name: "Red Paprika",
    price: 2.5,
  },
  {
    name: "Onions",
    price: 3.0,
  },
  {
    name: "Extra Cheese",
    price: 3.5,
  },
];

export default function App() {
  const [total, setTotal] = useState(0);
  const [selection, { matchSelection, toggleSelection, updateSelection }] =
    useSelectableList(toppings, 0);

  useEffect(() => {
    setTotal(selection[1].price);
  }, [selection]);

  return (
    <div className="App">
      <GlobalStyles />
      <h3>useSelectableList Example</h3>
      <ul className="toppings-list">
        {toppings.map(({ name, price }, index) => {
          return (
            <li key={index}>
              <div className="toppings-list-item">
                <div className="left-section">
                  <input
                    type="checkbox"
                    id={`custom-checkbox-${index}`}
                    name={name}
                    checked={matchSelection({ index })}
                    onChange={() => toggleSelection({ index })()}
                  />
                  <label htmlFor={`custom-checkbox-${index}`}>{name}</label>
                </div>
                <div className="right-section">{price}</div>
              </div>
            </li>
          );
        })}
        <li>
          <div className="toppings-list-item">
            <div className="left-section">Total:</div>
            <div className="right-section">{total}</div>
          </div>
        </li>
      </ul>
    </div>
  );
}

Allow clearing the current selection

import { useSelectableList } from "rooks";

const statuses = ["draft", "review", "published"];

export default function StatusPicker() {
  const [selection, { toggleSelection, matchSelection }] = useSelectableList(
    statuses,
    0,
    true
  );

  return (
    <div>
      {statuses.map((status) => (
        <button
          key={status}
          onClick={toggleSelection({ value: status })}
          style={{
            fontWeight: matchSelection({ value: status }) ? "bold" : "normal",
          }}
        >
          {status}
        </button>
      ))}
      <p>Current selection: {selection[1] ?? "none"}</p>
    </div>
  );
}

Parameters

Argument valueTypeDescriptionDefault value
listArrayA list of items of any type[]
initialIndexnumberIndex of the initially selected item0
allowUnselectedbooleanWhether to allow unselect when update selectionfalse

Return value

Returns an array of following items:

Return valueTypeDescription
selectionArrayThe first item is the selected index, the second item is the selected value
methodsObjectObject with methods to control the selectable list, see the table below

Methods:

MethodsTypeDescription
matchSelection({ index?: number, value?: T }) => Booleanreturns true if the item is selected
toggleSelection({ index?: number, value?: T }) => () => voidreturns a function to toggle an item by index or value
updateSelection({ index?: number, value?: T }) => () => voidreturns a function to update specified item

Behavior and lifecycle

The hook stores an index and derives the selected value from the current list. Control factories return event handlers. Values are matched with indexOf/strict equality; missing values cause a warning and no update. Passing both index and value, or neither, also warns. Selecting the active item clears the selection to index -1 only when allowUnselected is true; otherwise it warns and retains the selection.

Compatibility and accessibility

The hook is SSR-safe. Use native radio/select semantics or expose aria-selected, keyboard navigation, and a visible focus indicator when building a custom single-selection widget.

On this page