SearchBar should use defaultValue, value or none? #826
-
So this is my first time trying nuqs. Most of the examples I saw were using the import { SearchOutlined } from '@ant-design/icons';
import { useMeetingsTableParams } from '@/hooks/useMeetingsTableParams';
import { Dropdown, Input, MenuProps } from 'antd';
import type { SearchProps } from 'antd/es/input/Search';
export default function SearchBar(props: SearchProps) {
const { search, searchTarget, setSearch, setSearchTarget } =
useMeetingsTableParams();
const items: MenuProps['items'] =
searchTarget === 'meetingName'
? [
{
key: 'search-in-transcript',
type: 'group',
label: 'Search in transcript',
children: [
{
key: 'meetingTranscript',
label: `‟${search}”`,
onClick: () => setSearchTarget('meetingTranscript'),
},
],
},
]
: [
{
key: 'search-in-meeting-name',
type: 'group',
label: 'Search in meeting name',
children: [
{
key: 'meetingName',
label: `‟${search}”`,
onClick: () => setSearchTarget('meetingName'),
},
],
},
];
const handleSearchChange = (value: string) => {
setSearch(value);
};
return (
<Dropdown
menu={{
items: search ? items : [],
}}
placement="bottom"
>
<Input
className="shadow-none w-72"
placeholder="Search meetings or in transcript"
defaultValue={search}
onChange={(e) => handleSearchChange(e.target.value)}
prefix={<SearchOutlined className="text-[#D9D9D9]" />}
{...props}
/>
</Dropdown>
);
} import dayjs, { Dayjs } from 'dayjs';
import utc from 'dayjs/plugin/utc';
import {
parseAsArrayOf,
parseAsInteger,
parseAsIsoDateTime,
parseAsString,
parseAsStringEnum,
useQueryStates,
} from 'nuqs';
import { useTransition } from 'react';
import { useDebounceCallback } from 'usehooks-ts';
dayjs.extend(utc);
export const useMeetingsTableParams = () => {
const [isPending, startTransition] = useTransition();
const [
{
page,
search,
searchTarget,
participantsFilter,
meetingTypeFilter,
companyFilter,
dealFilter,
tagFilter,
sourceFilter,
statusFilter,
dateFilter,
},
setParams,
] = useQueryStates(
{
page: parseAsInteger.withDefault(1),
search: parseAsString.withDefault(''),
searchTarget: parseAsStringEnum([
'meetingName',
'meetingTranscript',
]).withDefault('meetingName'),
participantsFilter: parseAsArrayOf(parseAsString).withDefault([]),
meetingTypeFilter: parseAsArrayOf(parseAsString).withDefault([]),
companyFilter: parseAsArrayOf(parseAsString).withDefault([]),
dealFilter: parseAsArrayOf(parseAsString).withDefault([]),
tagFilter: parseAsArrayOf(parseAsString).withDefault([]),
sourceFilter: parseAsArrayOf(parseAsString).withDefault([]),
statusFilter: parseAsArrayOf(parseAsString).withDefault([]),
dateFilter: parseAsArrayOf(parseAsIsoDateTime).withDefault([
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',
},
},
);
const setPage = (newPage: number) => {
setParams({ page: newPage });
};
const setSearch = useDebounceCallback((newSearch: string) => {
setParams({ search: newSearch });
}, 500);
const setSearchTarget = useDebounceCallback((newSearchTarget: string) => {
setParams({
searchTarget: newSearchTarget as 'meetingName' | 'meetingTranscript',
});
}, 500);
const setParticipantsFilter = (newParticipantsFilter: string[]) => {
setParams({ participantsFilter: newParticipantsFilter });
};
const setMeetingTypeFilter = (newMeetingTypeFilter: string[]) => {
setParams({ meetingTypeFilter: newMeetingTypeFilter });
};
const setCompanyFilter = (newCompanyFilter: string[]) => {
setParams({ companyFilter: newCompanyFilter });
};
const setDealFilter = (newDealFilter: string[]) => {
setParams({ dealFilter: newDealFilter });
};
const setTagFilter = (newTagFilter: string[]) => {
setParams({ tagFilter: newTagFilter });
};
const setSourceFilter = (newSourceFilter: string[]) => {
setParams({ sourceFilter: newSourceFilter });
};
const setStatusFilter = (newStatusFilter: string[]) => {
setParams({ statusFilter: newStatusFilter });
};
const setDateFilter = (newDateFilter: [Dayjs | null, Dayjs | null]) => {
if (!newDateFilter[0] || !newDateFilter[1]) {
setParams({ dateFilter: [] }); // Clear the filter if either date is null
return;
}
setParams({
dateFilter: [
newDateFilter[0].startOf('day').toDate(),
newDateFilter[1].endOf('day').toDate(),
],
});
};
const clearAllFilters = () => {
setParams({
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,
};
}; useMeetings hook uses the useQuery from tanstack to make db callss const {
search,
searchTarget,
statusFilter,
meetingTypeFilter,
companyFilter,
dealFilter,
tagFilter,
participantsFilter,
sourceFilter,
dateFilter,
} = useMeetingsTableParams();
const { data, isLoading, pagination, handleTableChange } = useMeetings({
fromDate: dateFilter[0] && dayjs(dateFilter[0]).utc().format('YYYY-MM-DD'),
toDate: dateFilter[1] && dayjs(dateFilter[1]).utc().format('YYYY-MM-DD'),
searchTerm: search,
searchTarget,
types: meetingTypeFilter,
statuses: statusFilter,
companies: companyFilter,
deals: dealFilter,
tags: tagFilter,
owners: participantsFilter,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sources: sourceFilter as any,
}); |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
I think I see the issue: by debouncing the state updater functions, and using a controlled input (value & onChange), you do end up with only the last character pressed. You would get the same behaviour from useState. nuqs has support for throttling, which works a bit differently (emit the first character immediately, then batch updates every Built-in debouncing support is planned (see #291, #373, #449), and should hopefully land some time early in the new year. In the mean time, you could:
The one caveat with this approach is that if the search component is mounted and another part of the UI updates the associated search param, the input won't reflect it (uncontrolled components aren't reactive). Hope that helps, let me know if you need anything else. |
Beta Was this translation helpful? Give feedback.
I think I see the issue: by debouncing the state updater functions, and using a controlled input (value & onChange), you do end up with only the last character pressed. You would get the same behaviour from useState.
nuqs has support for throttling, which works a bit differently (emit the first character immediately, then batch updates every
throttleMs
). This gives you consistent updates, but for search you might want eventual updates, when the user stops typing for a while, which is what debouncing does.Built-in debouncing support is planned (see #291, #373, #449), and should hopefully land some time early in the new year.
In the mean time, you could: