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()
}
}
}
Related
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.
I have a simple React component that will initially have a Tailwind CSS class of hidden which apply CSS display: none and will change the class to visible on button click.
When I test with expect().not.toBeVisible() it tells me the element is already visible while it has a hidden class.
If I don't use Tailwind CSS and use a normal style={{display: 'none'}} it'll correctly identify that the element isn't visible. That means clearly the issue is with Tailwind CSS.
Here's my test:
test("Notification bar should be initially hidden but visible on click", async () => {
render(<Notifications />);
expect(await screen.findByTestId("list")).not.toBeVisible();
// this test fails while the element already has a Tailwind CSS class of "hidden"
});
While this's my component:
<ul className="hidden" data-testid="list">
<li>item 1</li>
</ul>
The solution explained in this Stack Overflow: cannot check expectelm not tobevisible for semantic ui react component. Based on that thread, I extend the solution to make it works with TailwindCSS as the steps explained below,
Project structure
root/
src/
test/
index.css
test-utils.tsx
component.test.tsx
index.css
1. Generate CSS from the TailwindCSS template files
By issuing the command below, the CSS file called index.css will be generated in src/test directory
npx tailwindcss -i ./src/index.css -o ./src/test/index.css
Further reading: TailwindCSS installation
2. Create custom render function
Next we need to inject the generated CSS file into the JSDOM. Custom render function will be useful so we won't be needed to repeat this task for each test
import { render, RenderOptions } from '#testing-library/react';
import React, { FC, ReactElement } from 'react';
import fs from 'fs';
const wrapper: FC<{ children: React.ReactNode }> = ({ children }) => {
return <>{children}<>;
};
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) => {
const view = render(ui, { wrapper, ...options });
const style = document.createElement('style');
style.innerHTML = fs.readFileSync('src/test/index.css', 'utf8');
document.head.appendChild(style);
return view;
};
export * from '#testing-library/react';
export { customRender as render };
Further reading: Testing Library Setup
3. Perform testing, unit test suppose to be success now
import React from 'react';
import { render, screen } from './test-utils';
test('Renders hidden hello world', () => {
render(<span className="hidden">Hello World</span>);
expect(screen.getByText('Hello World')).not.toBeVisible();
});
Why souldn't we use toHaveClass matchers instead?
it wouldn't align with the Testing Library guiding principle of “emphasize a focus on tests that closely resemble how your web pages are interacted by the users“ because by doing so, you are interacting with the component unnaturally
I have this react component. It works just fine for me.
import { Widget } from 'rasa-webchat';
function CustomWidget(){
return (
<Widget
initPayload={"payload"}
socketPath={"/socket.io/"}
customData={{"language": "en"}}
/>
)
}
export default CustomWidget;
But when I try to use it on my next.js website it fails to work.
It gives me a window is not defined error.
I think I resolved this particular error by using the dynamic importer:
import dynamic from "next/dynamic";
const webchat = dynamic(
() => {
return import('rasa-webchat');
},
{ ssr: false }
);
But now I can't figure out how to actually use the widget component from the package.
Am I allowed to import { Widget } from 'rasa-webchat' or is this just not compatible with next.js for some reason? If it's possible, how do I do it?
The syntax for named exports is slightly different. You can use the widget with a dynamic import as follows:
import dynamic from 'next/dynamic';
const Widget = dynamic(
() => import('rasa-webchat').then((mod) => mod.Widget),
{ ssr: false }
);
function CustomWidget(){
return (
<Widget
initPayload={"payload"}
socketPath={"/socket.io/"}
customData={{"language": "en"}}
/>
)
}
export default CustomWidget;
For further details check Next.js dynamic import documentation.
Nextjs is a frame work that allows you to build Static and Server Side rendered apps. So, it uses Nodesj under hood and window is not defined in nodejs. Only way to accessing window in react ssr frameworks is useEffect hook. Your dynamic import solution is right , becuase you are getting file on client side. I hope it makes sense.
Have a great day
I've started to code my first React app and it's awesome, but I can't figure out how to manage css files per-component(so the actual CSS won't load if it is not necessary).
React with webpack(correct me if I'm wrong please) wraps the project in such a way that at every given moment the app loads only what it needs(in terms of JS).
So if I have my main App component with only two buttons visible: btn-01 and btn-02, and inside of this component I have another two: component-01 and component-02, and they are hidden till the corresponded button is clicked(btn-01 for component-01), these components won't be loaded until the actual button is clicked(am I getting this right?), however this is not the same with css as I can tell, because I see the css of each of these(component-01 and component-02) components loaded right away the App is loaded, even though none of the buttons are clicked.
I'm not a big fan of inline styling, but I did test it with css module, but the result is the same in this aspect. So I'm not even sure if this is possible to implement in an easy way.
Here's a code, so perhaps I'm not implementing it correctly, but please don't mind the none-DRY code etc.
So as you may see, the style of Component-01 and -02 are loaded even though there is no need for them at the moment(none of the button is pressed).
App.js
import React, { Component } from "react";
import "./App.css";
import Component_01 from "./Component-01/Component-01";
import Component_02 from "./Component-02/Component-02";
class App extends Component {
state = {
isComponent_01: false,
isComponent_02: false,
};
toggleComponent01 = () => {
this.setState({
isComponent_01: !this.state.isComponent_01,
});
};
toggleComponent02 = () => {
this.setState({
isComponent_02: !this.state.isComponent_02,
});
};
render() {
let showComponent_01 = null;
if (this.state.isComponent_01) {
showComponent_01 = <Component_01 />;
}
let showComponent_02 = null;
if (this.state.isComponent_02) {
showComponent_02 = <Component_02 />;
}
return (
<div className="App">
<button className="btn-01" onClick={this.toggleComponent01}>
Btn-01
</button>
<button className="btn-02" onClick={this.toggleComponent02}>
Btn-02
</button>
{showComponent_01}
{showComponent_02}
</div>
);
}
}
export default App;
Component-01.js (and Component-02.js, just with -02.js)
import React from "react";
import style from "./Component-01.module.css";
function App() {
return <div className={style["component-01"]}>Component-01</div>;
}
export default App;
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.