Chapter 2: Material-UI. Client-side and server-side rendered pages. Dark theme, CssBaseline. Shared layout. Adding styles. Shared components. MenuWithLinks. Notifier. Confirmer. Nprogress. Mobile browser.
We keep our book up to date with recent libraries and packages.
The section below is a preview of SaaS Boilerplate Book. To read the full text, you will need to purchase the book.
In Chapter 2, you will start with the codebase in the 2-begin folder of our saas repo and end up with the codebase in the 2-end folder.
We will cover the following topics in this chapter:
Material-UI - Client-side and server-side rendered pages - Theme, dark theme, CssBaseline - Remove styles injected on the server
Shared layout
Adding styles
Shared components - MenuWithLinks - Notifier - Confirmer
Nprogress
Mobile browser
In this chapter, we will integrate our app
project, which is currently a simple Next.js project, with Material-UI. We will also discuss differences between server-side and client-side rendering. We will modify both Document
and App
HOCs so that Material-UI integration works for both types of rendering. We will create a new component, Layout
. It is a shared layout that we will explicitly use in all of the app
project's pages. We will define an isMobile
method that returns true
or false
depending on whether the browser is mobile or desktop. Finally, we will modify our App
HOC so that it passes isMobile
and firstGridItem
props to all pages of the app
project. We will discuss the purpose of these props later in this chapter when we discuss them.
Material-UI link
As you develop your own project, you can choose to create your own styles and apply these styles to components and page components. In this chapter, in the subsection Shared styles, we will show different ways of adding styles in this project.
We are a two-person software team. Creating and maintaining a constistent styling library is not a good use of our time. We find our time better spent on listening to our customers and adding/improving business logic of our existing software businesses. If you have a dedicated designer on your team, you may want to create your own styling library. However, if you are a small team like us, we recommend using an existing library.
Material Design (https://material.io/guidelines) is a design framework for web and mobile apps. It was developed and released by Google under an Apache-2 license. Since then, developers have created React-specific libraries for material design. We use the Material-UI library (https://github.com/mui-org/material-ui) to implement material design in our web application.
We find the Material-UI library straighforward to use, relatively mature, and well-maintained by its active team of contributors. As for design, it seems relatively clean and familiar to most users, since it is based on Google's Material Design.
As you already know, a Next.js web application supports two types of page rendering - server-side and client-side. Material-UI's library works out-of-the-box for client-side rendered pages but not for server-side rendered pages. We have to modify our Document
HOC to inject Material-UI's styles on the server so that Next.js can use these injected styles to render pages on the server. Later in this section, we will create a CSRPage
with Material-UI's Button
component. You will see a so-called "flash of style" problem when the page is server-side rendered.
To integrate our Next.js app with Material-UI, we will follow the official example from Material-UI's public repository:
https://github.com/mui-org/material-ui/tree/master/examples/nextjs-with-typescript
Client-side and server-side rendered pages link
We mentioned that Material-UI's library works fine for client-side rendered pages but not server-side rendered pages. Let's actually test it out using a new page called CSRPage
. Open the file book/2-begin/app/pages/index.tsx
, which contains code for our Index
page. Import and add a navigational link to this page as follows:
import React from 'react';
import Head from 'next/head';
import Link from 'next/link';
const Index = () => (
<div>
<Head>
<title>Index page</title>
<meta name="description" content="This is a description of the Index page" />
</Head>
<div>
<p>Content on Index page</p>
<Link href='/csr-page' as='/csr-page'>
Go to CSR page
</Link>
</div>
</div>
);
export default Index;
Read more about navigational links at:
https://nextjs.org/docs/api-reference/next/link
Why did we add a navigational link that leads to the CSRPage
at route /csr-page
? That's because Next.js web applications can render the same page either on the server or on the client (browser) depending on how the page is accessed. If we access CSRPage
by loading it in a new browser tab or reloading the browser tab, then this page will server-side rendered. If this page is loaded via clicking on a navigational link, then this page will be client-side rendered.
Now create a new page, CSRPage
, by creating a new file, book/2-begin/app/pages/csr-page.tsx
, with the following content:
import Button from '@material-ui/core/Button';
import React from 'react';
import Head from 'next/head';
const CSRPage = () => (
<div>
<Head>
<title>CSR page</title>
<meta name="description" content="This is a description of the CSR page" />
</Head>
<div style={{ padding: '0px 30px', fontSize: '15px', height: '100%', color: '#222' }}>
<p>Content on CSR page</p>
<Button variant="outlined">Some button</Button>
</div>
</div>
);
export default CSRPage;
Note that CSRPage
page contains our first component from Material-UI's library - Button
:
https://material-ui.com/components/buttons/
Although we called CSRPage
, it does not mean that it will always be client-side rendered. The rendering type depends on how the page is accessed (new browser tab or navigational link).
Start your app with yarn dev
and navigate to http://localhost:3000
:
Now click on the Go to CSR
navigational link:
As you can see, the Button
component and its styles are loaded without problems! Since you loaded the page via a navigational link, this page rendered on the client (on the browser). This means that Material-UI's library works out-of-the-box for client-side rendering.
Since CSRPage
has no dynamic data and only contains static HTML, Next.js has prefetched this page in the background by default. This loads page faster. To learn about the prefetch
option, search prefetch
on this page about the navigational link Link
in the docs:
https://nextjs.org/docs/api-reference/next/link
Let's load the same page, but this time, render the page on the server. Stay on the /csr-page
page and click the refresh button on your browser. Alternatively, you can open a new tab, paste https://localhost:3000/csr-page
URL, and press Enter
:
The CSR page looks the same, but the page is now rendered on the server. We can prove server-side rendering by adding a console.log
statement to our Document
HOC. Open your book/2-begin/app/pages/_document.tsx
file and add console.log('rendered on the server');
statement in this location:
import Document, { Head, Html, Main, NextScript } from 'next/document';
import React from 'react';
class MyDocument extends Document {
public render() {
console.log('rendered on the server');
return (
<Html lang="en">
<Head>
<meta charSet="utf-8" />
<meta name="google" content="notranslate" />
<meta name="theme-color" content="#303030" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
Restart your app and observe the output of your terminal.
Navigate to the CSR
page via the navigational link on the Index
page. There is no rendered on the server
printed in your terminal.
Now, reload the CSR
page by pressing the refresh button or by loading it in a new tab. Look at the terminal every time you do it:
You indeed see rendered on the server
printed in your terminal.
Though the page looks the same, the rendering type is different. There is one more striking difference. There is a noticable flash of style in the server-side rendered page. Try refreshing the tab multiple times while you are on the CSR page
. You will notice, for a fraction of a second, that the Button
component does not have styles from Material-UI:
The flash of style contains an unstyled Button
- barebone HTML button - without any styles from Material-UI's library. That's because our integration is not complete, and our application only adds Material-UI's styles properly to the client-side rendered pages. No styles are added on the server, to the server-side rendered pages.
We discussed the benefits of server-side rendering in Chapter 1. Thus, our goal here is to make Material-UI's library work properly for server-side rendering. To do so, we need to inject Material-UI styles on the server. Thus, we need to modify our Document
HOC that runs whenever a page is rendered on the server. Next.js docs suggest that the renderPage
method needs to be customized to add styles to a server-side rendered page:
https://nextjs.org/docs/advanced-features/custom-document#customizing-renderpage
Prescription from the above link on how to customize the renderPage
method:
import Document from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const originalRenderPage = ctx.renderPage
ctx.renderPage = () =>
originalRenderPage({
// useful for wrapping the whole react tree
enhanceApp: App => App,
// useful for wrapping in a per-page basis
enhanceComponent: Component => Component,
})
// Run the parent `getInitialProps` using `ctx` that now includes our custom `renderPage`
const initialProps = await Document.getInitialProps(ctx)
return initialProps
}
}
export default MyDocument
The ctx.renderPage
method runs actual React rendering logic (on the server), takes the entire React-tree enhanceApp
, and returns a rendered page, which is an HTML string:
https://github.com/vercel/next.js/blob/57e156bc49024fda19ffdffb1ed4befc4a07c2c3/packages/next/pages/_document.tsx#L76-L86
static async getInitialProps(
ctx: DocumentContext
): Promise<DocumentInitialProps> {
const enhanceApp = (App: any) => {
return (props: any) => <App {...props} />
}
const { html, head } = await ctx.renderPage({ enhanceApp })
const styles = [...flush()]
return { html, head, styles }
}
Here is an example of a customized renderPage
from Material-UI's documentation:
https://github.com/mui-org/material-ui/blob/master/examples/nextjs-with-typescript/pages/_document.tsx
As you can see, the main difference between our current Document
HOC and Document
in the Material-UI example is changes to the MyDocument.getInitialProps
method:
MyDocument.getInitialProps = async ctx => {
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: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
};
};
You may notice methods that you are not familiar - for example, ServerStyleSheets()
, sheets.collect()
, sheets.getStyleElement()
, and React.Children.toArray()
. Check up Material-UI docs for explanations:
https://material-ui.com/styles/api/#serverstylesheets
You've reached the end of the Chapter 2 preview. To continue reading, you will need to purchase the book.
We keep our book up to date with recent libraries and packages.