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

bug: Ionic component styles do not apply in Next.js application #25100

Open
4 of 6 tasks
MarianDabrowski opened this issue Apr 11, 2022 · 27 comments
Open
4 of 6 tasks

bug: Ionic component styles do not apply in Next.js application #25100

MarianDabrowski opened this issue Apr 11, 2022 · 27 comments
Labels
package: react @ionic/react package type: bug a confirmed bug report

Comments

@MarianDabrowski
Copy link

Prerequisites

Ionic Framework Version

  • v4.x
  • v5.x
  • v6.x

Current Behavior

SSR/SSG of Nextjs do not work with ionic/react: "^6.0.0". The version worked well with ionic/react 5.
I have reached out to ionic community in discord and was suggested creating the issue. The styles do not apply the behaviour is odd when we use SSR or SSG. It works well on client side though.

Expected Behavior

I expect the code to work similar as it worked on version 5.9.3.
All the styles do apply there.

Steps to Reproduce

  1. git clone https://github.com/MarianDabrowski/next-app
  2. cd next-app
  3. npm i
  4. npm run dev

Code Reproduction URL

https://github.com/MarianDabrowski/next-app

Ionic Info

[WARN] You are not in an Ionic project directory. Project context may be
missing.

Ionic:

Ionic CLI : 6.19.0

Utility:

cordova-res : 0.15.4
native-run : not installed globally

System:

NodeJS : v17.3.1
npm : 8.5.3
OS : macOS Monterey

Additional Information

Based on the discussion with @sean-perkins I believe Sean is right saying: "...custom elements should workin SSR/SSG, so the fact that it isn't, is a little concerning. The root issue is likely Stencil, if the styles are not being applied, but I would recommend creating a ticket in ionic-framework, since the UI kit should support Next.js."

_app.tsx is rendered before each page render
As i understand setupIonicReact shall be called at the initial stage.

Altering pages/index.tsx to

const Home: NextPage = () => {
  const [component, setComponent] = useState(<div>I Am a placeholder</div>);

  useEffect(() => {
    setTimeout(() => {
      setComponent(<App />);
    }, 3000);
  });
  return component;
};

makes the code work properly

@ionitron-bot ionitron-bot bot added the triage label Apr 11, 2022
@sean-perkins sean-perkins self-assigned this Apr 11, 2022
@sean-perkins sean-perkins changed the title bug: bug: Ionic component styles do not apply in Next.js application Apr 11, 2022
@sean-perkins
Copy link
Contributor

Context from my discovery & our discord conversation:

The web components are being defined/registered/detected correctly when served through Next.js. The style tag for each web component is not being rendered though. There is an inline comment where the style blocks should be. This results in all the components being in a broken/styleless state.

The fact that it worked < v6, suggests it is an issue with the custom elements build.

I've confirmed that nothing in the Next.js project configuration points at an issue with the reproduction app, so my thoughts are that:

  1. Next.js is somehow stripping out those styles
  2. Stencil is unable to apply the styles in the right execution context with Next.js.

@sean-perkins sean-perkins added package: react @ionic/react package type: bug a confirmed bug report labels Apr 11, 2022
@sean-perkins sean-perkins removed their assignment Apr 11, 2022
@ionitron-bot ionitron-bot bot removed the triage label Apr 11, 2022
@MarianDabrowski
Copy link
Author

Hi there! Any updates on this topic?

@liamdebeasi
Copy link
Contributor

This is also happening with Ionic Core loaded from a CDN, so possibly related: #25398

@sean-perkins
Copy link
Contributor

sean-perkins commented Jun 16, 2022

Upon further discovery, this only effects components that have multiple stylesheets.

For instance:

<IonText color="danger">Hello world</IonText>

Renders correctly, since it only has one stylesheet, regardless of the mode.

Components that have stylesheets per-mode, seem to not create the constructed stylesheet correctly. I believe the source of this issue likely resides from Stencil's internals.

Edit:

Also confirmed that with a brand new Stencil component library making use of multiple stylesheets, it is not a problem within Next.js. This appears to be isolated to Ionic Framework.

@liamdebeasi
Copy link
Contributor

Does the Stencil component library use the custom elements bundle or lazy loaded bundle?

@sean-perkins
Copy link
Contributor

sean-perkins commented Jun 16, 2022

Custom elements bundle, doing something similar to:

const App: React.FC = () => {
  useEffect(() => {
    import("stencil-nextjs/dist/components/my-component.js").then((x) => {
      x.defineCustomElement();
    });
  }, []);

  return (
    <my-component first="Sean" last="Perkins" mode="md"></my-component>
  );
};

Compared with setting the mode explicitly for our components, but still does not render with the stylesheet. I am curious if our logic to return early if window is not defined in initialize is related: https://github.com/ionic-team/ionic-framework/blob/main/core/src/global/ionic-global.ts#L17-L19

@MarianDabrowski
Copy link
Author

I love to see some updates on this topic! Please tell me, if there are any tasks that I can help you with.
@sean-perkins awesome analysis on your side!

@liamdebeasi
Copy link
Contributor

@sean-perkins getIonMode should be falling back to defaultMode: https://github.com/ionic-team/ionic-framework/blob/main/core/src/global/ionic-global.ts#L13, but I see that it is not set until here: https://github.com/ionic-team/ionic-framework/blob/main/core/src/global/ionic-global.ts#L60-L64

I wonder if we could make it so that the in-memory config is setup even if window is not defined? That would at least let us have the mode get configured properly even if configFromSession and configFromURL won't work in SSR mode.

@liamdebeasi
Copy link
Contributor

liamdebeasi commented Jun 17, 2022

Something like this:

const configObj = {
  persistConfig: false,
  ...userConfig,
};

config.reset(configObj);
if (config.getBoolean('persistConfig')) {
  saveConfig(win, configObj);
}

// we can't call || isPlatform(win, 'ios') ? 'ios' : 'md' here because `window`
// is not necessarily available. Maybe we can re-update `defaultMode` in the
// if block below?
defaultMode = config.get(
  'mode',
   doc.documentElement.getAttribute('mode')
);

if (typeof window !== 'undefined') {
  // everything else
}

@MarianDabrowski
Copy link
Author

@liamdebeasi as I understand, the consequences of this issue is that no meter the framework we cannot use SSR/SSG with ionic 6. Is it so?

@ihormak
Copy link

ihormak commented Jun 24, 2022

Have a similar issue. Any progress? @liamdebeasi @sean-perkins

@MarianDabrowski
Copy link
Author

Something like this:

const configObj = {
  persistConfig: false,
  ...userConfig,
};

config.reset(configObj);
if (config.getBoolean('persistConfig')) {
  saveConfig(win, configObj);
}

// we can't call || isPlatform(win, 'ios') ? 'ios' : 'md' here because `window`
// is not necessarily available. Maybe we can re-update `defaultMode` in the
// if block below?
defaultMode = config.get(
  'mode',
   doc.documentElement.getAttribute('mode')
);

if (typeof window !== 'undefined') {
  // everything else
}

@liamdebeasi I wonder if you can elaborate on this, I tried to build it locally, but I failed to understand what changes are necessary.

@liamdebeasi
Copy link
Contributor

The main issue is that when setting up the config we return early if no window is available: https://github.com/ionic-team/ionic-framework/blob/main/core/src/global/ionic-global.ts#L18

This means that the in-memory config object is never created. As a result, certain things like the default mode are not defined. The in-memory config should be usable even if window is not defined, so we likely need to re-arrange the initialize function to not return early. Instead, we likely need to wrap the window-specific parts in if/else blocks so that the in-memory config is still created even in SSR/SSG environments.

@MarianDabrowski
Copy link
Author

MarianDabrowski commented Jun 29, 2022

@liamdebeasi thank you. for the response it has helped me much. I have played with the order and guess the one below may solve the issue.

export const initialize = (userConfig: IonicConfig = {}) => {
  if (typeof (window as any) === 'undefined') {
    Context.config = config;
    const Ionic = {} as any;

    const platformHelpers: any = {};
    if (userConfig._ael) {
      platformHelpers.ael = userConfig._ael;
    }
    if (userConfig._rel) {
      platformHelpers.rel = userConfig._rel;
    }
    if (userConfig._ce) {
      platformHelpers.ce = userConfig._ce;
    }
    setPlatformHelpers(platformHelpers);

    const configObj = {
      persistConfig: false,
      ...userConfig,
    };

    config.reset(configObj);

    Ionic.config = config;
    Ionic.mode = defaultMode = config.get(
      'mode',
      'md' // I assume 'md' is default
    );

    config.set('mode', defaultMode);

    if (config.getBoolean('_testing')) {
      config.set('animated', false);
    }
    setMode((_: any) => defaultMode);
  }
  else {
    const doc = window.document;
    const win = window;
    Context.config = config;
    const Ionic = ((win as any).Ionic = (win as any).Ionic || {});

    const platformHelpers: any = {};
    if (userConfig._ael) {
      platformHelpers.ael = userConfig._ael;
    }
    if (userConfig._rel) {
      platformHelpers.rel = userConfig._rel;
    }
    if (userConfig._ce) {
      platformHelpers.ce = userConfig._ce;
    }
    setPlatformHelpers(platformHelpers);

    // create the Ionic.config from raw config object (if it exists)
    // and convert Ionic.config into a ConfigApi that has a get() fn
    const configObj = {
      ...configFromSession(win),
      persistConfig: false,
      ...Ionic.config,
      ...configFromURL(win),
      ...userConfig,
    };

    config.reset(configObj);
    if (config.getBoolean('persistConfig')) {
      saveConfig(win, configObj);
    }

    // Setup platforms
    setupPlatforms(win);

    // first see if the mode was set as an attribute on <html>
    // which could have been set by the user, or by pre-rendering
    // otherwise get the mode via config settings, and fallback to md
    Ionic.config = config;
    Ionic.mode = defaultMode = config.get(
      'mode',
      doc.documentElement.getAttribute('mode') || (isPlatform(win, 'ios') ? 'ios' : 'md')
    );
    config.set('mode', defaultMode);
    doc.documentElement.setAttribute('mode', defaultMode);
    doc.documentElement.classList.add(defaultMode);

    if (config.getBoolean('_testing')) {
      config.set('animated', false);
    }

    const isIonicElement = (elm: any) => elm.tagName?.startsWith('ION-');

    const isAllowedIonicModeValue = (elmMode: string) => ['ios', 'md'].includes(elmMode);

    setMode((elm: any) => {
      while (elm) {
        const elmMode = (elm as any).mode || elm.getAttribute('mode');
        if (elmMode) {
          if (isAllowedIonicModeValue(elmMode)) {
            return elmMode;
          } else if (isIonicElement(elm)) {
            console.warn('Invalid ionic mode: "' + elmMode + '", expected: "ios" or "md"');
          }
        }
        elm = elm.parentElement;
      }
      return defaultMode;
    });
  }
};

Are there any docs that describe how to build ionic so that I can check if it works with nextjs SSR/SSG?

@sean-perkins
Copy link
Contributor

@MarianDabrowski our contributing guide includes steps for building both core and the individual framework packages: https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#building-changes

You will want to:

  1. Run npm run build from core/
  2. Run npm run build from /packages/react-router (you may need to install dependencies in this directory if it is your first time)
  3. Run npm run build from /packages/react (you may need to install dependencies in this directory if it is your first time)
  4. Run npm pack from /packages/react
  5. Copy the generated .tgz into your NextJS application folder
  6. npm install the path of the .tgz
  7. Re-run your NextJS application

@MarianDabrowski
Copy link
Author

@sean-perkins thank you! I will do it!

@MarianDabrowski
Copy link
Author

I have just tested the proposed idea and unfortunately the app does not work as expected. I have followed the steps to get the package (during npm i have used --legacy-peer-deps, as there was some conflict with tslint), the experiment result is here: https://github.com/MarianDabrowski/next-app/tree/experiment/changes-inonic-core-initialize

@liamdebeasi
Copy link
Contributor

liamdebeasi commented Jul 1, 2022

The problem appears to be that the style variable in Stencil's initializeComponent function is undefined: https://github.com/ionic-team/stencil/blob/63dbb47a14cc840c8d37f1bf7ce315d306194788/src/runtime/initialize-component.ts#L95

Taking ion-button as an example, the stylesheet strings do appear to be loaded. The computeMode function in that initializeComponent function returns undefined, so Stencil does not grab the correct stylesheet string. Understanding why computeMode cannot determine the mode is likely the key to fixing this issue.


Editing the file and then saving it "fixes" the issue as the styles get loaded.

@liamdebeasi
Copy link
Contributor

There are a couple problems here:

  1. The initialize function returns early, causing the config + mode to never be set up.
    We return early if the window is not defined:
    if (typeof (window as any) === 'undefined') {

While this prevents the config from being setup, it also prevents a critical piece of Stencil's "mode" infrastructure from being initialized:

setMode((elm: any) => {

This function is what automatically determines which mode to apply to the components. Since no mode could be determined, no stylesheets are loaded for components with mode-specific stylesheets. (See #25100 (comment))

  1. setMode is called too late in a Next.js app.
    Even if we reconfigured the initialize function to move setMode all the way to the top, that would not completely fix the issue. The initialize function appears to be getting called after Stencil tries to apply the stylesheets.

This code reproduces the issue in Next.js:

import { setupIonicReact, IonApp, IonButton } from "@ionic/react";

setupIonicReact();

const App: React.FC = () => (
  <IonApp>
    <IonButton color="danger">Cick me</IonButton>
  </IonApp>
);

export default App;

If you were to place the same code in a Create React App/Webpack React app, the styles would get loaded because initialize is called before Stencil tries to apply the stylesheets. This difference needs to be investigated more.

@MarianDabrowski
Copy link
Author

Are there any updates?

@romfilippini-gp
Copy link

Our team is having the same exact issue, but we're trying to integrate our Stencil component library's React wrapper (reactOutputTarget). So that we can use React-wrapped versions of our components in NextJS.

We've tried several iterations of styled-components setups.

Has anyone found any possible solutions or ideas that we can try?

@MarianDabrowski
Copy link
Author

@romfilippini-gp I believe the issue is related to this ionic-team/stencil-ds-output-targets#323

@qbx2
Copy link

qbx2 commented Mar 15, 2023

Wow, calling setupIonicReact() worked for me.

@romfilippini-gp
Copy link

Wow, calling setupIonicReact() worked for me.

@qbx2 Could you please post a sample of how you did it in your project?

@qbx2
Copy link

qbx2 commented Mar 15, 2023

@romfilippini-gp I was trying to setup nextjs+tailwind+ionic+capacitor from scratch. Without setupIonicReact(), ionic css didn't apply. The example is here: https://github.com/mlynch/nextjs-tailwind-ionic-capacitor-starter/blob/main/components/AppShell.jsx#L14 . Now I have nextjs+tailwind+ionic+capacitor with latest version

@gabfiocchi
Copy link

Any update with Ionic 7? Thanks!

@elix1er

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package: react @ionic/react package type: bug a confirmed bug report
Projects
None yet
Development

No branches or pull requests

8 participants