Skip to content

Commit 54a5940

Browse files
committed
wip: commit progress for searchbar fix
1 parent 29656c1 commit 54a5940

File tree

2 files changed

+58
-53
lines changed

2 files changed

+58
-53
lines changed

site/src/components/Filter/filter.tsx

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -36,53 +36,23 @@ type FilterValues = Record<string, string | undefined>;
3636

3737
type UseFilterConfig = {
3838
/**
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.
39+
* The fallback value to use in the event that no filter params can be used.
40+
* This value is allowed to change on re-renders.
4541
*/
46-
initialValue?: string | (() => string);
42+
initialValue?: string;
4743
searchParamsResult: ReturnType<typeof useSearchParams>;
4844
onUpdate?: (newValue: string) => void;
4945
};
5046

5147
const useFilterParamsKey = "filter";
5248

5349
export const useFilter = ({
54-
initialValue = "",
55-
onUpdate,
50+
initialValue: fallbackFilter = "",
5651
searchParamsResult,
52+
onUpdate,
5753
}: UseFilterConfig) => {
5854
const [searchParams, setSearchParams] = searchParamsResult;
5955

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-
});
64-
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);
68-
useEffect(() => {
69-
if (typeof lazyOnMountRef.current === "string") {
70-
return;
71-
}
72-
73-
const lazyInitialValue = lazyOnMountRef.current();
74-
setInitializedValue(lazyInitialValue);
75-
76-
setSearchParams((current) => {
77-
const currentFilter = current.get(useFilterParamsKey);
78-
if (currentFilter !== lazyInitialValue) {
79-
current.set(useFilterParamsKey, lazyInitialValue);
80-
}
81-
82-
return current;
83-
});
84-
}, [setSearchParams]);
85-
8656
const update = (newValues: string | FilterValues) => {
8757
const serialized =
8858
typeof newValues === "string" ? newValues : stringifyFilter(newValues);
@@ -100,9 +70,9 @@ export const useFilter = ({
10070
500,
10171
);
10272

103-
const query = searchParams.get("filter") ?? initializedValue;
73+
const query = searchParams.get(useFilterParamsKey) ?? fallbackFilter;
10474
const values = parseFilterQuery(query);
105-
const used = query !== "" && query !== initialValue;
75+
const used = query !== "" && query !== fallbackFilter;
10676

10777
return {
10878
query,

site/src/pages/WorkspacesPage/WorkspacesPage.tsx

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
useDashboard,
55
useIsWorkspaceActionsEnabled,
66
} from "components/Dashboard/DashboardProvider";
7-
import { FC, useEffect, useState } from "react";
7+
import { type FC, useEffect, useState, useSyncExternalStore } from "react";
88
import { Helmet } from "react-helmet-async";
99
import { pageTitle } from "utils/page";
1010
import { useWorkspacesData, useWorkspaceUpdate } from "./data";
@@ -44,7 +44,11 @@ const WorkspacesPage: FC = () => {
4444
// each hook.
4545
const searchParamsResult = useSafeSearchParams();
4646
const pagination = usePagination({ searchParamsResult });
47-
const filterProps = useWorkspacesFilter({ searchParamsResult, pagination });
47+
const filterProps = useWorkspacesFilter({
48+
searchParamsResult,
49+
onPageChange: () => pagination.goToPage(1),
50+
});
51+
4852
const { data, error, queryKey, refetch } = useWorkspacesData({
4953
...pagination,
5054
query: filterProps.filter.query,
@@ -136,30 +140,61 @@ const WorkspacesPage: FC = () => {
136140

137141
export default WorkspacesPage;
138142

143+
const workspaceFilterKey = "WorkspacesPage/filter";
144+
const defaultWorkspaceFilter = "owner:me";
145+
146+
// Function should stay outside components as much as possible; if declared
147+
// inside the component, React would add/remove event listeners every render
148+
function subscribeToFilterChanges(notifyReact: () => void) {
149+
const onStorageChange = (event: StorageEvent) => {
150+
const { key, storageArea, oldValue, newValue } = event;
151+
152+
const shouldNotify =
153+
key === workspaceFilterKey &&
154+
storageArea === window.localStorage &&
155+
newValue !== oldValue;
156+
157+
if (shouldNotify) {
158+
notifyReact();
159+
}
160+
};
161+
162+
window.addEventListener("storage", onStorageChange);
163+
return () => window.removeEventListener("storage", onStorageChange);
164+
}
165+
139166
type UseWorkspacesFilterOptions = {
140167
searchParamsResult: ReturnType<typeof useSearchParams>;
141-
pagination: ReturnType<typeof usePagination>;
168+
onPageChange: () => void;
142169
};
143170

144-
const filterPreferencesKey = "WorkspacesPage/filterPreferences";
145-
146171
const useWorkspacesFilter = ({
147172
searchParamsResult,
148-
pagination,
173+
onPageChange,
149174
}: UseWorkspacesFilterOptions) => {
175+
// Using useSyncExternalStore store to safely access localStorage from the
176+
// first render; both snapshot callbacks return primitives, so no special
177+
// trickery needed to prevent hook from immediately blowing up in dev mode
178+
const localStorageFilter = useSyncExternalStore(
179+
subscribeToFilterChanges,
180+
() => {
181+
return (
182+
window.localStorage.getItem(workspaceFilterKey) ??
183+
defaultWorkspaceFilter
184+
);
185+
},
186+
() => defaultWorkspaceFilter,
187+
);
188+
150189
const filter = useFilter({
190+
/**
191+
* @todo Rename initialValue to fallbackFilter once changes have been tested
192+
*/
193+
initialValue: localStorageFilter,
151194
searchParamsResult,
152-
initialValue: () => {
153-
const fallbackValue = "owner:me";
154-
if (typeof window === "undefined") {
155-
return fallbackValue;
156-
}
157-
158-
return window.localStorage.getItem(filterPreferencesKey) ?? fallbackValue;
159-
},
160195
onUpdate: (newValues) => {
161-
window.localStorage.setItem(filterPreferencesKey, newValues);
162-
pagination.goToPage(1);
196+
window.localStorage.setItem(workspaceFilterKey, newValues);
197+
onPageChange();
163198
},
164199
});
165200

0 commit comments

Comments
 (0)