brackt/app/hooks/useSortableData.ts
2026-03-22 20:41:44 -07:00

74 lines
2.1 KiB
TypeScript

import { useState, useMemo } from "react";
export type SortDirection = "asc" | "desc";
export interface SortConfig<T> {
key: keyof T | null;
direction: SortDirection;
}
type Comparators<T> = Partial<Record<keyof T, (a: T, b: T, direction: SortDirection) => number>>;
/**
* Generic hook for client-side sortable table data.
*
* @param data - The array of items to sort
* @param defaultKey - The column key to sort by initially (null = no sort)
* @param defaultDirection - Initial sort direction
* @param comparators - Optional per-key custom comparators. Receives both items and the
* current direction so the comparator can handle ascending/descending internally
* (return positive to sort a after b).
*/
export function useSortableData<T>(
data: T[],
defaultKey: keyof T | null = null,
defaultDirection: SortDirection = "asc",
comparators?: Comparators<T>
): {
sortedData: T[];
sortConfig: SortConfig<T>;
requestSort: (key: keyof T) => void;
} {
const [sortConfig, setSortConfig] = useState<SortConfig<T>>({
key: defaultKey,
direction: defaultDirection,
});
const sortedData = useMemo(() => {
if (!sortConfig.key) return data;
const key = sortConfig.key;
const dir = sortConfig.direction;
return data.toSorted((a, b) => {
// Use custom comparator if provided
if (comparators?.[key]) {
return comparators[key]?.(a, b, dir) ?? 0;
}
const aVal = a[key];
const bVal = b[key];
if (aVal === null || aVal === undefined) return 1;
if (bVal === null || bVal === undefined) return -1;
let cmp = 0;
if (typeof aVal === "string" && typeof bVal === "string") {
cmp = aVal.localeCompare(bVal);
} else {
cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
}
return dir === "asc" ? cmp : -cmp;
});
}, [data, sortConfig, comparators]);
const requestSort = (key: keyof T) => {
setSortConfig((prev) => ({
key,
direction: prev.key === key && prev.direction === "asc" ? "desc" : "asc",
}));
};
return { sortedData, sortConfig, requestSort };
}