-
Note This discussion turned into a kind of sounding board / raw brain dump. Sorry about the mess! 😅 Context: discussion started with Pierre Spring on Mastodon. After merging #328 which adds support for the app router in Next.js 13+, some performance issues appear there. Any high-frequency updates (eg: binding a query state to a text input and typing characters, or to a slider and moving it) cause a lot of lag on the UI. This requires some further investigation, but it opens the question of how this hook is being used in the wild. For UIs where the URL is not really the source of truth, but a way to share state with others, or keep it in case of reloads, a throttling approach would make sense. The internal state would be the source of truth, and periodically be persisted in the URL. On mount, it would read the initial state value from the URL, but not react to changes. |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments 24 replies
-
An initial test shows that the app router now performs a network request every time there is a querystring change. app-router-lag.movIt kind of makes sense to let server components handle it, which wasn't the case in the pages router (only pathname changes triggered server-side data fetching code). Unfortunately, it makes the hook unusable as-is (dependency on network can be exacerbated with throttling network requests in devtools). Any attempt at throttling or debouncing won't really solve the problem, as it is now dependent on network conditions. In the pages router, the hook could even work offline for a truly client-first experience, which won't be possible now. Some alternatives to consider:
Related Next.js issues: |
Beta Was this translation helpful? Give feedback.
-
After testing a lot of potential ways to fix this, the following comment helped gain some reactivity. Using the HTML history API allows changing the URL without triggering navigation (ie: without hitting the server and re-rendering and whatnot). It's ideal for client-only updates, when paired with an internal state per hook and a way to keep them all in sync (either via some event listener on history changes, or via a dedicated event emitter). There are a couple of issues with this approach though (similar to what we had in the pages router):
Edit: The one issue left is that of syncing the internal state of all hooks with the same key. The idea would be that the URL be the source of truth, and the individual internal states would sync up to it. This is wasteful as it requires re-parsing for every hook instance. Instead, an event emitter pattern can be used to sync the parsed states directly, and have only the writer hook update the URL. Some potential issues with this approach include:
Another issue is that the history API is rate-limited by browsers, so maybe some form of throttling URL updates would be welcome. The UI would be just as reactive as we'd still be syncing the states as fast as possible. |
Beta Was this translation helpful? Give feedback.
-
2023-09-04 update: while trying to solve a limitation of previous versions where a high volume of query updates would trigger a security error in browsers, I ended up removing another limitation: batched updates for useQueryState. In previous versions using the pages router, updates to a query had to be Since we're no longer bound to the Next.js router for URL updates, nor to the URL for the absolute source of truth, we can put a queue between the hook and actual updates to the URL, that gets throttled to avoid security errors and that runs one tick after the React event loop to allow batching. 🎉 This makes the whole point of maintaining two implementations (useQueryState for a single query key and useQueryStates for multiple related queries) kind of pointless: use one hook per query, update them in the same tick and they'll move together. Since React also collects and queues state updates within the same tick before triggering a render, it should also mean a single render pass for multiple query updates (TBC).
So I'm leaning into fitting it all in a v2.0.0 with a breaking change, that includes the following:
For shallow mode, and especially in server components that can now access the query string server-side, it may be useful to have a way to commit a query update to the server for remote processing. With strategies like a deferred update queue, that may be a problem as the URL/history entry won't be available right after a call to the hook's update method. so maybe it should still be something to propose as a param, eg: const [text, setText] = useQueryString('text')
const onSubmit = () => {
setText('foo', { server: true })
} For other purposes, accessing the updated URL can be done with the same deferring technique: const [a, setA = useQueryString('a')
const [b, setB = useQueryString('b')
const onSubmit = () => {
setA('foo')
setB('bar')
console.log(window.location.search) // ''
setTimeout(() => console.log(window.location.search), 0) // ?a=foo&b=bar
} We could also keep the Promise-based API by awaiting the next tick, so that async code can still work as it used to. |
Beta Was this translation helpful? Give feedback.
-
🚀 PSA:
Initial tests are very promising, performance is identical to what we could have with standard Note that for the pages router, under Next.js 13.4+ it's possible to use the app router from 'next/navigation', so importing
I'll test the compatibility with the pages router a bit more, and if things look good I'll expose only a single implementation for both. In any case, this will end up as a breaking change for various reasons (behavioural and API), so it will eventually end up in a v2.0.0. |
Beta Was this translation helpful? Give feedback.
-
I'm trying out the beta branch (1.8.0-beta.9) but finding that hydration is not working on page navigated to via link as in: function Page1() {
return <Link href={`/page2?foo=bar`}>Page2</Link>;
}
function Page2() {
const [foo] = useQueryState("foo");
return <p>{foo}</p>; // starts and stays null (not hydrated) until page refreshed
} The Is this expected? (only tested in app folder). |
Beta Was this translation helpful? Give feedback.
-
Another possible issue, perhaps related to the change to parsers. It seems like const [foo, setFoo] = useQueryState("foo", parseAsJson<Object>>());
setFoo({}) // expecting: foo=%7B%7D, current actual: foo=%257B%257D |
Beta Was this translation helpful? Give feedback.
-
Not sure if this is right place to say this, but something is not working right in latest version of next (14.0.2-canary.8 as of moment). Now it completely resets state multiple times. First time immediately after setting state, subsequent times on page scroll. It resets url to default (removes search query) and thus components rerender to initial state too. |
Beta Was this translation helpful? Give feedback.
🚀 PSA:
1.8.0-beta.6
has been published, and includes (for the app router only, no changes to the pages router yet):<Link>
and imperative calls to the Next.js routeruseQueryState
hooks and the same keys aggr…