47ng/nuqs

`withDefault()` behavior when reset

Opened this issue · 9 comments

Context

What's your version of nuqs?

"nuqs": "^1.15.2"

Next.js information (obtained by running next info):


Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.3.0: Wed Dec 20 21:30:27 PST 2023; root:xnu-10002.81.5~7/RELEASE_ARM64_T8103
Binaries:
  Node: 18.16.0
  npm: 10.2.3
  Yarn: 1.22.21
  pnpm: 8.14.0
Relevant Packages:
  next: 13.5.6
  eslint-config-next: 13.5.6
  react: 18.2.0
  react-dom: 18.2.0
  typescript: 5.3.3
Next.js Config:
  output: N/A

Are you using:

  • ✅ The app router
  • ❌ The pages router
  • ❌ The basePath option in your Next.js config
  • ❌ The experimental windowHistorySupport flag in your Next.js config

Description

Thank you for the great library. I love this library because it's really easy to use.
This is the kind of feature request.
When using withDefault, the default value is used when query value set to null.
But I think it's useful the library has the concept similar with initialState of useState.
The initial state is set at the initial mount and it can be clear with null.

Maybe something like this:

useQueryStates({
  q: parseAsString.withDefault('hoge', { resetToNull: true }),
})

How do you think?

I'm not sure I follow your idea. In the case of useState, the initial state is used on mount and then lives on its own, but here the state is derived from the URL as the source of truth. The default value is only relevant when there is no query string with the given key in the URL, or it can't be parsed to a valid state value.

Setting the state to null (or to the default value when using clearOnDefault) is an indication to remove the query key from the URL, and from there the hook will behave as usual for a missing query key (return defaultValue ?? null).

Could you show me an example on how you would use such a feature?

@franky47 Thank you for your reply. I understood.

I use nuqs mainly to persist table filter state with tanstack/table and want to set initial filter which is resettable.
I was suffering from "resettable" with withDefault, but while writing this reply, I thought it would be good to simply set the URL in the sidebar, etc., with query parameters attached without using withDefault.

I found this issue for the same reason described by @arwtyxouymz.
What makes withDefault a bit "unnatural", when you are coming from useState, is that the type of the value cannot be null

Example:

export const dateRanges = z.enum([
    "last_month",
    "month_before_last",
    "current_month",
  ]);

const [selectValue, setSelectValue] = useQueryState(
    "selected_value",
    parseAsStringEnum(dateRanges.options)
  );
  // const selectValue: "last_month" | "month_before_last" | "current_month" | null

  
const [selectValueWithDefault, setSelectValueWithDefault] = useQueryState(
   "selected_value_with_default",
    parseAsStringEnum(dateRanges.options).withDefault("last_month")
  );
  // const selectValueWithDefault: "last_month" | "month_before_last" | "current_month"

It would be awesome to have the option to set an initial value together with the option to set this value to null afterwards.

What makes withDefault a bit "unnatural", when you are coming from useState, is that the type of the value cannot be null.

@matheins The default value differs from an initial state, it's what should be returned when the URL does not provide a valid state value. If you're ok with a null type to represent this absence of state in the URL, then you don't need to specify a default value. If however you need a non-nullable type at all times, then the default value is here to ensure that.

It would be awesome to have the option to set an initial value together with the option to set this value to null afterwards.

Could you show me a code example on how you'd use this?

@franky47

Could you show me a code example on how you'd use this?

Although my problem was resolved by adding query params to link href,
I thought it's useful if the parser has an option like .withInitilalValue().
This is the example usage of this option.

// Initially we can show filtered result to users
export const useStatusFilter = () => {
  const [statusFilterValue, setStatusFilterValue] = useQueryState(
    'status',
    parseAsStringEnum(['NotStarted', 'InProgress', 'Completed']).withInitialValue("NotStarted")
  );

  // filter can be cleared
  const clearFilter = () => setStatus(null);


  return {
    statusFilterValue,
    setStatusFilterValue,
    clearFilter
  }
}

Although my problem was resolved by adding query params to link href

I think you came to the right conclusion by yourself. The reason an initial value like in useState does not work here is that the URL is the source of truth from which the state value will be derived. Introducing a branch from that source of truth by allowing an initial value potentially different than what the URL would provide makes the state harder to reason about IMHO.

I like the idea of @arwtyxouymz. Using withInitialValue would be really comfortable.
When I got you right, @franky47, the workaround is setting the initial value via useSearchParams together with a useEffect OR I need to make sure every link, linking to this page, sets the search-param correctly.
Imo it would still make a lot of sense to abstract this, to make it more handy.

"nuqs": "^1.17.0"
Hi, there's a similar problem with "withDefault()" option when the value is default actually.
(If this question is different from yours, I can start a separate issue post.)

const [tab, setTab] = useQueryState( 'tab', parseAsStringLiteral(TabType).withDefault('default') )

if the value of "tab" param in URL is not match the TabType, the value of tab will be set as 'default', but the tab query in URL is still the wrong one.
For example, /page?tab=wrong, the "wrong" is not match TabType, the tab state value will be set as 'default', and it's actually work.
As the same time, the URL was expected to become /page?tab=default, but there's nothing change.

I don't think nuqs has this feature, so i've tried use withOptions({clearOnDefault: true}) to avoid the wrong url displayed, but the tab param is still in URL.

It seem to be similar to my use case

When I try to use parseAsIsoDateTime withDefault option, I can't clear the date since it will be fallback to the lastMonth and endOfToday. What I want it just remove these searchParams or at least set the startDate and endDate to null

Whether I use clearOnDefault it still be the same or maybe I use it the wrong. please correct me!

const [date, setDate] = useQueryStates(
{
	startDate: searchParams.startDate,
	endDate: searchParams.endDate,
	page: searchParams.page,
},
{
	startTransition,
	shallow: false,
	clearOnDefault: true,
}
)
	
const searchParams = {
 startDate: parseAsIsoDateTime.withDefault(lastMonth),
 endDate: parseAsIsoDateTime.withDefault(endOfToday),
}

For more information, they are using with nuqs/server. And for now, I overwrite this pattern by remove withDefault and use the default value with useEffect on the client mount instead.

const [date, setDate] = useQueryStates(
{
	startDate: searchParams.startDate,
	endDate: searchParams.endDate,
	page: searchParams.page,
},
{
	startTransition,
	shallow: false,
}
)
	
const searchParams = {
 startDate: parseAsIsoDateTime,
 endDate: parseAsIsoDateTime
}

// biome-ignore lint/correctness/useExhaustiveDependencies: Set effect on mount since we only want to set the initial date
	React.useEffect(() => {
		setDate({
			startDate: lastMonth,
			endDate: endOfToday(),
		})
	}, [])