@usefy/use-list
v0.25.1
Published
A React hook for managing array state with immutable updates
Maintainers
Readme
Overview
@usefy/use-list manages an array as React state with immutable, ergonomic updates. Every mutation produces a brand-new array (so React re-renders correctly and the previous state is never mutated), and the returned list is typed as readonly T[] to steer you toward the provided actions.
Part of the @usefy ecosystem — a collection of production-ready React hooks designed for modern applications.
Why use-list?
- Zero Dependencies — Pure React implementation
- TypeScript First — Full
<T>generics with exported types - Immutable Updates — New array on every change;
readonly T[]return type prevents accidental in-place mutation - Rich Action Set —
set,push,filter,sort,clear,removeAt,insertAt,updateAt,reset setwith updater —set(prev => [...prev, item]), just likeuseState- Stable Actions — Action identities never change, so they're safe as
useEffectdependencies - No Wasted Renders — No-op updates (out-of-range index, empty clear, unchanged value, filtering out nothing) are skipped
- Lazy Initialization — Accepts an array/iterable or a factory
Installation
# npm
npm install @usefy/use-list
# yarn
yarn add @usefy/use-list
# pnpm
pnpm add @usefy/use-listPeer Dependencies
This package requires React 18 or 19:
{
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}Quick Start
import { useList } from "@usefy/use-list";
interface Todo {
id: number;
text: string;
completed: boolean;
}
function TodoApp() {
const [todos, { push, removeAt, updateAt }] = useList<Todo>([]);
const addTodo = (text: string) =>
push({ id: Date.now(), text, completed: false });
const toggleTodo = (index: number) => {
const todo = todos[index];
updateAt(index, { ...todo, completed: !todo.completed });
};
return (
<ul>
{todos.map((todo, i) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(i)}
/>
{todo.text}
<button onClick={() => removeAt(i)}>×</button>
</li>
))}
</ul>
);
}API Reference
useList<T>(initialState?)
Returns a tuple of the current read-only list and a stable actions object.
Parameters
| Parameter | Type | Default | Description |
| -------------- | --------------------- | ------- | -------------------------------------------------------------- |
| initialState | ListInitializer<T> | empty | An array/iterable of items, or a factory returning one (evaluated once) |
Returns [list, actions]
| Item | Type | Description |
| --------- | -------------------- | ---------------------------------------------- |
| list | readonly T[] | Current list. Read via index, map, iteration |
| actions | UseListActions<T> | Stable action handlers (see below) |
Actions
| Action | Signature | Description |
| ---------- | -------------------------------------------------------------- | --------------------------------------------------------------------- |
| set | (next: T[] \| ((prev: readonly T[]) => T[])) => void | Replace the whole list, by value or updater |
| push | (...items: T[]) => void | Append one or more items |
| filter | (predicate: (item: T, index: number) => boolean) => void | Keep only matching items (no-op if nothing removed) |
| sort | (compareFn?: (a: T, b: T) => number) => void | Sort immutably (the current list is not mutated) |
| clear | () => void | Remove all items (no-op if already empty) |
| removeAt | (index: number) => void | Remove the item at index (no-op if out of range) |
| insertAt | (index: number, ...items: T[]) => void | Insert item(s) at index (index clamped to [0, length]) |
| updateAt | (index: number, item: T) => void | Replace the item at index (no-op if out of range or unchanged) |
| reset | () => void | Restore the initial items (a fresh copy) |
The returned
listisreadonly T[], so callinglist.push(...)directly is a TypeScript error. Use the actions — mutating the array in place would bypass React state and break re-renders.
Examples
Functional set, sort, filter
const [nums, { set, sort, filter }] = useList<number>([3, 1, 2]);
set((prev) => [...prev, 4]); // append via updater
sort((a, b) => a - b); // immutable ascending sort
filter((n) => n % 2 === 0); // keep evensInsert and reorder
const [steps, { insertAt, removeAt }] = useList<string>(["start", "end"]);
insertAt(1, "middle"); // ["start", "middle", "end"]
removeAt(0); // ["middle", "end"]Reset to initial
const [items, { push, reset }] = useList<string>(["a", "b"]);
push("c");
reset(); // back to ["a", "b"]Stable actions as effect dependencies
const [log, actions] = useList<string>();
useEffect(() => {
const unsub = subscribe((line) => actions.push(line));
return unsub;
}, [actions]); // actions never changes identity — effect runs onceTypeScript
import {
useList,
type ListInitializer,
type UseListActions,
type UseListReturn,
} from "@usefy/use-list";
const [list, actions]: UseListReturn<number> = useList<number>([1, 2, 3]);Behavior Notes
- Immutable — Actions never mutate the current list; they replace it with a new one. Any snapshot you captured stays valid.
- Referentially stable actions — The actions object and each function keep the same identity for the lifetime of the component.
- Initial value is copied — The array you pass in is never mutated, and
resetalways yields a fresh copy of it. - No-op skipping — Updates that wouldn't change anything don't create a new array or trigger a re-render.
Testing
This package maintains comprehensive test coverage to ensure reliability and stability.
Test Coverage
📊 View Detailed Coverage Report (GitHub Pages)
Test Files
useList.test.ts— 30 tests for hook behavior and immutability
Total: 30 tests
License
MIT © mirunamu
This package is part of the usefy monorepo.
