Skip to content

Commit b06d772

Browse files
committed
refactor: Centralize stable useSearchParams
1 parent 553ce89 commit b06d772

File tree

2 files changed

+22
-11
lines changed

2 files changed

+22
-11
lines changed

site/src/components/Filter/filter.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import Divider from "@mui/material/Divider";
2626
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
2727

2828
import { useDebouncedFunction } from "hooks/debounce";
29-
import { useEffectEvent } from "hooks/hookPolyfills";
3029

3130
export type PresetFilter = {
3231
name: string;
@@ -48,17 +47,18 @@ export const useFilter = ({
4847
onUpdate,
4948
searchParamsResult,
5049
}: UseFilterConfig) => {
50+
const [searchParams, setSearchParams] = searchParamsResult;
51+
5152
// Fully expect the initialValue functions to have some impurity (e.g. reading
5253
// from localStorage during a render path). (Ab)using useState's lazy
5354
// initialization mode to guarantee impurities only exist on mount. Pattern
5455
// has added benefit of locking down initialValue and ignoring any accidental
5556
// value changes on re-renders
5657
const [readonlyInitialQueryState] = useState(initialValue);
5758

58-
// React Router doesn't give setSearchParams a stable memory reference; need
59-
// extra logic to prevent on-mount effect from running too often
60-
const [searchParams, setSearchParams] = searchParamsResult;
61-
const syncSearchParamsOnMount = useEffectEvent(() => {
59+
// Sync the params with the value provided via the initialValue function;
60+
// should behave only as an on-mount effect
61+
useEffect(() => {
6262
setSearchParams((current) => {
6363
const currentFilter = current.get(useFilterParamsKey);
6464
if (currentFilter !== readonlyInitialQueryState) {
@@ -67,11 +67,7 @@ export const useFilter = ({
6767

6868
return current;
6969
});
70-
});
71-
72-
useEffect(() => {
73-
syncSearchParamsOnMount();
74-
}, [syncSearchParamsOnMount]);
70+
}, [setSearchParams, readonlyInitialQueryState]);
7571

7672
const update = (newValues: string | FilterValues) => {
7773
const serialized =

site/src/pages/WorkspacesPage/WorkspacesPage.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,28 @@ import { MONOSPACE_FONT_FAMILY } from "theme/constants";
2121
import TextField from "@mui/material/TextField";
2222
import { displayError } from "components/GlobalSnackbar/utils";
2323
import { getErrorMessage } from "api/errors";
24+
import { useEffectEvent } from "hooks/hookPolyfills";
25+
26+
function useSafeSearchParams() {
27+
// Have to wrap setSearchParams because React Router doesn't make sure that
28+
// the function's memory reference stays stable on each render, even though
29+
// its logic never changes, and it even has function update support
30+
const [searchParams, setSearchParams] = useSearchParams();
31+
const stableSetSearchParams = useEffectEvent(setSearchParams);
32+
33+
// Need this to be a tuple type, but can't use "as const", because that would
34+
// make the whole array readonly and cause type mismatches
35+
return [searchParams, stableSetSearchParams] as ReturnType<
36+
typeof useSearchParams
37+
>;
38+
}
2439

2540
const WorkspacesPage: FC = () => {
2641
const [dormantWorkspaces, setDormantWorkspaces] = useState<Workspace[]>([]);
2742
// If we use a useSearchParams for each hook, the values will not be in sync.
2843
// So we have to use a single one, centralizing the values, and pass it to
2944
// each hook.
30-
const searchParamsResult = useSearchParams();
45+
const searchParamsResult = useSafeSearchParams();
3146
const pagination = usePagination({ searchParamsResult });
3247
const filterProps = useWorkspacesFilter({ searchParamsResult, pagination });
3348
const { data, error, queryKey, refetch } = useWorkspacesData({

0 commit comments

Comments
 (0)