Skip to content
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

Directive basePixels hidration mismatch with React and SSR #751

Open
HriBB opened this issue Oct 2, 2024 · 1 comment
Open

Directive basePixels hidration mismatch with React and SSR #751

HriBB opened this issue Oct 2, 2024 · 1 comment

Comments

@HriBB
Copy link

HriBB commented Oct 2, 2024

First of all, thanks for this great package. I was able to generate a fully reponsive picture with only a few lines of code, which is amazing! To the problem

I am trying to use the basePixels directive with React, but it does not work as expected. When rendered on the server, I get the correct 1x and 2x images, but when it renders on the client, it switches to 300w and 600w versions, and I get a hydration mismatch.

Packages

  • vite-imagetools 7.0.4
  • React canary 19.0.0-rc-1eaccd82-20240816
  • Remix 2.12.1
  • Vite 5.1.0

This is my component:

import square2x from '~/image/services.jpg?w=300;600&basePixels=300&format=avif;webp;jpeg&as=picture'

console.log(square2x)

const formats = ['avif', 'webp', 'jpeg']

export default function ViteImageTools() {
  return (
    <main>
      <h1>vite-imagetool</h1>
      <div className="my-6 w-[300px]">
        <picture>
          {formats.map((f) => (
            <source key={f} srcSet={square2x.sources[f]} type={`image/${f}`} />
          ))}
          <img
            src={square2x.img.src}
            alt="Testing vite-imagetools"
            width={square2x.img.w}
            height={square2x.img.h}
          />
        </picture>
      </div>
    </main>
  )
}

This is the output in the linux console (SSR)

{
  sources: {
    avif: '/@imagetools/6b96b93013bb7a72f4ff6f967dbd76974b92bfbc 1x, /@imagetools/16bf4d6638f522afb34cd2b0322ce3629a686963 2x',
    webp: '/@imagetools/a9ce3b8e03e646d59be2173e1e8a5ac7b4757714 1x, /@imagetools/8c6aafbb3319208df40ff436fabfc501426a5684 2x',
    jpeg: '/@imagetools/b14e2bce7ff2b46c1553fd7dfeb18b7d3659eeae 1x, /@imagetools/f9ebb33f5f5802dcf07f93c05a54f3068167bd29 2x'
  },
  img: {
    src: '/@imagetools/f9ebb33f5f5802dcf07f93c05a54f3068167bd29',
    w: 600,
    h: 600
  }
}

And the output in the browser console

{
  "sources": {
    "avif": "/@imagetools/6b96b93013bb7a72f4ff6f967dbd76974b92bfbc 300w, /@imagetools/16bf4d6638f522afb34cd2b0322ce3629a686963 600w",
    "webp": "/@imagetools/a9ce3b8e03e646d59be2173e1e8a5ac7b4757714 300w, /@imagetools/8c6aafbb3319208df40ff436fabfc501426a5684 600w",
    "jpeg": "/@imagetools/b14e2bce7ff2b46c1553fd7dfeb18b7d3659eeae 300w, /@imagetools/f9ebb33f5f5802dcf07f93c05a54f3068167bd29 600w"
  },
  "img": {
    "src": "/@imagetools/f9ebb33f5f5802dcf07f93c05a54f3068167bd29",
    "w": 600,
    "h": 600
  }
}

React complains with

A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.

It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.

https://react.dev/link/hydration-mismatch

 <RenderedRoute match={{params:{}, ...}} routeContext={{...}}>
      <Layout>
        <html lang="en">
          <head>
          <body>
            <App>
              <Outlet>
                <RenderedRoute match={{params:{}, ...}} routeContext={{outlet:null, ...}}>
                  <ViteImageTool>
                    <main>
                      <h1>
                      <div className="my-6 w-[30...">
                        <picture>
                          <source
+                           srcSet="/@imagetools/6b96b93013bb7a72f4ff6f967dbd76974b92bfbc 300w, /@imagetools/16bf4d663..."
-                           srcSet="/@imagetools/6b96b93013bb7a72f4ff6f967dbd76974b92bfbc 1x, /@imagetools/16bf4d6638f..."
                            type="image/avif"
                          >
                          <source
+                           srcSet="/@imagetools/a9ce3b8e03e646d59be2173e1e8a5ac7b4757714 300w, /@imagetools/8c6aafbb3..."
-                           srcSet="/@imagetools/a9ce3b8e03e646d59be2173e1e8a5ac7b4757714 1x, /@imagetools/8c6aafbb331..."
                            type="image/webp"
                          >
                          <source
+                           srcSet="/@imagetools/b14e2bce7ff2b46c1553fd7dfeb18b7d3659eeae 300w, /@imagetools/f9ebb33f5..."
-                           srcSet="/@imagetools/b14e2bce7ff2b46c1553fd7dfeb18b7d3659eeae 1x, /@imagetools/f9ebb33f5f5..."
                            type="image/jpeg"
                          >

To be able to debug this problem, I forked and installed a local copy of vite-imagetools. I put a console.log(metadatas) into output-format.tsx > pictureFormat function and I see, that second log is missing the pixelDensityDescriptor.

First metadata contains pixelDensityDescriptor:

[
  {
    format: 'avif',
    width: 300,
    height: 300,
    space: 'srgb',
    channels: 3,
    depth: 'uchar',
    density: 300,
    chromaSubsampling: '4:4:4',
    isProgressive: true,
    resolutionUnit: 'inch',
    hasProfile: true,
    hasAlpha: false,
    orientation: 1,
    aspect: 1,
    allowUpscale: false,
    pixelDensityDescriptor: '1x',
    src: '/@imagetools/6b96b93013bb7a72f4ff6f967dbd76974b92bfbc',
    image: undefined
  },
  //...
]

Second data does not (also some other props are missing such as chromaSubsampling and others):

[
  {
    format: 'avif',
    width: 300,
    height: 300,
    space: 'srgb',
    channels: 3,
    depth: 'uchar',
    isProgressive: false,
    pages: 1,
    pagePrimary: 0,
    compression: 'av1',
    hasProfile: false,
    hasAlpha: false,
    src: '/@imagetools/6b96b93013bb7a72f4ff6f967dbd76974b92bfbc',
    image: undefined
  },
  //...
]

Not sure if this is a bug or I am doing something wrong ... I think it might be a caching problem or maybe some weird React/Remix race condition. I can create the reproduction repo, when I get back from vacation in a few days ;)

@HriBB HriBB changed the title Directive basePixels does not work as expected with React and SSR Directive basePixels hidration mismatch with React and SSR Oct 2, 2024
@HriBB
Copy link
Author

HriBB commented Oct 5, 2024

I am looking at the source code and I see the problem. Image metadata is different when generating from scratch or reading from the cache:

image

basePixels is implemented inside the resize transform, which only happens when applyTransforms is executed, which of course does not happen when reading from the cache. This should probably be calculated in the output function.

So basePixels approach does not work for me. Now I am trying to use the extendOutputFormats, but the problem is that input parameters are not available inside the output function, only metadatas ...

// vite.config.ts

const customOutput: OutputFormat = (config) => async (metadatas) => {
  // how can we get input params here?
  console.log('customOutput', { config, metadatas })
  return { metadatas, todo: 'square' }
}

export default defineConfig({
  plugins: [
    imagetools({
      extendOutputFormats: (builtins) => ({
        ...builtins,
        square: customOutput,
      }),
    }),
  ],
});

// in some page
import square from '~/image/spacenet.jpg?w=300;600&format=avif;webp;jpeg&as=square'

I was able to get some config with:

import square from '~/image/spacenet.jpg?w=300;600&format=avif;webp;jpeg&as=square:300'

config now contains ['300']:

const customOutput: OutputFormat = (config) => async (metadatas) => {
  console.log(config)
  return { metadatas, todo: 'square' }
}

We could simply pass parameters as a second argument to format function here or here. Then we can pass custom parameters and do whatever we want with them in the output function.

@JonasKruckenberg @benmccann I can make a PR, if you think that this is worth fixing/improving ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant