nuqs icon indicating copy to clipboard operation
nuqs copied to clipboard

Remix - search params should not update before navigation completed

Open overshom opened this issue 9 months ago • 6 comments

Context

What's your version of nuqs?

    "nuqs": "2.4.1",

What framework are you using?

  • ✅ Remix

Which version of your framework are you using?

    "@remix-run/react": "^2.9.2",

Description

Search params are updated before navigation completed which causes irrelevant renders and effects which should not happen.

Expected behaviour - search params should update while navigation not complete.

Reproduction

Bug visible for a fraction of a second, but still very annoying and may cause bigger issues if useEffect relies on param value change.

Imagine this case:

  • current search param ?category=all
  • click on navigation link to somewhere, zero search params, page not rendered yet
    • 🔴 bug here because old page still rendered, and search params from nuqs are already updated to their empty or default values
  • navigation complete, page rendered
  // use simple param
  const [immediateCategory, setCategory] = useQueryState(
    'category',
    parseAsString.withDefault('all'),
  );

  console.log(immediateCategory); // 🔴 see value resets to default when navigation started and page still mounted

  // step one - switch to `?category=red`
  // click link to navigate somewhere
  // 🔴 BUG - SomeLargeLayoutShiftComponent rendered because params updated to default value even though user did not navigate to "all" category, he just navigated to other page that has no search params at all
  return <div>
    {immediateCategory === 'all' && <SomeLargeLayoutShiftComponent />}
    <Link to="/some-page">some page</Link>
  </div>;

overshom avatar Mar 22 '25 14:03 overshom

As a workaround, I created another hook to not update value until remix navigation completed.

import { useNavigation } from '@remix-run/react';

const useValueAfterNavigation = <T,>(value: T) => {
  const navigation = useNavigation();
  const ref = useRef(value);
  if (navigation.state === 'idle') {
    ref.current = value;
  }
  return ref.current;
};

And then use it like here:

  const [immediateCategory, setCategory] = useQueryState(
    'category',
    parseAsString.withDefault('all'),
  );
  const category = useValueAfterNavigation(immediateCategory);

This way category value does not jump between navigations and works fine when search param is updated.

overshom avatar Mar 22 '25 14:03 overshom

Facing the same problem. I have sidebar which opens/closes too fast.

https://github.com/user-attachments/assets/bcd70883-cae7-42de-be69-b025d0abfdd1

rlesniak avatar May 08 '25 12:05 rlesniak

This problem also occurs in Next.js 🥲

luckyabner avatar Aug 07 '25 06:08 luckyabner

@luckyabner do you have a minimal reproduction example in Next.js?

Also, feel free to try the latest beta (2.5.0-beta.2), it changed a few things related to navigation sync which may impact (but not necessarily resolve) this issue.

franky47 avatar Aug 07 '25 06:08 franky47

@franky47 I'm not sure if this is the same issue, but I have a special requirement: after clicking a button, I want to open a modal and change the URL without navigating to a new page. When I'm on the /?tab=Tab3 page and click "detail", a dialog opens and the URL changes to /detail. At this point, the original page's tab changes to Tab1 (which I understand is because it's set as the default value). However, when I close the modal (by calling history.back()), the tab first changes to Tab1 and then to Tab3 — whereas the expected behavior is for it to directly return to Tab3. I tried upgrading to the beta version, but it still doesn't work. Here is the minimal reproduction example: https://github.com/luckyabner/nuqs-problem-demo I'd really appreciate any guidance or workaround you could suggest. Thanks again for your work on this project!

https://github.com/user-attachments/assets/6b2f0508-d901-439c-94ef-f86493303aa4

luckyabner avatar Aug 07 '25 08:08 luckyabner

I'm seeing this behavior with ReactRouter7 as well:

const TestComponent = () => {
  const location = useLocation()
  const [queryParam, setQueryParam] = useQueryState('param')

  console.log({ queryParam })
  console.log({ location })

  useEffect(() => {
    return () => {
      console.log('unmounting')
    }
  }, [])
  return null
}

When navigating away from the page (using RR7 Link to=), the component first logs an empty queryParam while still logging the same location.pathname and location.search, followed by the unmounting message

dreyks avatar Oct 14 '25 08:10 dreyks