@@ -24,7 +24,9 @@ import MenuList from "@mui/material/MenuList";
24
24
import { Loader } from "components/Loader/Loader" ;
25
25
import Divider from "@mui/material/Divider" ;
26
26
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined" ;
27
+
27
28
import { useDebouncedFunction } from "hooks/debounce" ;
29
+ import { useEffectEvent } from "hooks/hookPolyfills" ;
28
30
29
31
export type PresetFilter = {
30
32
name : string ;
@@ -33,28 +35,53 @@ export type PresetFilter = {
33
35
34
36
type FilterValues = Record < string , string | undefined > ;
35
37
38
+ type UseFilterConfig = {
39
+ initialValue ?: string | ( ( ) => string ) ;
40
+ searchParamsResult : ReturnType < typeof useSearchParams > ;
41
+ onUpdate ?: ( newValue : string ) => void ;
42
+ } ;
43
+
44
+ const useFilterParamsKey = "filter" ;
45
+
36
46
export const useFilter = ( {
37
47
initialValue = "" ,
38
48
onUpdate,
39
49
searchParamsResult,
40
- } : {
41
- initialValue ?: string ;
42
- searchParamsResult : ReturnType < typeof useSearchParams > ;
43
- onUpdate ?: ( ) => void ;
44
- } ) => {
50
+ } : UseFilterConfig ) => {
51
+ // Fully expect the initialValue functions to have some impurity (e.g. reading
52
+ // from localStorage during a render path). (Ab)using useState's lazy
53
+ // initialization mode to guarantee impurities only exist on mount. Pattern
54
+ // has added benefit of locking down initialValue and ignoring any accidental
55
+ // value changes on re-renders
56
+ const [ readonlyInitialQueryState ] = useState ( initialValue ) ;
57
+
58
+ // React Router doesn't give setSearchParams a stable memory reference; need
59
+ // extra logic to prevent on-mount effect from running too often
45
60
const [ searchParams , setSearchParams ] = searchParamsResult ;
46
- const query = searchParams . get ( "filter" ) ?? initialValue ;
47
- const values = parseFilterQuery ( query ) ;
61
+ const syncSearchParamsOnMount = useEffectEvent ( ( ) => {
62
+ setSearchParams ( ( current ) => {
63
+ const currentFilter = current . get ( useFilterParamsKey ) ;
64
+ if ( currentFilter !== readonlyInitialQueryState ) {
65
+ current . set ( useFilterParamsKey , readonlyInitialQueryState ) ;
66
+ }
48
67
49
- const update = ( values : string | FilterValues ) => {
50
- if ( typeof values === "string" ) {
51
- searchParams . set ( "filter" , values ) ;
52
- } else {
53
- searchParams . set ( "filter" , stringifyFilter ( values ) ) ;
54
- }
68
+ return current ;
69
+ } ) ;
70
+ } ) ;
71
+
72
+ useEffect ( ( ) => {
73
+ syncSearchParamsOnMount ( ) ;
74
+ } , [ syncSearchParamsOnMount ] ) ;
75
+
76
+ const update = ( newValues : string | FilterValues ) => {
77
+ const serialized =
78
+ typeof newValues === "string" ? newValues : stringifyFilter ( newValues ) ;
79
+
80
+ searchParams . set ( useFilterParamsKey , serialized ) ;
55
81
setSearchParams ( searchParams ) ;
56
- if ( onUpdate ) {
57
- onUpdate ( ) ;
82
+
83
+ if ( onUpdate !== undefined ) {
84
+ onUpdate ( serialized ) ;
58
85
}
59
86
} ;
60
87
@@ -63,6 +90,8 @@ export const useFilter = ({
63
90
500 ,
64
91
) ;
65
92
93
+ const query = searchParams . get ( "filter" ) ?? readonlyInitialQueryState ;
94
+ const values = parseFilterQuery ( query ) ;
66
95
const used = query !== "" && query !== initialValue ;
67
96
68
97
return {
0 commit comments