検索・フィルタパターン
:LiTarget: 用途
一覧画面の検索・複数フィルタ・ソートを統合管理するパターン。
:LiSparkle: 特徴
- 複数条件
- URLクエリ同期
- ファセット
- パフォーマンス対策
:LiCode: コード(コピペ用)
import { useState, useMemo, useEffect } from 'react';
// 一覧の検索・複数フィルタ・ソートを統合
type Filters = Record<string, string | undefined>;
type SortBy<T> = { field: keyof T; dir: 'asc' | 'desc' };
export function useFilteredList<T extends Record<string, any>>(
items: T[],
searchFields: (keyof T)[],
) {
const [q, setQ] = useState('');
const [filters, setFilters] = useState<Filters>({});
const [sort, setSort] = useState<SortBy<T> | null>(null);
// URL クエリ同期
useEffect(() => {
const params = new URLSearchParams();
if (q) params.set('q', q);
Object.entries(filters).forEach(([k, v]) => v && params.set(k, v));
if (sort) params.set('sort', `${String(sort.field)}:${sort.dir}`);
const url = params.toString() ? `?${params.toString()}` : window.location.pathname;
window.history.replaceState({}, '', url);
}, [q, filters, sort]);
const result = useMemo(() => {
let out = items;
if (q) {
const ql = q.toLowerCase();
out = out.filter((it) => searchFields.some((f) => String(it[f] ?? '').toLowerCase().includes(ql)));
}
Object.entries(filters).forEach(([k, v]) => {
if (v) out = out.filter((it) => String(it[k as keyof T]) === v);
});
if (sort) {
out = [...out].sort((a, b) => {
const dir = sort.dir === 'asc' ? 1 : -1;
return a[sort.field] > b[sort.field] ? dir : -dir;
});
}
return out;
}, [items, q, filters, sort, searchFields]);
return { q, setQ, filters, setFilters, sort, setSort, result };
}
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加