React-JSS with material-ui and server side rendering - reactjs

I have a react application and a custom component library built using material-ui. Components within the react application itself uses react-jss for styling and I'd like to avoid too many references to material-ui in the app itself just in case I wish to replace material-ui in the future.
From what I've read it seems that Material-UI uses a wrapped version of JSS so they should be compatible, but I'm unable to extract both my own styles as well as those from material-ui. I seem to be able to extract one or the other though...
I've followed the React-JSS Server Side Rendering guide and that works fine, but I'm wondering how I can most easily combine styles from both without having to resort to using #material-ui/core/styles everywhere.
Here is my bootstrap component that registers the JssProvider in my app:
export const Bootstrap: React.FC<IBootstrapProps> = ({
sheetsRegistry,
generateId
}) => {
return (
<JssProvider registry={sheetsRegistry} generateId={generateId}>
<App />
</JssProvider>
)
}
export default Bootstrap
The sheetsRegistry and generateId props comes from a render function called directly from an express handler:
const render = (props: ISSRRenderProps) => {
const sheetsRegistry = new SheetsRegistry()
const generateId = createGenerateId()
const appContent = renderToString(
<Bootstrap
sheetsRegistry={sheetsRegistry}
generateId={generateId}
/>
)
return {
appContent,
styles: sheetsRegistry.toString()
}
}
This gives me all my custom styles in the returned styles property which I in turn dump to the page without issues. The problem comes when I try to combine this with #material-ui. Simply including a component from the library does not give me it's styles server side though they do render on the client after a FOUC.
Using the server rendering guide from material-ui and modifying my Bootstrap component does give my the material-ui styles, but now my own styles are not collected. This also adds a single reference to material-ui in my react app, but if that is what it takes I can live with it as long as sub-components don't have to use material-uis style tools.
import { ServerStyleSheets } from "#material-ui/core/styles"
export const Bootstrap: React.FC<IBootstrapProps> = ({
sheetsRegistry,
generateId,
...rest
}) => {
const sheets = new ServerStyleSheets()
sheetsRegistry?.add(sheets as any)
return sheets.collect(
<JssProvider registry={sheetsRegistry} generateId={generateId}>
<App {...rest} />
</JssProvider>
)
}
Is there a way to get the best of both worlds? I'd love to use Material-UI components in my UI library and extract all styles globally for server side rendering.

I've been able to come up with a "workaround" by modifying my render function to collect material-ui styles separately and just concatenating them:
import { ServerStyleSheets } from "#material-ui/core/styles"
const render = (props: ISSRRenderProps) => {
const sheetsRegistry = new SheetsRegistry()
const generateId = createGenerateId()
const muiStyles = new ServerStyleSheets()
const appContent = renderToString(
muiStyles.collect(
<Bootstrap
sheetsRegistry={sheetsRegistry}
generateId={generateId}
/>
)
)
return {
appContent,
styles: muiStyles.toString() + sheetsRegistry.toString()
}
}
The reason believe this is a workaround and may not be the best solution is that I still have to separately handle my own styles vs material-ui styles. Seeing as they both use JSS behind the scenes I still think it should be possible to seamlessly collect both in the same sheetsRegistry, but this works for now.

Related

Set theme on server before sending to browser

I'm using Nextjs and Material UI. I want to change the theme based on the hostname(different clients will have different hostnames and different themes).
I did something like dynamically import theme file inside useEffect, but that briefly shows the default theme and then changes the colours after import is done.
I want to render the html with the respective theme for the client on the server itself. What I've done below works(I saw the network tab preview of html with respective theme applied), but don't know how it works since I am setting theme in useState(createTheme(themeObj))
This is theme/index.ts:
import defaultTheme from './default.theme';
import org1Theme from './org1.theme';
const allThemes = {
org1: org1Theme,
default: defaultTheme,
};
export default allThemes;
In _app.ts I import allThemes and use getInitialProps to get the theme object from allThemes based on hostname and send it to MyApp component as props.
_app.tsx:
MyApp.getInitialProps = async (appContext: AppContext) => {
const [hostName] = appContext.ctx.req?.headers.host?.split(':') || [''];
let themeKey = 'default';
if (hostName === 'org1.com') themeKey = 'org1';
return {
...appProps,
themeObj: allThemes[themeKey],
};
};
The MyApp component in _app.tsx:
import allThemes from '../theme';
function MyApp({themeObj}: AppProps) {
const [theme] = useState(createTheme(themeObj)); //themeObj from props from getInitialProps
return (
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
...
My question is, is this the right way to do what I'm trying to achieve and don't understand how useState(createTheme(themeObj)) works(I saw the network tab preview with respective theme applied) since useState is executed on client side(right?). I'm new to nextjs(react too, kind of) and don't know if something might break because I don't fully understand client/server side rendering.

Material UI breaks NextJS app in production

I created a NextJS app which uses server-side rendering and Material UI. It works fine in development.
The app compiles and builds without errors when I run "next build". When I run it with NODE_ENV=production, the webpage renders just fine but many features no longer work. For example:
The "Hidden" component for Material UI never shows any of its sub-components nested within even when it should (in my development app, it hides and shows certain divs depending on screen size).
None of the buttons on the webpage work. All these buttons have "onClick" events whose callback functions modify the React state object in some way when clicked. However, nothing happens when these are clicked. The state remains the same, so I'm assuming these functions never get called when these click events occur. This is true for Material UI's Button components as well as plain old HTML buttons (as JSX).
Everything works completely fine when I run this in dev mode on my laptop. However, when I build the NextJS app and deploy it to the server in production mode, I encounter the problems listed above. So far, my research has only turned up the possibility of class name conflicts during builds (this was said on Material UI's FAQ page). Has anyone had the same problem as I'm having?
EDIT: I just started a barebones NextJS app containing only one index page and minimal dependencies with one state parameter and one button to modify the parameter via an onClick event. I'm having the same problem. The button works in development but not in production. So this would be a NextJS issue rather than a Material UI problem. But that still doesn't explain why the "Hidden" component for Material UI always remains hidden regardless of screen size. Maybe it's both a Next JS and Material UI problem.
I think this can help you.
in _document.js
import React from 'react';
import Document, {
Html, Main, NextScript,
} from 'next/document';
import { ServerStyleSheets } from '#material-ui/core/styles';
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Render app and page and get the context of the page with collected side effects.
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () => originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
};
};
and in add in _app.js
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
Hope it work !
About the first Question, Because you open the ssr, The material ui can't judge the breakpointer in server side. Here's an official example。server-sider-rendering. You need to determine the user device based on the UA header to determine the page sizer. Use matches to determine what you need to display
_document.js -> MaterialUI styles for SSR.
import React from 'react'
import NextDocument from 'next/document'
import { ServerStyleSheet as StyledComponentSheets } from 'styled-components'
import { ServerStyleSheets as MaterialUiServerStyleSheets } from '#material-
ui/styles'
export default class Document extends NextDocument {
static async getInitialProps(ctx) {
const styledComponentSheet = new StyledComponentSheets()
const materialUiSheets = new MaterialUiServerStyleSheets()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props =>
styledComponentSheet.collectStyles(
materialUiSheets.collect(<App {...props} />),
),
})
const initialProps = await NextDocument.getInitialProps(ctx)
return {
...initialProps,
styles: [
<React.Fragment key="styles">
{initialProps.styles}
{materialUiSheets.getStyleElement()}
{styledComponentSheet.getStyleElement()}
</React.Fragment>,
],
}
} finally {
styledComponentSheet.seal()
}
}
}

React app using Material UI hooks not showing styling in production

I have a react app using Material UI (v4). In my development environment it all works fine.
In production the styling does not seem to be applied.
I have read about class name generation being different in dev to prod and potential clashes but I can't figure out how to fix it.
I have a js file which looks like:
import { makeStyles } from '#material-ui/core/styles';
const useStyles = makeStyles({
someClass: {
backgroundColor: 'red',
},
});
export default useStyles;
I then have multiple tsx files which use this, something like:
const Example = (props: IExampleState) => {
const styles = useStyles();
return (
<SomeMUIElement className={styles.someClass}>
{some inner stuff}
</SomeMUIElement>
)
}
const mapStateToProps = (state: IExampleState) => {
...
}
export default connect(mapStateToProps)(Calls);
In production none of these styling appear, and in dev tools I can see that there are no class names applied either, also appear to be no errors.
I'm sure I am missing something!

materialUI get class names without using withStyles

I have an app that renders components loaded dynamically from the database. I can render those components and everything works great, but I want to be able to apply specific styles to these components using withStyles.
Here's what I have tried, for sake of simplicity I'm modeling my database response with a JS file.
const dbResponse = {
__styles: {
myStyle: { background: 'blue' },
},
components: (styles) => ({
component: 'AppBar'
className: styles.myStyle,
}),
};
export default withStyles(dbResponse.__styles)(createComponentFromSchema(
dbResponse.components(dbResponse.__styles)
));
This will generate the AppBar component but not with the correct styles. The reason is obviously that what I need to pass to components is the Map that withStyles creates and passes to the children components. I could recursively clone the entire tree and replace the styles but I don't want to do that. I am using server side rendering and have access to all of the JSS specific pieces that are incidental to that scheme if it is helpful.

How to access custom namespaced (react-)JSS theme from components?

I'm creating a react/redux app, and want to utilize JSS theming to style my app/components. I'm also using other libraries that use JSS theming, e.g. Material UI, thus I need to create a namespaced theme as described in http://cssinjs.org/react-jss?v=v8.1.0#theming to avoid conflicts with other themes.
Does this mean that I have to import my namespaced theme in every component I want to style with that theme, and pass it to injectSheet? I.e:
import React from 'react
import injectSheet, {ThemeProvider} from 'react-jss
// import my custom namespaced theming object...
import theming from '../path/to/my/custom/theming'
const styles = theme => ({
container: {
background: theme.background,
}
})
const Demo = () => (
<div className={props.classes.container}>
//...
</div>
)
// injectSheet with my custom namespaced theming object..
export default injectSheet(styles, {theming})(Demo)
This feels very cumbersome. Is it another way that one should do this? Am I missing something? Thanks in advance :)
You could wrap your injectSheet function in a central place and always pass the theming there.

Resources