forked from DA0-DA0/dao-dao-ui
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathuseSearchFilter.ts
151 lines (140 loc) · 4.54 KB
/
useSearchFilter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import Fuse from 'fuse.js'
import {
ChangeEventHandler,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { SearchBarProps } from '../components'
import { useQuerySyncedState } from './useQuerySyncedState'
export type UseSearchFilterOptions<T> = {
/**
* Data to filter.
*/
data: T[]
/**
* The filterable keys for the dataset.
*/
filterableKeys: Fuse.FuseOptionKey<T>[]
/**
* Optional Fuse options.
*/
options?: Fuse.IFuseOptions<T>
/**
* Optionally store the search param in the URL query string.
*/
querySyncedParam?: string
/**
* Optionally add a filter change callback listener.
*/
onFilterChange?: (filter: string) => void
}
export type UseSearchFilterReturn<T> = {
/**
* Props to pass through to the `SearchBar` component.
*/
searchBarProps: SearchBarProps
/**
* The filtered data.
*/
filteredData: { item: T; originalIndex: number }[]
/**
* The filter string.
*/
filter: string
/**
* A function that lets setting the search filter.
*/
setFilter: Dispatch<SetStateAction<string>>
}
// Pass an array of data and filterable keys, and get `searchBarProps` (for
// passing to `SearchBar`) and memoized `filteredData`.
//
// NOTE: `data` is often un-memoized, so it is very important not to update any
// state when `data` changes. Doing so could lead to an infinite loop, in the
// event that the state update causes `data` to change again, which is not
// uncommon. This is due to the variety of input data sources. Ideally, all data
// comes from a state selector that is memoized, but often data must be updated
// on-the-fly based on various display conditions; memoizing all the inputs to
// this hook would require a bunch of extra `useMemo` hooks that would probably
// reduce performance beyond what would be worth it, and it would require all
// contributors to have an in-depth understanding of the nuances of React hooks,
// references, Fuse.js, etc. The solution here is to simply update the Fuse.js
// collection before filtering, which should be a relatively cheap (linear time)
// operation because most lists being filtered are short. We are not filtering
// thousands of items from an arbitrary database—typically we are just filtering
// a handful of actions or NFTs. Previously, this hook was trying to be clever
// and only update the Fuse.js collection when the data deeply changed, but that
// required extra state updates and logic that often led to infinite loops. If
// this becomes a performance issue, we can revisit this.
export const useSearchFilter = <T extends unknown>({
data,
filterableKeys,
options,
querySyncedParam,
onFilterChange,
}: UseSearchFilterOptions<T>): UseSearchFilterReturn<T> => {
// Store latest data in a ref so we can compare it to the current data.
const dataRef = useRef(data)
const [fuse, setFuse] = useState<Fuse<T>>(() =>
makeFuse(data, filterableKeys, options)
)
// Make new fuse when keys or options change. Update existing fuse when data
// changes.
useEffect(() => {
setFuse(makeFuse(data, filterableKeys, options))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterableKeys, options])
const [filter, setFilter] = useQuerySyncedState({
// When param is undefined, this is just like a normal `useState` hook.
param: querySyncedParam,
defaultValue: '',
})
// When collection or filter is updated, re-filter.
const filteredData: UseSearchFilterReturn<T>['filteredData'] = useMemo(() => {
if (filter) {
// If data has changed, update fuse collection before filtering.
if (dataRef.current !== data) {
fuse.setCollection(data)
dataRef.current = data
}
return fuse.search(filter).map(({ item, refIndex }) => ({
item,
originalIndex: refIndex,
}))
} else {
return data.map((item, originalIndex) => ({
item,
originalIndex,
}))
}
}, [fuse, filter, data])
const onFilterChangeRef = useRef(onFilterChange)
onFilterChangeRef.current = onFilterChange
const onChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => {
const filter = event.target.value
setFilter(filter)
onFilterChangeRef.current?.(filter)
},
[setFilter]
)
return {
searchBarProps: {
onChange,
value: filter,
},
filteredData,
filter,
setFilter,
}
}
const makeFuse = <T extends unknown>(
data: T[],
keys: Fuse.FuseOptionKey<T>[],
options?: Fuse.IFuseOptions<T>
) => new Fuse(data, { keys, ...options })