Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL state not available via getters #400

Closed
tordans opened this issue Nov 16, 2023 · 9 comments
Closed

URL state not available via getters #400

tordans opened this issue Nov 16, 2023 · 9 comments
Labels
bug Something isn't working

Comments

@tordans
Copy link
Contributor

tordans commented Nov 16, 2023

This ticket is now about #400 (comment) by damian-balas.

(I moved my report to #404.)

@tordans tordans added the bug Something isn't working label Nov 16, 2023
@tordans tordans changed the title Localstorage Debugging Flag seems to not be working URL state not available to getters Nov 16, 2023
@tordans tordans changed the title URL state not available to getters URL state not available via getters Nov 16, 2023
@franky47
Copy link
Member

franky47 commented Nov 16, 2023

Thanks for the detailed report. I'll have a look later, but at first glance, it looks like your initialisation performs side-effects in the render function, which is not recommended by React.

There are a few other things to consider:

  • In development, React will always render twice, due to Strict mode.
  • Updates to the URL are not synchronous, so they may occur after the second render, which could explain the discrepancies you observe.

@tordans

This comment was marked as outdated.

@damian-balas
Copy link

@franky47 I've found a weird behavior. I won't go into details until you say it's a real issue.

The case:

  1. I update in ComponentA the query with setter from
const useMenuQueryState = () =>
  useQueryState('menu', parseAsBoolean.withDefault(false));

like this: setIsMenuOpen(true)
2. In ComponentB I use a hook from next: useSearchParams
3. In ComponentB i use this code on button click / interval / useEffect etc.:

console.log(
  new URLSearchParams(Array.from(searchParams.entries())).toString()
)

The logged query doesn't change even though I see the change being applied by your library in the address bar.

Questions:

  1. Am I doing something wrong?
  2. Should I give you more info?
  3. Is there a possibility to get all query data with your library that would equal this:
new URLSearchParams(Array.from(searchParams.entries())).toString(),

Thanks!

@franky47
Copy link
Member

franky47 commented Nov 16, 2023

@damian-balas in stock1 Next.js, useSearchParams is not reactive to shallow URL updates. Since the setters change the URL in a shallow manner by default (ie: without going through the Next.js router to avoid sending network requests), it is not picked up by useSearchParams.

Edit: if you want to be notified of URL updates, you can use the subscribeToQueryUpdates function:

import { subscribeToQueryUpdates } from 'next-usequerystate'

React.useLayoutEffect(
  () => subscribeToQueryUpdates(({ search }) => console.log(search.toString()),
  []
)

Footnotes

  1. As of next@14.0.3-canary.6, the experimental windowHistorySupport flag changes this behaviour and makes useSearchParams reactive to shallow URL changes. However, doing so will still lag behind the state update, because of the internal throttling of URL updates. To get an update in the next render (like you would in useState), use a query getter: useMenuQueryState()[0].

@damian-balas
Copy link

@damian-balas in stock1 Next.js, useSearchParams is not reactive to shallow URL updates. Since the setters change the URL in a shallow manner by default (ie: without going through the Next.js router to avoid sending network requests), it is not picked up by useSearchParams.

Edit: if you want to be notified of URL updates, you can use the subscribeToQueryUpdates function:

import { subscribeToQueryUpdates } from 'next-usequerystate'

React.useLayoutEffect(
  () => subscribeToQueryUpdates(({ search }) => console.log(search.toString()),
  []
)

Footnotes

  1. As of next@14.0.3-canary.6, the experimental windowHistorySupport flag changes this behaviour and makes useSearchParams reactive to shallow URL changes. However, doing so will still lag behind the state update, because of the internal throttling of URL updates. To get an update in the next render (like you would in useState), use a query getter: useMenuQueryState()[0].

Thanks :)
I've implemented "my own" version of useSearchParams as an substitute.
This is what I came up with:

const useSubscribedSearchParams = () => {
  const _searchParams = useSearchParams();
  const [searchParams, setSearchParams] =
    useState<URLSearchParams>(_searchParams);

  useLayoutEffect(
    () =>
      subscribeToQueryUpdates(({ search }) => {
        setSearchParams(search);
      }),
    // * This returns an unsubscribe function
    [],
  );

  return searchParams;
};

It works well :)

My last question is... how should I actually handle changing dynamic queries?

Example:

  1. Get current search query
  2. Add dynamic key that comes via props
  3. Update the search query

How would you handle that case?

Help much appreciated! :)

@franky47
Copy link
Member

franky47 commented Nov 16, 2023

I would use a useQueryState to update the URL, that's what it's intended to do. If the key is not known ahead of time, or there can be an arbitrary number of them, you can use useQueryStates to go around the rules of hooks:

function DynamicQuery({ keys }: { keys: string[] }) {
  const [values, update] = useQueryStates(
    // Note: might want to memoize this
    keys.reduce((obj, key) => ({
      ...obj,
      [key]: parseAsString
    }), {})
  )

@oliverstreissi
Copy link

I would use a useQueryState to update the URL, that's what it's intended to do. If the key is not known ahead of time, or there can be an arbitrary number of them, you can use useQueryStates to go around the rules of hooks:

function DynamicQuery({ keys }: { keys: string[] }) {
  const [values, update] = useQueryStates(
    // Note: might want to memoize this
    keys.reduce((obj, key) => ({
      ...obj,
      [key]: parseAsString
    }), {})
  )

Thanks for the example, but how can I type this properly, since values in this case is of type {} and using useQueryState<Record<string, string>>(...) gives me Property 'parse' is missing in type 'string' but required in type 'Parser<any>'

@franky47
Copy link
Member

franky47 commented Feb 6, 2024

@oliverstreissi this should do it (note however that this is a bit of a hack):

import { Parser, parseAsString, useQueryStates } from 'nuqs'

const [values, update] = useQueryStates(
  // Note: might want to memoize this
  keys.reduce(
    (obj, key) => ({
      ...obj,
      [key]: parseAsString
    }),
    {} as Record<string, Parser<string>>
  )
)

If using default values, you'll need to be explicit about the default value existing on the reduce initial object:

const [values, update] = useQueryStates(
  // Note: might want to memoize this
  keys.reduce(
    (obj, key) => ({
      ...obj,
      [key]: parseAsString.withDefault('')
    }),
    {} as Record<string, Parser<string> & { defaultValue: string }>
  )
)

@tordans
Copy link
Contributor Author

tordans commented Feb 14, 2024

@damian-balas I am under the impression that this is solved so I will close it. I can reopen if needed, of course.

@tordans tordans closed this as completed Feb 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants