Skip to content

Hydration error occurred in @module-federation/nextjs-mf #3346

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

Closed
5 tasks done
dfd1123 opened this issue Dec 12, 2024 · 14 comments
Closed
5 tasks done

Hydration error occurred in @module-federation/nextjs-mf #3346

dfd1123 opened this issue Dec 12, 2024 · 14 comments

Comments

@dfd1123
Copy link

dfd1123 commented Dec 12, 2024

Describe the bug

스크린샷 2024-12-12 오후 3 24 18
스크린샷 2024-12-12 오후 3 24 49

If you statically import remoteApp's page component into the shell and deploy it, then change the markup of remoteApp and redeploy it again, a hydration error will occur in the shell.

I 'revalidate' _document.tsx as you suggested, but this doesn't even work. So I waited for 10 to 15 minutes to see if it would update over time, but the hydration error still occurred.

I don't understand. If this is the case, why use module-federation?

Should I use dynamic import (ssr: false) or lazy-load? If that's the case, why do you use next.js?

Reproduction

https://github.com/dfd1123/nextjs-with-turborepo

Used Package Manager

yarn

System Info

{
  "dependencies": {
    "next": "14.2.4",
    "react": "^18.2.0",
    "react-dom": "^18"
  },
  "devDependencies": {
    "@module-federation/nextjs-mf": "^8.8.4",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "next-external-remotes-plugin": "^1.0.4",
    "prettier": "^3.2.5",
    "turbo": "^2.3.3",
    "typescript": "5.5.4"
  }
}

Validations

@ScriptedAlchemy
Copy link
Member

_document implementation is outdated.

import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import {
  revalidate,
  FlushedChunks,
  flushChunks,
} from '@module-federation/nextjs-mf/utils';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    if (ctx.pathname) {
      if (!ctx.pathname.endsWith('_error')) {
        await revalidate().then((shouldUpdate) => {
          if (shouldUpdate) {
            console.log('should HMR', shouldUpdate);
          }
        });
      }
    }

    const initialProps = await Document.getInitialProps(ctx);

    const chunks = await flushChunks();

    return {
      ...initialProps,
      chunks,
    };
  }

  render() {
    return (
      <Html>
        <Head>
          <FlushedChunks chunks={this.props.chunks} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

@dfd1123
Copy link
Author

dfd1123 commented Dec 17, 2024

@ScriptedAlchemy

Even if you do that, the hydration error doesn't go away. My question hasn't been resolved, so why are you closing it? The biggest cause I asked about was hydration error. Have you confirmed that this has been resolved?

@dfd1123
Copy link
Author

dfd1123 commented Dec 17, 2024

It seems to me that the revalidate function is not performing its function properly. Even after calling the revalidate function, a hydration error continues to occur. When I restart the shell, the hydration error disappears.

@ScriptedAlchemy
Copy link
Member

Show me your document code now

@ScriptedAlchemy
Copy link
Member

@ScriptedAlchemy

Even if you do that, the hydration error doesn't go away. My question hasn't been resolved, so why are you closing it? The biggest cause I asked about was hydration error. Have you confirmed that this has been resolved?

Next.js is currently in maintenance mode. Vercel should be the one you contact
#3153

@rr-victor-nazareth
Copy link

rr-victor-nazareth commented Dec 28, 2024

I'm facing this issue too...

FWIW, my document code is using the same methodology as pasted above. I'm also using a custom express server, and have implement a clearRoutes function as documented in Step 1 of https://module-federation.io/practice/frameworks/next/express.html.

Step 2 is using your updated code @ScriptedAlchemy as pasted above

Here's the full document code

/* eslint-disable react/no-danger */
/* eslint-disable no-underscore-dangle */
import * as React from 'react';
import BaseDocument, {
  Html, Head, Main, NextScript, DocumentContext, DocumentProps, DocumentInitialProps,
} from 'next/document';
import { CookiesProvider } from 'react-cookie';
import { Response, Request } from 'express';
import Cookies from 'universal-cookie';
import {
  revalidate,
  FlushedChunks,
  flushChunks,
} from '@module-federation/nextjs-mf/utils';
import { addCookieChangeListener } from './server/lib/cookies';
import { AppState } from './contexts';
import { ERROR_500_PATHNAME } from './constants/routes';
import { BaseDocumentProps } from './types/document';
import logger from './logger';

type CustomDocumentProps = BaseDocumentProps & {
  initialAppState?: AppState;
  chunks: Awaited<ReturnType<typeof flushChunks>>;
};

interface FullCustomDocumentProps extends DocumentProps, CustomDocumentProps { }

type GetInitialPropsReturnType = DocumentInitialProps & CustomDocumentProps;

export default class Document extends BaseDocument<FullCustomDocumentProps> {
  static async getInitialProps(context: DocumentContext): Promise<GetInitialPropsReturnType> {
    const originalRenderPage = context.renderPage;
    const { pathname, req, res } = context;
    const request = req as Request;
    const response = res as Response;
    const cookiesInstance = new Cookies(response.locals.cookies || request.cookies);
    addCookieChangeListener(cookiesInstance, response);

    context.renderPage = () => originalRenderPage({
      // Use https://nextjs.org/docs/advanced-features/custom-document#customizing-renderpage to provide server cookies
      // This allows use to NOT pass up cookies in the HTML
      enhanceApp: (App) => (props) => (
        <CookiesProvider cookies={cookiesInstance}>
          <App {...props} />
        </CookiesProvider>
      ),
    });

    if (pathname && !pathname?.endsWith('_error')) {
      console.log('about to revalidate')
      await revalidate().then((shouldReload) => {
        console.log('shouldReload', shouldReload);
        if (shouldReload) {
          logger.info('HMR activated, chunks revalidated. Clearing express routes...', { shouldReload });
          global.clearRoutes();
        }
      });
    }

    const initialProps = await BaseDocument.getInitialProps(context);
    const chunks = await flushChunks();

    // next.js error page will be compiled at build time, and getInitialProps will be invoked
    if (pathname === ERROR_500_PATHNAME) {
      return {
        ...initialProps,
        chunks,
      };
    }

    // getInitialProps is executed first in App
    const { initialAppState } = response.locals || {};

    return {
      ...initialProps,
      initialAppState,
      chunks,
    };
  }

  render(): JSX.Element {
    const { initialAppState, priorityScripts, chunks } = this.props;

    const configMarkup = `window.__CONFIGS__=${JSON.stringify(global.__CLIENT_CONFIGS__)}`;
    const initialAppStateMarkup = `window.__INITIAL_APP_STATE__=${JSON.stringify(initialAppState)};`;

    return (
      <Html>
        <Head>
          {priorityScripts?.map((scriptData) => <script key={scriptData.id} {...scriptData} />)}
          <script
            id="configEl"
            dangerouslySetInnerHTML={{ __html: configMarkup }}
          />
          <script
            id="initialAppStateEl"
            dangerouslySetInnerHTML={{ __html: initialAppStateMarkup }}
          />
          <FlushedChunks chunks={chunks} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Here's the express snippet code (currently on Express v4):

global.clearRoutes = () => {
  (expressApp._router as IRouter).stack = (expressApp._router as IRouter).stack
    ?.filter?.((k) => !k?.route?.path);
};

to add to this - when I make an update in the producer, and refresh the host page, revalidation does kick off, and shouldUpdate is true and does invoke the clearRoutes function. However, the host server still returns the output of the previous remote chunk, which leads to the hydration issue.

about to revalidate
remote_producer hash is different - must hot reload server
shouldReload true
HMR activated, chunks revalidated. Clearing express routes... { shouldReload: true }

Currently using Nextjs 13.5.8, and I'm using React.lazy to lazily load the remote component from the producer

@ScriptedAlchemy
Copy link
Member

Pull request welcomed.

@rr-victor-nazareth
Copy link

rr-victor-nazareth commented Dec 29, 2024

Thanks @ScriptedAlchemy , I'll do my best. Contributing to this ecosystem is new to me, so I'm unsure where to start - could you provide any pointers here please, as how you'd approach solving this yourself? That would help a lot! 🙏🏽 Thank you, I'll start looking into this in the meantime...

@ScriptedAlchemy
Copy link
Member

Start with the revalidation and hot-reload files. See what the use / do / track. Then search for those things in the next mf plugin or your build source code and check that they exist / are implemented.

Hot reload and revalidation are a good starting point.

If a double reload fixes the hydration error then that means something is not cleared from require cache in time or correctly.

@rr-victor-nazareth
Copy link

Thanks @ScriptedAlchemy, that helps a lot! Will link the PR once I get somewhere 🙌🏽 I'll most likely contribute under my @aaalrightalrightalright personal account then

@ScriptedAlchemy
Copy link
Member

Try testing without express but like a hello world app of next. It might be express cache or next hot reload not release. So best to try it without the variable of custom express server

@aaalrightalrightalright

Thanks for that pointer @ScriptedAlchemy , makes sense and will do!

@rr-victor-nazareth
Copy link

rr-victor-nazareth commented Dec 30, 2024

Confirmed expressjs is not a factor here via simple producer + consumer create nextjs apps

producer: https://github.com/rr-victor-nazareth/debug-nextjs-mf-producer
consumer: https://github.com/rr-victor-nazareth/debug-fix-nextjs-mf

Confirmed that the hydration error continuously occurs with any number of page reloads...

With that factored out, will continue investigating the plugin implementations per your notes.

@adit-shah-devrev
Copy link

Hi team, have we fixed it? I am still getting the hydration error. Thank you.

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

5 participants