vueuse icon indicating copy to clipboard operation
vueuse copied to clipboard

useBreakpoint and useMediaQuery cause "Hydration text mismatch" on Nuxt

Open sharifzadesina opened this issue 1 year ago • 4 comments

Describe the bug

This bug can be related to #912

It seems like useWindowSize, we need to use the tryOnMounted function also for useMediaQuery, to prevent the mismatch error.

Reproduction

https://stackblitz.com/edit/nuxt-starter-4j78pj

System Info

Doesn't matter.

Used Package Manager

npm

Validations

  • [X] Follow our Code of Conduct
  • [X] Read the Contributing Guidelines.
  • [X] Read the docs.
  • [X] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
  • [X] Make sure this is a VueUse issue and not a framework-specific issue. For example, if it's a Vue SFC related bug, it should likely be reported to https://github.com/vuejs/core instead.
  • [X] Check that this is a concrete bug. For Q&A open a GitHub Discussion.
  • [X] The provided reproduction is a minimal reproducible example of the bug.

sharifzadesina avatar Jul 16 '24 19:07 sharifzadesina

struggling with the same issue 🤕

MoeinMirkiani avatar Jul 17 '24 17:07 MoeinMirkiani

Same

soylomass avatar Aug 16 '24 21:08 soylomass

same issue

jamesray avatar Aug 29 '24 23:08 jamesray

Same issue, maybe vue.js 3.5 data-allow-mismatch can solve it, The most common case is that a class is set with useMediaQuery ref variable, so we can write code like this:

<div 
    data-allow-mismatch="class" 
    :class="isLargeScreen && 'largeScreen'"
>
     lorem...
</div>

Up to now, I've noticed that the latest version(v3.13.1) of Nuxt has already supported vue 3.5, I would like to try it later.

jaufey avatar Sep 06 '24 10:09 jaufey

For now I rolled out my own composable to deal with this, until hydration it uses a fixed screen size of 768px (because we don't get this info on server) and during hydration it will go back to using the real useBreakPoints

import { createSharedComposable, tryOnMounted, useBreakpoints as useBreakpointsVueUse } from '@vueuse/core';
import breakpoints from '~/breakpoints';
import { MaybeRefOrGetter } from '@vueuse/shared';

export const useBreakpoints = createSharedComposable(() => {
  type K = keyof breakpoints;
  const ssrSize = 768;

  const mounted = ref<boolean>(false);
  const realBreakpoints = useBreakpointsVueUse(breakpoints);
  tryOnMounted(() => (mounted.value = true));
  const getValue = (point: MaybeRefOrGetter<K>) => Number.parseInt(breakpoints[unref(point)].replace('px', ''));
  const proxyComputed = <Signature>(realMethod: Signature, fakeMethod: Signature): ReturnType<Signature> => {
    return (...args: Parameters<Signature>) => {
      const real = realMethod(...args);
      return computed(() => (mounted.value ? real.value : fakeMethod(...args)));
    };
  };
  return {
    greaterOrEqual: proxyComputed(realBreakpoints.greaterOrEqual, (k) => ssrSize >= getValue(k)),
    smallerOrEqual: proxyComputed(realBreakpoints.smallerOrEqual, (k) => ssrSize <= getValue(k)),
    greater: proxyComputed(realBreakpoints.greater, (k) => ssrSize > getValue(k)),
    smaller: proxyComputed(realBreakpoints.smaller, (k) => ssrSize < getValue(k)),
    between: proxyComputed(realBreakpoints.between, (a, b) => {
      return computed(() => ssrSize >= getValue(a) && ssrSize < getValue(b));
    }),
    isGreater(k: MaybeRefOrGetter<K>) {
      return this.greater(k).value;
    },
    isGreaterOrEqual(k: MaybeRefOrGetter<K>) {
      return this.greaterOrEqual(k).value;
    },
    isSmaller(k: MaybeRefOrGetter<K>) {
      return this.smaller(k).value;
    },
    isSmallerOrEqual(k: MaybeRefOrGetter<K>) {
      return this.smallerOrEqual(k).value;
    },
    isInBetween(a: MaybeRefOrGetter<K>, b: MaybeRefOrGetter<K>) {
      return this.between(a, b).value;
    },
    current: proxyComputed(realBreakpoints.current, () =>
      mountedObject.keys(breakpoints).filter((key) => getValue(key) >= ssrSize),
    ),
    active() {
      const bps = this.current();
      return computed(() => (bps.value.length === 0 ? '' : bps.value.at(-1)));
    },
  };
});

The problem is that vueuse has to work for both ssr and non ssr and this solution will probably cause a screen jump if the final client size is not the ssrSize, this means that for people without SSR this will also happen because it will cause a rerender onMounted, so to fix this properly in vueuse we need to be able to detect if SSR is enabled or a flag in the composable that will set mounted to true straight away

Tofandel avatar Oct 30 '24 19:10 Tofandel

I might have encountered a similar issue. I'm using Nuxt3 + VueUse's useMediaQuery, and using useMediaQuery to determine the value of a class, but even when isLargeScreen is true, the correct dynamic styling isn't applied.

This is my minimal reproduction.

judy00 avatar Dec 18 '24 10:12 judy00