Skip to content

Commit df1f8df

Browse files
committed
refactor: clean up filter to be fully pure on mount
1 parent b06d772 commit df1f8df

File tree

2 files changed

+27
-16
lines changed

2 files changed

+27
-16
lines changed

site/src/components/Filter/filter.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ export type PresetFilter = {
3535
type FilterValues = Record<string, string | undefined>;
3636

3737
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+
*/
3846
initialValue?: string | (() => string);
3947
searchParamsResult: ReturnType<typeof useSearchParams>;
4048
onUpdate?: (newValue: string) => void;
@@ -49,25 +57,31 @@ export const useFilter = ({
4957
}: UseFilterConfig) => {
5058
const [searchParams, setSearchParams] = searchParamsResult;
5159

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+
});
5864

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);
6168
useEffect(() => {
69+
if (typeof lazyOnMountRef.current === "string") {
70+
return;
71+
}
72+
73+
const lazyInitialValue = lazyOnMountRef.current();
74+
setInitializedValue(lazyInitialValue);
75+
6276
setSearchParams((current) => {
6377
const currentFilter = current.get(useFilterParamsKey);
64-
if (currentFilter !== readonlyInitialQueryState) {
65-
current.set(useFilterParamsKey, readonlyInitialQueryState);
78+
if (currentFilter !== lazyInitialValue) {
79+
current.set(useFilterParamsKey, lazyInitialValue);
6680
}
6781

6882
return current;
6983
});
70-
}, [setSearchParams, readonlyInitialQueryState]);
84+
}, [setSearchParams]);
7185

7286
const update = (newValues: string | FilterValues) => {
7387
const serialized =
@@ -82,11 +96,11 @@ export const useFilter = ({
8296
};
8397

8498
const { debounced: debounceUpdate, cancelDebounce } = useDebouncedFunction(
85-
(values: string | FilterValues) => update(values),
99+
update,
86100
500,
87101
);
88102

89-
const query = searchParams.get("filter") ?? readonlyInitialQueryState;
103+
const query = searchParams.get("filter") ?? initializedValue;
90104
const values = parseFilterQuery(query);
91105
const used = query !== "" && query !== initialValue;
92106

site/src/pages/WorkspacesPage/WorkspacesPage.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,6 @@ const useWorkspacesFilter = ({
151151
searchParamsResult,
152152
initialValue: () => {
153153
const fallbackValue = "owner:me";
154-
155-
// Have to include check because initialValue will be called during the
156-
// render itself; future-proofing for SSR, if we ever need that
157154
if (typeof window === "undefined") {
158155
return fallbackValue;
159156
}

0 commit comments

Comments
 (0)