@@ -35,6 +35,14 @@ export type PresetFilter = {
35
35
type FilterValues = Record < string , string | undefined > ;
36
36
37
37
type UseFilterConfig = {
38
+ /**
39
+ * If initialValue is a string, that value will be used immediately from the
40
+ * first render.
41
+ *
42
+ * If it's a function, the function will be lazy-evaluated via an effect
43
+ * after the initial render. The initial render will use an empty string while
44
+ * waiting for the effect to run.
45
+ */
38
46
initialValue ?: string | ( ( ) => string ) ;
39
47
searchParamsResult : ReturnType < typeof useSearchParams > ;
40
48
onUpdate ?: ( newValue : string ) => void ;
@@ -49,25 +57,31 @@ export const useFilter = ({
49
57
} : UseFilterConfig ) => {
50
58
const [ searchParams , setSearchParams ] = searchParamsResult ;
51
59
52
- // Fully expect the initialValue functions to have some impurity (e.g. reading
53
- // from localStorage during a render path). (Ab)using useState's lazy
54
- // initialization mode to guarantee impurities only exist on mount. Pattern
55
- // has added benefit of locking down initialValue and ignoring any accidental
56
- // value changes on re-renders
57
- const [ readonlyInitialQueryState ] = useState ( initialValue ) ;
60
+ // Copying initialValue into state to lock the value down from the outside
61
+ const [ initializedValue , setInitializedValue ] = useState ( ( ) => {
62
+ return typeof initialValue === "string" ? initialValue : "" ;
63
+ } ) ;
58
64
59
- // Sync the params with the value provided via the initialValue function;
60
- // should behave only as an on-mount effect
65
+ // Lazy-evaluate initialValue only on mount; have to resolve via effect
66
+ // because initialValue is allowed to be impure (e.g., read from localStorage)
67
+ const lazyOnMountRef = useRef ( initialValue ) ;
61
68
useEffect ( ( ) => {
69
+ if ( typeof lazyOnMountRef . current === "string" ) {
70
+ return ;
71
+ }
72
+
73
+ const lazyInitialValue = lazyOnMountRef . current ( ) ;
74
+ setInitializedValue ( lazyInitialValue ) ;
75
+
62
76
setSearchParams ( ( current ) => {
63
77
const currentFilter = current . get ( useFilterParamsKey ) ;
64
- if ( currentFilter !== readonlyInitialQueryState ) {
65
- current . set ( useFilterParamsKey , readonlyInitialQueryState ) ;
78
+ if ( currentFilter !== lazyInitialValue ) {
79
+ current . set ( useFilterParamsKey , lazyInitialValue ) ;
66
80
}
67
81
68
82
return current ;
69
83
} ) ;
70
- } , [ setSearchParams , readonlyInitialQueryState ] ) ;
84
+ } , [ setSearchParams ] ) ;
71
85
72
86
const update = ( newValues : string | FilterValues ) => {
73
87
const serialized =
@@ -82,11 +96,11 @@ export const useFilter = ({
82
96
} ;
83
97
84
98
const { debounced : debounceUpdate , cancelDebounce } = useDebouncedFunction (
85
- ( values : string | FilterValues ) => update ( values ) ,
99
+ update ,
86
100
500 ,
87
101
) ;
88
102
89
- const query = searchParams . get ( "filter" ) ?? readonlyInitialQueryState ;
103
+ const query = searchParams . get ( "filter" ) ?? initializedValue ;
90
104
const values = parseFilterQuery ( query ) ;
91
105
const used = query !== "" && query !== initialValue ;
92
106
0 commit comments