Remix - search params should not update before navigation completed
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
nuqsare already updated to their empty or default values
- 🔴 bug here because old page still rendered, and search params from
- 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>;
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.
Facing the same problem. I have sidebar which opens/closes too fast.
https://github.com/user-attachments/assets/bcd70883-cae7-42de-be69-b025d0abfdd1
This problem also occurs in Next.js 🥲
@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 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
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