hydration-overlay icon indicating copy to clipboard operation
hydration-overlay copied to clipboard

Example on how to make it work in a NX nextjs project in monorepo?

Open victorcarvalhosp opened this issue 2 years ago • 7 comments

Hi! First of all, thanks for developing this cool library, we're facing some hydration errors that are being hard to debug, and hopefully this lib will help with that! We're facing an issue though, we use a NX monorepo, and we're not being able to setup this lib, as soon as we add withHydrationOverlay to the next.config.js file our application stops working.

This is how our next.config.js looks by default:

const {withNx} = require('@nx/next/plugins/with-nx')
const {withSentryConfig} = require('@sentry/nextjs')
const path = require('path')

const isAnalyzingBundle = process.env['ANALYZE'] === 'true'

const regexEqual = (x, y) => {
  return (
    x instanceof RegExp &&
    y instanceof RegExp &&
    x.source === y.source &&
    x.global === y.global &&
    x.ignoreCase === y.ignoreCase &&
    x.multiline === y.multiline
  )
}

/**
 * @type {import('next').NextConfig}
 */
let nextConfig = {
  // We should not increase it, otherwise we run out of Vercel's limits.
  staticPageGenerationTimeout: 60,
  swcMinify: true,
  reactStrictMode: true,
  distDir: 'dist',
  pageExtensions: ['page.tsx', 'page.ts', 'api.tsx', 'api.ts'],

  /*
   Optional build-time configuration options
   https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#extend-nextjs-configuration
  */
  sentry: {
    // This will be true by default on Sentry 8.0 so meanwhile we need to force its usage
    hideSourceMaps: true,
  },
  webpack: (config) => {
    // Adds rule in order to be able to use svgs using import, otherwise we get an error
    config.module.rules.push({
      test: /\.svg$/i,
      issuer: /\.[jt]sx?$/,
      resourceQuery: {not: [/url/]}, // exclude react component if *.svg?url
      use: ['@svgr/webpack'],
    })

    /*
     * Start of CSS modules classes output format configuration
     * This entire section is needed just to change CSS modules
     * classes output format as seen here:
     * https://stackoverflow.com/a/71450423/10504792
     */
    config.resolve.modules.push(path.resolve('./'))
    const oneOf = config.module.rules.find(
      (rule) => typeof rule.oneOf === 'object'
    )

    if (oneOf) {
      // Find the module which targets *.scss|*.sass files
      const moduleSassRule = oneOf.oneOf.find((rule) =>
        regexEqual(rule.test, /\.module\.(scss|sass)$/)
      )

      if (moduleSassRule) {
        // Get the config object for css-loader plugin
        const cssLoader = moduleSassRule.use.find(({loader}) =>
          loader.includes('css-loader')
        )

        if (cssLoader)
          cssLoader.options = {
            ...cssLoader.options,
            modules: {
              ...cssLoader.options.modules,
              localIdentName: '[local]--[hash:base64:5]',
              // You can also add other css-loader options here
            },
          }
      }
    }

    return config
  },
}

// Next js Bundle Analyzer https://www.npmjs.com/package/@next/bundle-analyzer
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: true,
})

const plugins = []

if (isAnalyzingBundle) {
  plugins.push(withBundleAnalyzer)
}

module.exports = async (phase, context) => {
  let updatedConfig = plugins.reduce((acc, fn) => fn(acc), nextConfig)

  updatedConfig = await withNx(updatedConfig)(phase, context)
  updatedConfig = withSentryConfig(updatedConfig)

  return updatedConfig
}

We've tried to add the withHydrationOverlay in different ways, and all of them causes errors:

If we add withplugins.push(withHydrationOverlay) we get 404 for all pages.

If we add with

module.exports = async (phase, context) => {
  let updatedConfig = plugins.reduce((acc, fn) => fn(acc), nextConfig)

  updatedConfig = await withNx(updatedConfig)(phase, context)
  updatedConfig = withSentryConfig(updatedConfig)
  updatedConfig = withHydrationOverlay({})(updatedConfig)

  return updatedConfig
}

we receive this warning: Warning: React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports. and nothing works with a 500 (Internal Server Error). Tried moving the withHydrationOverlay to different places without success as well.

I know this is probably something related to NX, but maybe it's something you're interested to take a look and have some documentation around it?

victorcarvalhosp avatar Dec 15 '23 16:12 victorcarvalhosp

I am completely unfamiliar with the Nx plugin and how it impacts adding other things, so I'm not sure what to suggest here.

Since the only changes made by the plugin is altering the webpack config, we could potentially extract a withHydrationOverlayWebpack(webpackConfig) function that you can provide inside of your webpack configuration directly.

samijaber avatar Dec 15 '23 16:12 samijaber

In case having a webpack wrapper adds more flexibility: I released this https://github.com/BuilderIO/hydration-overlay/releases/tag/%40builder.io%2Freact-hydration-overlay%400.0.5

So you might be able to call that directly inside of your webpack config and hopefully that'll work?

samijaber avatar Dec 15 '23 17:12 samijaber

Hi @samijaber! First off, major kudos to you for working on this! 🎉 Hydration errors are so frustrating to debug and I'm excited to use this library.

We also use an Nx monorepo with our Next.js app, and we ran into the same issue that Victor mentioned. We tried using the webpack wrapper, but it seems like the initial setup in hydration-overlay-initializer.js isn't happening soon enough, because we get this error: image

Overlay line 46 is trying to access window.BUILDER_HYDRATION_OVERLAY.SSR_HTML, but it's undefined, which is why I'm assuming that setup script isn't running soon enough (though I could definitely be wrong there): image

Our next config has the webpack plugin imported and it's being called in our webpack config section: image

image

Any thoughts on what might be missing?

Thanks again for all your work! I'll keep poking at this but I'd appreciate any insight you might have!

keisha-rw avatar Dec 18 '23 21:12 keisha-rw

Unfortunately, hard to debug this since it seems to be specific to Nx. Can you provide a small repro?

My instinct is that the initializer script is not running, or not running at the right time.

You can confirm that by adding a console.log to this file: node_modules/@builder.io/react-hydration-overlay/dist/hydration-overlay-initializer.js:

+ console.log('initializer script ran');

window.BUILDER_HYDRATION_OVERLAY = {};
window.addEventListener("error", (event)=>{
    const msg = event.message.toLowerCase();
    const isReactDomError = event.filename.includes("react-dom");
    const isHydrationMsg = msg.includes("hydration") || msg.includes("hydrating");
    if (isReactDomError && isHydrationMsg) {
        window.BUILDER_HYDRATION_OVERLAY.ERROR = true;
        let appRootEl = document.querySelector(window.BUILDER_HYDRATION_OVERLAY.APP_ROOT_SELECTOR);
        if (appRootEl) {
            window.BUILDER_HYDRATION_OVERLAY.CSR_HTML = appRootEl.innerHTML;
        }
    }
});
let BUILDER_HYDRATION_OVERLAY_ELEMENT = document.querySelector(window.BUILDER_HYDRATION_OVERLAY.APP_ROOT_SELECTOR);
if (BUILDER_HYDRATION_OVERLAY_ELEMENT) {
    window.BUILDER_HYDRATION_OVERLAY.SSR_HTML = BUILDER_HYDRATION_OVERLAY_ELEMENT.innerHTML;
}

If this script doesn't run, then it means the webpack plugin is not adding the script correctly.

samijaber avatar Jan 02 '24 17:01 samijaber

I dug into this a bit more today, and I noticed this in my console:

error - ../node_modules/@builder.io/react-hydration-overlay/dist/hydration-overlay-initializer.js (2:0) @ eval
error - unhandledRejection: ReferenceError: window is not defined
    at eval (webpack-internal:///../../node_modules/@builder.io/react-hydration-overlay/dist/hydration-overlay-initializer.js:2:1)
    at ../../node_modules/@builder.io/react-hydration-overlay/dist/hydration-overlay-initializer.js (/Users/H726042/git/eks-test-nsk-app/apps/ui/dist/.next/server/node_modules_builder_io_react-hydration-overlay_dist_hydration-overlay-initializer_js.js:20:1)
    at __webpack_require__ (/Users/H726042/git/eks-test-nsk-app/apps/ui/dist/.next/server/webpack-runtime.js:33:42)
    at __webpack_require__.t (/Users/H726042/git/eks-test-nsk-app/apps/ui/dist/.next/server/webpack-runtime.js:139:38)
null

It appears that the initializer script is indeed running, but on the server instead of the client.

keisha-rw avatar Jan 03 '24 21:01 keisha-rw

Huh. I am not familiar enough with Nextjs internals to know why this is happening (ONLY for Nx). We should add a check for window in the script to avoid running on the server, but I am not sure if it will run on the client afterwards.

Would you be able to create a minimum reproduction codebase using Nx + Next + react-hydration-overlay that showcases your issue? To make sure it isn't caused by some other dependency in your project and add it to this project to make sure there are no regressions.

samijaber avatar Jan 04 '24 16:01 samijaber

Could your issue be the same as https://github.com/BuilderIO/hydration-overlay/issues/25#issuecomment-1877521434 ?

samijaber avatar Jan 04 '24 17:01 samijaber

I did a small proto here: https://github.com/belgattitude/flowblade/tree/main/examples/nextjs-app. It works with nx in a monorepo. Haven"t checked all but you can make a test

Check the nextjs.config.mjs

let nextConfig = //...

if (
  process.env.NEXT_PUBLIC_HYDRATION_OVERLAY === 'yes' &&
  process.env.NODE_ENV === 'development' &&
  !process.env.TURBOPACK
) {
  try {
    const { withHydrationOverlay } = await import(
      '@builder.io/react-hydration-overlay/next'
    ).then((mod) => mod);
    nextConfig = withHydrationOverlay({})(nextConfig);
    console.log(`- ${pc.green('info')} HydrationOverlay enabled`);
  } catch {
    // simply ignore
  }
}


export default nextConfig;

The global app root layout

export default function RootLayout({
  children,
}: Readonly<{
  children: ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${fontInter.variable} antialiased`}>
         <DevHydrationOverlayProvider>
           {children}
         </DevHydrationOverlayProvider>
      </body>
    </html>
  );
}

And the provider

export const DevHydrationOverlayProvider: FC<Props> = (props) => {
  const { children, integrations = defaultIntegrations } = props;
  if (
    process.env.NODE_ENV === 'development' &&
    process.env.NEXT_PUBLIC_ENABLE_HYDRATION_OVERLAY !== 'true' &&
    !process.env.TURBOPACK
  ) {
    return <>{children}</>;
  }
  return (
    <HydrationOverlay integrations={integrations}>{children}</HydrationOverlay>
  );
};

belgattitude avatar Oct 03 '24 15:10 belgattitude

Closing since we can't reproduce. feel free to reopen if issue persists

samijaber avatar Oct 03 '24 18:10 samijaber