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:
- Material-UI (v1.0.0-beta-26) https://material-ui-next.com
- JSS https://github.com/cssinjs/jss
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
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();
};
Thank you @aheissenberger
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.