Skip to content

Commit e2d3a67

Browse files
committed
chore: update filter to have better callback support
1 parent 2f92f86 commit e2d3a67

File tree

1 file changed

+44
-15
lines changed

1 file changed

+44
-15
lines changed

site/src/components/Filter/filter.tsx

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import MenuList from "@mui/material/MenuList";
2424
import { Loader } from "components/Loader/Loader";
2525
import Divider from "@mui/material/Divider";
2626
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
27+
2728
import { useDebouncedFunction } from "hooks/debounce";
29+
import { useEffectEvent } from "hooks/hookPolyfills";
2830

2931
export type PresetFilter = {
3032
name: string;
@@ -33,28 +35,53 @@ export type PresetFilter = {
3335

3436
type FilterValues = Record<string, string | undefined>;
3537

38+
type UseFilterConfig = {
39+
initialValue?: string | (() => string);
40+
searchParamsResult: ReturnType<typeof useSearchParams>;
41+
onUpdate?: (newValue: string) => void;
42+
};
43+
44+
const useFilterParamsKey = "filter";
45+
3646
export const useFilter = ({
3747
initialValue = "",
3848
onUpdate,
3949
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
4560
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+
}
4867

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);
5581
setSearchParams(searchParams);
56-
if (onUpdate) {
57-
onUpdate();
82+
83+
if (onUpdate !== undefined) {
84+
onUpdate(serialized);
5885
}
5986
};
6087

@@ -63,6 +90,8 @@ export const useFilter = ({
6390
500,
6491
);
6592

93+
const query = searchParams.get("filter") ?? readonlyInitialQueryState;
94+
const values = parseFilterQuery(query);
6695
const used = query !== "" && query !== initialValue;
6796

6897
return {

0 commit comments

Comments
 (0)