Integrating Nuqs with Local Storage #832
-
Hello, I'm trying to persist filters into localstorage and next time the window reloads whatever is inside the localstorage should be applied as filters. I also wanted to sync localstorage with URL changes but I'm failing to do so. Below is my best attempt to solve the problem :D It looks like localstorage data is being overwritten endlessly this way. Is there an example implementation to this problem I'm kind of stuck import dayjs, { type Dayjs } from "dayjs";
import utc from "dayjs/plugin/utc";
import {
parseAsArrayOf,
parseAsInteger,
parseAsIsoDateTime,
parseAsString,
parseAsStringEnum,
useQueryStates,
} from "nuqs";
import { useEffect, useTransition } from "react";
import { useDebounceCallback, useLocalStorage } from "usehooks-ts";
dayjs.extend(utc);
export const useMeetingsTableParams = () => {
const [isPending, startTransition] = useTransition();
const [persistedFilters, persistFiltersToLocalStorage] = useLocalStorage<{
page: number;
search: string;
searchTarget: "meetingName" | "meetingTranscript";
participantsFilter: string[];
meetingTypeFilter: string[];
companyFilter: string[];
dealFilter: string[];
tagFilter: string[];
sourceFilter: string[];
statusFilter: string[];
dateFilter: Date[];
}>("meetingsTableFilters", {
page: 12,
search: "",
searchTarget: "meetingName" as "meetingName" | "meetingTranscript",
participantsFilter: [],
meetingTypeFilter: [],
companyFilter: [],
dealFilter: [],
tagFilter: [],
sourceFilter: [],
statusFilter: [],
dateFilter: [
dayjs().subtract(1, "month").startOf("day").toDate(),
dayjs().endOf("day").toDate(),
],
});
console.log(persistedFilters);
const [
{
page,
search,
searchTarget,
participantsFilter,
meetingTypeFilter,
companyFilter,
dealFilter,
tagFilter,
sourceFilter,
statusFilter,
dateFilter,
},
setParams,
] = useQueryStates(
{
page: parseAsInteger.withDefault(persistedFilters.page || 1),
search: parseAsString.withDefault(persistedFilters.search || ""),
searchTarget: parseAsStringEnum([
"meetingName",
"meetingTranscript",
]).withDefault(
(persistedFilters.searchTarget as
| "meetingName"
| "meetingTranscript") || "meetingName",
),
participantsFilter: parseAsArrayOf(parseAsString).withDefault(
persistedFilters.participantsFilter || [],
),
meetingTypeFilter: parseAsArrayOf(parseAsString).withDefault(
persistedFilters.meetingTypeFilter || [],
),
companyFilter: parseAsArrayOf(parseAsString).withDefault(
persistedFilters.companyFilter || [],
),
dealFilter: parseAsArrayOf(parseAsString).withDefault(
persistedFilters.dealFilter || [],
),
tagFilter: parseAsArrayOf(parseAsString).withDefault(
persistedFilters.tagFilter || [],
),
sourceFilter: parseAsArrayOf(parseAsString).withDefault(
persistedFilters.sourceFilter || [],
),
statusFilter: parseAsArrayOf(parseAsString).withDefault(
persistedFilters.statusFilter || [],
),
dateFilter: parseAsArrayOf(parseAsIsoDateTime).withDefault(
persistedFilters.dateFilter || [
dayjs().subtract(1, "month").startOf("day").toDate(),
dayjs().endOf("day").toDate(),
],
),
},
{
history: "replace",
clearOnDefault: true,
throttleMs: 500, // debounce the history push
startTransition,
urlKeys: {
search: "q",
searchTarget: "in",
participantsFilter: "participants",
meetingTypeFilter: "templates",
companyFilter: "companies",
dealFilter: "deals",
tagFilter: "tags",
sourceFilter: "sources",
statusFilter: "statuses",
dateFilter: "within",
},
},
);
// Add effect to sync URL params to localStorage
useEffect(() => {
persistFiltersToLocalStorage({
page: page,
search: search,
searchTarget: searchTarget,
participantsFilter: participantsFilter,
meetingTypeFilter: meetingTypeFilter,
companyFilter: companyFilter,
dealFilter: dealFilter,
tagFilter: tagFilter,
sourceFilter: sourceFilter,
statusFilter: statusFilter,
dateFilter: dateFilter,
});
}, [
page,
search,
searchTarget,
participantsFilter,
meetingTypeFilter,
companyFilter,
dealFilter,
tagFilter,
sourceFilter,
statusFilter,
dateFilter,
persistFiltersToLocalStorage]);
const setParamsWithPersist = (
newParams: Partial<typeof persistedFilters>,
) => {
setParams(newParams);
// persistFiltersToLocalStorage({
// ...persistedFilters,
// ...newParams,
// });
};
const setPage = (newPage: number) => {
setParamsWithPersist({ page: newPage });
};
const setSearch = useDebounceCallback((newSearch: string) => {
setParamsWithPersist({ search: newSearch });
}, 500);
const setSearchTarget = useDebounceCallback((newSearchTarget: string) => {
setParamsWithPersist({
searchTarget: newSearchTarget as "meetingName" | "meetingTranscript",
});
}, 500);
const setParticipantsFilter = (newParticipantsFilter: string[]) => {
setParamsWithPersist({ participantsFilter: newParticipantsFilter });
};
const setMeetingTypeFilter = (newMeetingTypeFilter: string[]) => {
setParamsWithPersist({ meetingTypeFilter: newMeetingTypeFilter });
};
const setCompanyFilter = (newCompanyFilter: string[]) => {
setParamsWithPersist({ companyFilter: newCompanyFilter });
};
const setDealFilter = (newDealFilter: string[]) => {
setParamsWithPersist({ dealFilter: newDealFilter });
};
const setTagFilter = (newTagFilter: string[]) => {
setParamsWithPersist({ tagFilter: newTagFilter });
};
const setSourceFilter = (newSourceFilter: string[]) => {
setParamsWithPersist({ sourceFilter: newSourceFilter });
};
const setStatusFilter = (newStatusFilter: string[]) => {
setParamsWithPersist({ statusFilter: newStatusFilter });
};
const setDateFilter = (newDateFilter: [Dayjs | null, Dayjs | null]) => {
if (!newDateFilter[0] || !newDateFilter[1]) {
setParamsWithPersist({ dateFilter: [] }); // Clear the filter if either date is null
return;
}
setParamsWithPersist({
dateFilter: [
newDateFilter[0].startOf("day").toDate(),
newDateFilter[1].endOf("day").toDate(),
],
});
};
const clearAllFilters = () => {
setParamsWithPersist({
search: "",
searchTarget: "meetingName",
participantsFilter: [],
meetingTypeFilter: [],
companyFilter: [],
dealFilter: [],
tagFilter: [],
sourceFilter: [],
statusFilter: [],
dateFilter: [
dayjs().subtract(1, "month").startOf("day").toDate(),
dayjs().endOf("day").toDate(),
],
});
};
return {
isPending,
page,
search,
setPage,
setSearch,
searchTarget,
setSearchTarget,
participantsFilter,
setParticipantsFilter,
meetingTypeFilter,
setMeetingTypeFilter,
companyFilter,
setCompanyFilter,
dealFilter,
setDealFilter,
tagFilter,
setTagFilter,
sourceFilter,
setSourceFilter,
statusFilter,
setStatusFilter,
dateFilter,
setDateFilter,
clearAllFilters,
};
}; |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
If you're thinking of localStorage as the source of truth, and only want to populate it on mount from whatever is contained in the URL, you could do that in an effect. Trying to synchronise localStorage updates back to the URL will cause issues, as localStorage is scoped to the whole browser, but URLs are scoped to a specific tab. Opening multiple tabs of the app with different URLs will cause conflicts. Also, nuqs 2.3.0 introduced loaders, which you can use in an effect to parse |
Beta Was this translation helpful? Give feedback.
If you're thinking of localStorage as the source of truth, and only want to populate it on mount from whatever is contained in the URL, you could do that in an effect.
Trying to synchronise localStorage updates back to the URL will cause issues, as localStorage is scoped to the whole browser, but URLs are scoped to a specific tab. Opening multiple tabs of the app with different URLs will cause conflicts.
Also, nuqs 2.3.0 introduced loaders, which you can use in an effect to parse
location.search
and avoid using reactive hooks (saving a few re-renders in the process).