stereobooster / react-snap

👻 Zero-configuration framework-agnostic static prerendering for SPAs

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

material-ui / jss / dynamic imports

aheissenberger opened this issue · comments

The default class names (.jss123) created by jss will be merged from different dynamic imports which breaks the design.

Setup:

Solution:
replace default name generator (http://cssinjs.org/js-api/?v=v9.5.0#generate-your-own-class-names) with a variation which uses random strings as part of the class name and wrap your app with this HOC.

import React, { Component } from 'react';
import JssProvider from 'react-jss/lib/JssProvider';
import { create } from 'jss';
import preset from 'jss-preset-default';
//import createGenerateClassName from 'material-ui/styles/createGenerateClassName';

import Layout from './components/pages/layout';

const createGenerateClassName = () => {
  let counter = 0
  return (rule, sheet) => `c${Math.random().toString(36).substring(2, 4) + Math.random().toString(36).substring(2, 4)}-${rule.key}-${counter++}`
}

const jss = create(preset());
// Custom Material-UI class name generator for better debug and performance.
jss.options.createGenerateClassName = createGenerateClassName;

function App() {
  return (
    <JssProvider jss={jss}>
      <Layout />
    </JssProvider>
  );
}

export default App;

please add this to the recipe section of your docs

I do not fully understand problem. I will need to investigate. Thanks for the tip

Yes I was able to reproduce. Strange thing. I wonder how JSS works with SSR.

UPD: oh my

Once JS on the client is loaded, components initialized and your JSS styles are regenerated, it's a good time to remove server-side generated style tag in order to avoid side-effects

https://github.com/cssinjs/jss/blob/master/docs/ssr.md

render(<Button />, document.getElementById('app'), () => {
  // We don't need the static css any more once we have launched our application.
  const ssStyles = document.getElementById('server-side-styles')
  ssStyles.parentNode.removeChild(ssStyles)
})

Another fix

basically JSS doesn't support rehydration

import React from 'react';
import { hydrate, render } from 'react-dom';
import { loadComponents } from 'loadable-components';
import { getState } from 'loadable-components/snap';
import Index from './pages/index';

const app = <Index />;
const rootElement = document.getElementById('root');

loadComponents().then(() => {
  render(app, rootElement, () => {
    Array.from(document.querySelectorAll('[data-jss-snap]')).forEach(elem =>
      elem.parentNode.removeChild(elem),
    );
  });
});

window.snapSaveState = () => {
  Array.from(document.querySelectorAll('[data-jss]')).forEach(elem =>
    elem.setAttribute('data-jss-snap', ''),
  );
  return getState();
};

function App() {

perfect, this solution was amazing , thanks a lot

thanks @aheissenberger and @stereobooster! Very helpful!

@aheissenberger I had today same problem with React.lazy() and JSS from Material-UI, without SSR. Your solution seems to be helpful. Do you think it is 1 to 1 related? I think so, but I'm not 100% sure.

Another solution

const generateClassName = createGenerateClassName({
  productionPrefix: navigator.userAgent === 'ReactSnap' ? 'snap' : 'jss',
});

<StylesProvider jss={jss} generateClassName={generateClassName}>
    <Layout/>
</StylesProvider>

A much easier solution for material-ui users.

For people who are using material-ui v4, wrap the components in <NoSsr/>.

For material-ui v5, which is currently in the alpha stage, this will no longer be an issue in the future since it is migrating to emotion.

<NoSsr/>.

@matthewkwong2
how do ou know what components to wrap with <NoSsr/> with a large application

<NoSsr/>.

@matthewkwong2
how do ou know what components to wrap with <NoSsr/> with a large application

If you want to do pre-rendering on a large scale project, then react-snap is not the right tool. It is more or less an experiment and prove of concept and have a lot of imperfection. Instead, you should use a proper solution like Gatsby.

In our case of using react-snap, with react-jss, we realised that for some reason, the styles created while running react-snap (so Puppeteer) are missing some selectors and styles.

Here is the solution we ended up with, that works every time:

import React from 'react';
import { render, hydrate } from 'react-dom';
import { createGenerateId, JssProvider, SheetsRegistry } from 'react-jss';

import App from './App';

const rootElement = document.getElementById('root');
if (rootElement && rootElement.hasChildNodes()) {
  hydrate(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    rootElement,
    () => {
      const reactSnapStyles = document.getElementById('react-snap-styles');
      reactSnapStyles?.parentNode?.removeChild(reactSnapStyles);
    },
  );
} else {
  const registry = new SheetsRegistry();
  const generateId = createGenerateId();

  render(
    <JssProvider registry={registry} generateId={generateId}>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </JssProvider>,
    rootElement,
    () => {
      if (navigator.userAgent === 'ReactSnap') {
        const badStyles = document.querySelectorAll('[data-jss]');
        badStyles.forEach((cssStyle) => cssStyle.parentNode?.removeChild(cssStyle));

        const style = document.createElement('style');
        style.innerHTML = registry.toString();
        style.setAttribute('id', 'react-snap-styles');

        const head = document.querySelector('head');
        head.appendChild(style);
      }
    },
  );
}

Basically, when we're running the app with react-snap, we remove the generated react-jss styles (since they might be broken) and we append a <style id="react-snap-styles"> to head, with all the styles we collected in the SheetsRegistry. Then, when we get to rehydrate the app, after everything is rendered (and snap generated new styles), we remove the #react-snap-styles element.

I hope this helps people that might end up here with a similar issue.