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

SVG Icon causing Uncaught Error: Cannot initialize 'routeModules'. #3015

Closed
afk-mario opened this issue Apr 28, 2022 · 17 comments
Closed

SVG Icon causing Uncaught Error: Cannot initialize 'routeModules'. #3015

afk-mario opened this issue Apr 28, 2022 · 17 comments

Comments

@afk-mario
Copy link

What version of Remix are you using?

1.4.3

Steps to Reproduce

Can't reproduce on a clean install but happens whenever I use a ReactIcon component, nothing else changes in the application.

Expected Behavior

To render the application without server code

Actual Behavior

The app crashes

@revelt
Copy link
Contributor

revelt commented Apr 29, 2022

Please post the error stack if possible; it might hint us what's going on. Feel free to edit-out the paths/sensitive data. I would like to help but I'm in a peculiar situation: you admit it can't be recreated on a fresh install and I don't have access to your project so I can't recreate...

@afk-mario
Copy link
Author

afk-mario commented Apr 29, 2022

Sure let me know how else I can help to track this down

Uncaught Error: Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.
Check this link for more details:
https://remix.run/pages/gotchas#server-code-in-client-bundles
    invariant2 invariant.js:13
    RemixRoute components.js:176
    React 16
        renderWithHooks
        mountIndeterminateComponent
        beginWork
        callCallback2
        invokeGuardedCallbackDev
        invokeGuardedCallback
        beginWork$1
        performUnitOfWork
        workLoopSync
        renderRootSync
        performSyncWorkOnRoot
        flushSyncCallbacks
        flushSync
        legacyCreateRootFromDOMContainer
        legacyRenderSubtreeIntoContainer
        hydrate
    <anonymous> entry.client.jsx:4
invariant.js:13:10
    invariant2 invariant.js:13
    RemixRoute components.js:176
    React 11
        renderWithHooks
        mountIndeterminateComponent
        beginWork
        callCallback2
        invokeGuardedCallbackDev
        invokeGuardedCallback
        beginWork$1
        performUnitOfWork
        workLoopSync
        renderRootSync
        performSyncWorkOnRoot
    performSyncWorkOnRoot self-hosted:1168
    React 5
        flushSyncCallbacks
        flushSync
        legacyCreateRootFromDOMContainer
        legacyRenderSubtreeIntoContainer
        hydrate
    <anonymous> entry.client.jsx:4
    InnerModuleEvaluation self-hosted:2331
    evaluation self-hosted:2292
invariant2@http://localhost:3000/build/_shared/chunk-A62FEDJL.js:1146:11
RemixRoute@http://localhost:3000/build/_shared/chunk-A62FEDJL.js:2636:13
renderWithHooks@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:11532:35
mountIndeterminateComponent@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:14773:21
beginWork@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:15695:22
beginWork$1@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:18985:22
performUnitOfWork@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:18436:20
workLoopSync@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:18372:30
renderRootSync@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:18351:15
performSyncWorkOnRoot@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:18110:42
flushSyncCallbacks@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:8645:30
flushSync@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:18194:15
legacyCreateRootFromDOMContainer@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:20473:13
legacyRenderSubtreeIntoContainer@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:20513:21
hydrate@http://localhost:3000/build/_shared/chunk-BHAU3OQB.js:20561:18
@http://localhost:3000/build/entry.client-F52PTIBS.js:20:30

@revelt
Copy link
Contributor

revelt commented Apr 29, 2022

Check the https://remix.run/docs/en/v1/pages/gotchas#server-code-in-client-bundles — something within invariant2 does not suit the server bundle and is client-only. Sorry for basic hint, I can't advise more without seeing real code. But read the gotchas chapter, either move statements from global scope to actions, or use typeof window checks...

@revelt
Copy link
Contributor

revelt commented Apr 30, 2022

How did it go? Any luck troubleshooting?

@ibreslauer
Copy link

@afk-mario I have encountered the same issue and the problem was having statements outside actions, something @revelt
warned against in his comment.

So, this will error:

const foo = process.env.FOO; // statement in the global scope!

export const action: ActionFunction = async () => {
  console.log(foo).
  ...
};

...while this won't:

export const action: ActionFunction = async () => {
  const foo = process.env.FOO;
  console.log(foo).
  ...
};

@afk-mario
Copy link
Author

Thanks for the pointers @revelt @ibreslauer I'm gonna spend today debugging this and come back with more info later

@afk-mario
Copy link
Author

afk-mario commented May 2, 2022

I have isolated the issue to reach-dialog not sure what's going on.

I have tried rendering an empty reach dialog and this happens, I tried to render the same children outside of the dialog and the error doesn't happen.

Also this worked fine until I added a react-icon as stated before

This is my Dialog component:

import PropTypes from "prop-types";
import classNames from "classnames";
import { DialogOverlay, DialogContent } from "@reach/dialog";

import Header from "./header";
import Content from "./content";
import Footer from "./footer";

import { statuses, themes, classNamePrefix } from "./constants";

/**
 * @param {Object} props
 * @param	{string} [props.className=] - Custom class name
 * @param	{string} [props.theme=light] - [light, dark]
 * @param	{string} [props.status=normal] - [normal, danger]
 * @param	{Object} [props.isOpen=] - Boolean to open and close the modal
 * @param	{Object} [props.onDismiss=] - What will happen when the user press ESC (close the modal)
 * @param	{Object} [props.dangerouslyBypassFocusLock=] - Should be false always, unless you know what you are doing
 */
function Modal({
  theme,
  status,
  isOpen,
  onDismiss,
  children,
  className,
  ...rest
}) {
  const customClassName = classNames(`${classNamePrefix}-container`, className);

  return (
    <DialogOverlay
      className={`${classNamePrefix}-backdrop`}
      isOpen={isOpen}
      onDismiss={onDismiss}
      data-theme={theme}
      data-status={status}
    >
      <DialogContent className={customClassName} {...rest}>
        {children}
      </DialogContent>
    </DialogOverlay>
  );
}

Modal.propTypes = {
  status: PropTypes.oneOf(statuses),
  theme: PropTypes.oneOf(themes),
  className: PropTypes.string,
  children: PropTypes.node.isRequired,
  isOpen: PropTypes.bool,
  onDismiss: PropTypes.func.isRequired,
};

Modal.defaultProps = {
  className: null,
  status: "normal",
  theme: "light",
  isOpen: false,
};

Modal.Header = Header;
Modal.Content = Content;
Modal.Footer = Footer;

export default Modal;

@afk-mario
Copy link
Author

I have been debugging this for the last couple of days. I figured out that the issue happens only when two specific component share the same Dialog component.

I still don't understand how the modal logic + a react-icon in a different file = to server code leaking to the client.

I'm going to try to explain the components.

Component A is a Modal component in charge of letting the user login and it's inside my layout that it's applied to any root route.

It looks something like this:

// page.jsx
function Page({ className, children }) {
  const customClassName = classnames("page", className);
  const isConnected = useIsConnected();

  return (
    <>
      <SiteHeader>
        {isConnected ? <User className="site-header-action" /> : null}
        {!isConnected ? <ConnectModal className="site-header-action" /> : null}
      </SiteHeader>
      <main className={customClassName}>{children}</main>
    </>
  );
}
// connect-modal.jsx
import { useState, useCallback, useEffect } from "react";
import { CgEthernet } from "react-icons/cg";

import { getEnv } from "~/utils/env";

import { useisConnected } from "~/hooks/use-is-connected";

import Modal from "~/components/modal";
import LoginForm from "~/containers/login-form";

function ConnectModal({ className }) {
  const { REMIX_APP_NETWORK_ID: networkId } = getEnv();
  const [isOpen, setIsOpen] = useState();
  const open = useCallback(() => setIsOpen(true), [setIsOpen]);
  const close = useCallback(() => setIsOpen(false), [setIsOpen]);
  const isConnected = useisConnected();

  useEffect(() => {
    if (isConnected && isOpen) {
      close();
    }
  }, [isConnected, isOpen, close]);

  return (
    <>
      <button
        className={`connect-modal-button ${className}`}
        type="button"
        onClick={open}
      >
        Connect
      </button>
      <Modal
        isOpen={isOpen}
        onDismiss={close}
        aria-label="Connect Modal"
        theme="dark"
      >
        <Modal.Header
          title="Connect"
          onDismiss={close}
          icon={CgEthernet}
        />

        <LoginForm networkId={networkId} />

        <Modal.Footer>
          <button className="button" type="button" onClick={close}>
            Close
          </button>
        </Modal.Footer>
      </Modal>
    </>
  );
}

export default ConnectModal;

And the Component B is a modal component in charge of deleting items from a DB

// product-delete-modal.jsx
import { useFetcher } from "@remix-run/react";
import { useEffect } from "react";
import { CgTrash } from "react-icons/cg";

import Modal from "~/components/modal";

function ProductDeleteModal({ id, name, isOpen, onDismiss }) {
  const fetcher = useFetcher();
  const mutate = () => {
    fetcher.submit(null, {
      method: "post",
      action: `/admin/product/${id}/delete`,
    });
  };

  useEffect(() => {
    if (fetcher.type === "done" && fetcher.data.ok) {
      onDismiss();
    }
  }, [fetcher, onDismiss]);

  return (
    <Modal
      isOpen={isOpen}
      onDismiss={onDismiss}
      aria-label="Product Delete modal"
      theme="dark"
    >
      <Modal.Header title={`Delete ${name}`} icon={CgTrash} />
      <p>Are you sure you want to delete {name}?</p>
      <p>
        This action <strong>cannot</strong> be undone.
      </p>
      <Modal.Footer>
        <button className="button" type="button" onClick={onDismiss}>
          Cancel
        </button>
        <button
          className="button"
          type="button"
          onClick={mutate}
          disabled={fetcher.state !== "idle"}
        >
          {fetcher.state !== "idle" ? "..." : "Delete"}
        </button>
      </Modal.Footer>
    </Modal>
  );
}

export default ProductDeleteModal;

My current solution was to use my normal Modal component for the ComponentA and a AlertDialog component for the ComponentB.

After this I was able to add the react-icon component in my other components.

I guess there is something in the logic of the connect modal or the delete product modal that's is causing the leakage but the current error message is not really helpful.

@nakleiderer
Copy link

What is in this file?

import { getEnv } from "~/utils/env";

The error message is pretty descriptive and is likely calling out the root cause.

This normally occurs when you have server code in your client modules.

You are likely calling a node API somewhere in your client code. The ‘getEnv’ seems suspicious. You might want to try renaming the file to ‘env.server.ts’ to tell Remix not to include it in the client bundle.

@afk-mario
Copy link
Author

The error message is pretty descriptive and is likely calling out the root cause.

I think it would be helpful to get the function or code that it's getting leaked from the back-end.

You are likely calling a node API somewhere in your client code. The ‘getEnv’ seems suspicious. You might want to try renaming the file to ‘env.server.ts’ to tell Remix not to include it in the client bundle.

Was my first suspicion as well, removed all the getEnv calls and still got the error.

What I don't understand is how a SVG icon component is related to leaked front-end/back-end code

@revelt
Copy link
Contributor

revelt commented May 13, 2022

It would be a very opportunistic shortcut, but what if you took the SVG source of the icon, placed it into your ad-hoc component, and then attributed the icon creators somewhere? I'm just introducing a possible option — that's what I'd do if I were stuck — I'd lift the raw SVG and mention it somewhere in root readme and call it a day?

@ngbrown
Copy link
Contributor

ngbrown commented May 16, 2022

I'm triggering this error by adding the class, hover:cursor-pointer to a <span>. Removing the class resumes running. When running dev. No crash in the built app.

Error: Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.
Check this link for more details:
https://remix.run/pages/gotchas#server-code-in-client-bundles
    at invariant2 (http://localhost:3000/build/_shared/chunk-YBAZ5Y5H.js:1119:11)
    at RemixRoute (http://localhost:3000/build/_shared/chunk-YBAZ5Y5H.js:2595:3)
    at renderWithHooks (http://localhost:3000/build/entry.client-TF6QMZ3N.js:11070:26)
    at mountIndeterminateComponent (http://localhost:3000/build/entry.client-TF6QMZ3N.js:13190:21)
    at beginWork (http://localhost:3000/build/entry.client-TF6QMZ3N.js:13977:22)
    at HTMLUnknownElement.callCallback2 (http://localhost:3000/build/entry.client-TF6QMZ3N.js:3680:22)
    at Object.invokeGuardedCallbackDev (http://localhost:3000/build/entry.client-TF6QMZ3N.js:3705:24)
    at invokeGuardedCallback (http://localhost:3000/build/entry.client-TF6QMZ3N.js:3739:39)
    at beginWork$1 (http://localhost:3000/build/entry.client-TF6QMZ3N.js:17086:15)
    at performUnitOfWork (http://localhost:3000/build/entry.client-TF6QMZ3N.js:16314:20)

https://github.com/ngbrown/remix-blog-tutorial/blob/fe73f144e81c1bfc004952ed3b1bf01423f6527f/app/routes/posts/admin/index.tsx#L9

Making almost any change makes it work again. Even as simple as adding a comment // in the component or file.

Update: In this case, it ended up being a cache name collision in the browser. The file chunk-L6QOVPVL.js had been something else before, and the browser was still caching it. Toggling "Disable cache" in the developer tools and refreshing resolved the issue.

@kasprownik
Copy link

kasprownik commented May 23, 2022

Also experiencing this issue, the problem is that nobody in the team can reproduce it except random occurrences between deploys to prod. It works just fine after refreshing. Our suspect is cache conflict as well, but what concerns me is why this error leaks from the ErrorBoundary components exported on all levels from Root to individual routes?

Edit: we use Cloudflare cache

@afk-mario
Copy link
Author

I think this is related to #2987 as I'm getting that error after updating

@afk-mario
Copy link
Author

It is related to #2987 ! I minified the code and it's working now!

@afk-mario
Copy link
Author

Closed in favor of #2987

@ile
Copy link

ile commented Aug 31, 2022

For me this happens if I do this:

import { db } from "../../../common/src/utils/db.server";

But everything works if I do this:

import { db } from "~/utils/db.server";`

The db.server.ts file is from the Jokes tutorial app.

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

No branches or pull requests

7 participants