-
-
Notifications
You must be signed in to change notification settings - Fork 122
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
Server client html mismatch occurs on first render #290
Comments
There are some other drawbacks on the above code, like const [queryState, setQueryState] = useQueryState(...);
const isFirstRender = useRef(true);
useEffect(() => isFirstRender.current = false, []);
const state = isFirstRender.current ? defaultValue : queryState; |
Any way to handle this when you have multiple queries? |
Is this fixed for server side rendering yet? I would love to use this package instead of getting query params with |
Unfortunately the Next.js router does not expose querystring params in the render tree in SSR, so those should be treated as client-side only (just like you would for code that depends on local storage values for example). While theoretically it could be possible to get this kind of information on "per request" SSR (ie: page with |
I think the correct solution would be to always return the default value on both server and client-side on the initial render. Reading from |
Isn't this a major issue? This means this boilerplate has to be used by everyone, or am I missing something? |
I'm running into this as well |
Got hit by this as well. FWIW - we are using this wrapper hook as a workaround. It's based on @doxylee's comment above. function useSsrSafeQueryState<T>(
key: string,
options: UseQueryStateOptions<T> & {
defaultValue: T;
}
) {
const [queryState, setQueryState] = useQueryState<T>(key, options);
const isFirstRender = useRef(true);
useEffect(() => {
isFirstRender.current = false;
}, []);
const state = isFirstRender.current ? options.defaultValue : queryState;
return [state, setQueryState] as [T, typeof setQueryState];
} ☮️ |
Expanding on @joelso's comment above, here I'm including all of import { useEffect, useRef } from 'react'
import {
HistoryOptions,
queryTypes,
useQueryState as _useQueryState,
UseQueryStateOptions,
UseQueryStateReturn
} from 'next-usequerystate'
function useQueryState<T>(
key: string,
options: UseQueryStateOptions<T> & { defaultValue: T }
): UseQueryStateReturn<NonNullable<ReturnType<typeof options.parse>>, typeof options.defaultValue>
function useQueryState<T>(
key: string,
options: UseQueryStateOptions<T>
): UseQueryStateReturn<NonNullable<ReturnType<typeof options.parse>>, undefined>
function useQueryState(
key: string,
options: {
history?: HistoryOptions
defaultValue: string
}
): UseQueryStateReturn<string, typeof options.defaultValue>
function useQueryState(
key: string,
options: Pick<UseQueryStateOptions<string>, 'history'>
): UseQueryStateReturn<string, undefined>
function useQueryState(key: string): UseQueryStateReturn<string, undefined>
function useQueryState<T = string>(
key: string,
options: Partial<UseQueryStateOptions<T>> & { defaultValue?: T } = {
history: 'replace',
parse: (x) => x as unknown as T,
serialize: String,
defaultValue: undefined
}
) {
const [queryState, setQueryState] = _useQueryState(key, options)
const isFirstRender = useRef(true)
useEffect(() => {
isFirstRender.current = false
}, [])
const state = isFirstRender.current ? options.defaultValue : queryState
return [state, setQueryState] as [T, typeof setQueryState]
}
export { useQueryState, queryTypes } Then after a find and replace of import { useQueryState, queryTypes } from 'next-usequerystate' with import { useQueryState, queryTypes } from '@/lib/useQueryState' it works everywhere with SSR |
Nice @drichar |
Ran into this problem today and found the solutions above fixed one issue but introduced another:
The only solution I could find was to wrap the whole component in a parent component which would conditionally render if const ProblemComponent = () => {
const [productIds, setProductIds] = useQueryState(
'productIds',
queryTypes.array(queryTypes.string).withDefault([]),
)
...
return (
<ProductList productIds={productIds} />
)
}
export default function RouterSafeProblemComponent() {
const isReady = useRouterReady()
return isReady ? <ProblemComponent /> : null
} Where the hook export const useRouterReady = () => {
const [isReady, setIsReady] = useState(false)
const router = useRouter()
useEffect(() => {
setIsReady(router.isReady)
}, [router.isReady])
return isReady
} |
import { useEffect, useState } from "react";
import { useQueryState, type UseQueryStateOptions } from "next-usequerystate";
export const useSsrSafeQueryState = <T,>(
key: string,
options: UseQueryStateOptions<T> & { defaultValue: T }
) => {
const [queryState, setQueryState] = useQueryState<T>(key, options);
const [isFirstRender, setIsFirstRender] = useState<boolean>(true);
useEffect(() => {
setIsFirstRender(false);
}, []);
const state = isFirstRender ? options.defaultValue : queryState;
return [state, setQueryState] as [T, typeof setQueryState];
}; Had to switch from |
I have the same issue. Is there not going to be a fix for this? |
same. |
Unfortunately the only way to solve this issue properly is the following suggested above by @camin-mccluskey:
Why is that ?
First, you need to understand hydration, and how Next.js behaves differently on initial page load (or page reloads, same thing) vs on client-side navigation. Page (re)load involves getting the HTML that was generated at build time. It was done so without any search params, so the default value (or On the other side, navigating client-side (using a Why can't we use the default value on the hydration render, then switch to the correct value ?This causes two renders, one that uses an incorrect value (the default), to make hydration happy, then a second with the correct search param value. Flickering of UI being bad UX aside, the root of the hydration error is: the content generated at build time is incorrect for the current URL. Rendering twice allows that incorrect content to creep up in the application, only to be replaced. The alternative of not rendering the parent component (which we'd call a client component in the app router, and wrap around a Suspense boundary) is both more correct and more efficient. Why does SSR solve the problem ?SSRd pages are never pre-rendered at build time, and are rendered per-request. Those requests do contain the search params, so I want to keep a static page, what's the solution ?Search params are runtime variables that can't be accessed at build time, much like the contents of cookies or localStorage, for which you would also need to fence off parts of the static render tree that depends on them. Fortunately, the app router isn't affected by this issue, as it uses a different set of behaviours for initial page load and client-side navigation. But I can understand that upgrading is not a valid option for some. |
Would it be possible to apply this logic at |
That is a good question, and also: would this create performance issues, like the app having to rerender everything multiple in different scenarios because the query params get changed while using the app? |
Technically, yes. It would essentially prevent any page from being pre-rendered at build time, and be rendered on the client only. However you'd lose a lot of perks of using Next.js while doing so (SEO and First Contentful Paint being the most relevant). Just like the app router paradigm suggests moving client component boundaries as low in the tree as possible, this pattern of fencing off client-only code in the pages router should follow the same guidelines.
No, the initial load would perform an empty hydration step on the empty shell generated at build time, then the second render would kick in and render the actual contents. Since the hook approach doesn't re-render subsequently, there should be no more re-renders on client side navigation. |
@franky47 I seem to be running into this issue, or something quite similar again. This time the snippet above ( Basically I can't navigate from a page where The precise issue an aborted fetch for the new route. If I hammer the link button I can eventually get through to the next route. Happy to create a new issue but it felt quite similar to this one. |
@camin-mccluskey that's very odd, yes please open a dedicated issue with the details, I'll try and reproduce it locally. |
Actually looks like I was able to "solve" it by increasing the |
Warnings like
Warning: Expected server HTML to contain a matching <button> in <div>.
occurs because html was rendered with no url params on server side, but rendered with url params in client side.I think Next.js handles this problem by setting router.query to an empty object on the first render, and sets client url params on the next render. But because useUrlState uses window.location.search and not router, this warning occurs.
This can be avoided by using code like this:
But it would be better if this boilerplate could be avoided in some way.
The text was updated successfully, but these errors were encountered: