Builder Book logo

Book: SaaS Boilerplate

  1. Introduction. Project structure.
  2. GitHub. VS Code Editor. Node. Yarn. TypeScript. TSLint. Next.js. Environmental variables.
  3. Material-UI. Theme. Dark theme. Shared layout. Shared styles. Shared components. Mobile browser.
  4. HTTP. APP server. Next-Express server. Fetch method. API methods. async/await. API server. Express server. Environmental variables. Logs.
  5. User model. Mongoose and MongoDB. MongoDB index. Jest testing. Your Settings page. File upload to AWS S3.
  6. Login page. Session and cookie. Google OAuth API. Authentication HOC withAuth. firstGridItem logic in App HOC.
  7. AWS SES API. Passwordless OAuth API. Mailchimp API.
  8. Application state, App HOC, store and MobX. Data store for User. Toggle theme API. Team. Invitation.
  9. Discussion API. Post API. Websockets.
  10. Stripe API and paid subscription. Email notification for new post. AWS Lambda. AWS API Gateway.
  11. Environmental variables, production/development. Logger. API server. Server-side caching. SEO - robots.txt, sitemap.xml. Server-side caching. Heroku. AWS Elastic Beanstalk.

Chapter 2: Material-UI. Theme. Dark theme. Shared layout. Shared styles. Shared components. Mobile browser.

We periodically update code and book's content.


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
    - Dark theme
  • Shared layout
  • Shared 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 server, with Material-UI. We will also dive deeper into server-side versus client-side rendering. For example, you will learn when a Next.js app renders pages on the server and when on the client. You will learn how to make environmental variables - which are available only on the server by default - available on the client. You will learn how to tell your Next.js app if a user's browser is mobile, so the app can render the page on the server with styles that are appropriate for a mobile browser.

In addition to the above, we will create shared layout, styles, and components for our app project.

link Material-UI

As you develop your own project, you can choose to create your own styles and apply these styles to components and pages. In this chapter, in subsection Shared styles, we show different ways to add shared styles.

We, as a small team of three software engineers, find it challenging to create and maintain a constistent styling library. We find our time better spent on listening to our customers and adding/improving business logic into our projects. If you have a dedicated designer on your team, you may want to create your own styling library. However, if you are small team like us, we recommend using an existing library. We chose Material-UI for React. Material-UI is inspired by Material Design.

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.

It's important to note that pages in Next.js can be rendered on either the server or the client (the browser). Material-UI supports server-side rendering; however, you have to properly integrate your Next.js project with the Material-UI library.

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

link Client-side and server-side rendered pages

To integrate our app project with Material-UI, we need to create a theme and modify two higher-order components. Here are our objectives:

  • create Material-UI's theme
  • modify App HOC
  • modify Document HOC

After completing integration, you will be able to import and use Material-UI's components inside your own components and pages. For example, we will use Material-UI's Grid, Avatar, and Button components (among others) later in this chapter.

To create a Material-UI theme, check up: https://github.com/mui-org/material-ui/blob/master/examples/nextjs-with-typescript/src/theme.tsx

As you can see, we simply import and call the createMuiTheme method.

Create a lib folder inside book/2-begin/app/. The code in the lib folder can run both on the server and on the browser (unlike the content inside the server folder). We want code related to design to be available on both the server and the browser - as we will discuss later, we need both server-side and client-side rendered pages to have proper design.

Inside book/2-begin/app/lib/, create a theme.ts file with the following content:

book/2-begin/app/lib/theme.ts

import grey from '@material-ui/core/colors/grey';
import { createMuiTheme } from '@material-ui/core/styles';

const theme = createMuiTheme({
    palette: {
        primary: { main: grey[800] },
        secondary: { main: grey[900] },
        type: 'light',
    },
});

export { theme };

As you can see, we chose to modify only the palette property in our custom theme object. The theme object has many other properties that you are free to modify: https://material-ui.com/customization/default-theme/#default-theme

In this book, we only modify palette.primary, palette.secondary, and palette.type.

Next, check up the App higher-order component in Material-UI's official example: https://github.com/mui-org/material-ui/blob/master/examples/nextjs-with-typescript/pages/_app.tsx

Summarizing the example's code - we have to make three changes to our current App HOC:

  • First, we need to wrap <Component {...pageProps} /> with ThemeProvider and remember to pass our custom theme object as ThemeProvider's prop. Simply import ThemeProvider and theme from their respective locations and wrap ``<Component {...pageProps} />` like this:
    <ThemeProvider theme={theme}>
      <Component {...pageProps} />
    </ThemeProvider>
    Once we finish integration, we will modify our custom theme and see changes in our browser.
  • Second, we need to import and add the <CssBaseline /> component. By using <CssBaseline />, we add some baseline styles to our app. To find all baseleine styles, check up:
    https://material-ui.com/components/css-baseline/#css-baseline
    https://github.com/necolas/normalize.css/blob/master/normalize.css
    Import <CssBaseline /> and let's remember to wrap ThemeProvider around <CssBaseline />:
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Component {...pageProps} />
    </ThemeProvider>
    Once we finish integration, we will use Chrome Dev Tools to find these baseline styles.
  • Third, once the component is rendered on the browser, we need to remove CSS styles that were injected to the server-side rendered page on the server. To do so, we will use the component lifecycle method componentDidMount. This method is called right after a component is added to the browser's DOM. We will find all styles with id=jss-server-side and remove them. This way, we avoid the problem of style duplication. One set of styles is inserted on the server, and then a second set of the same styles is added to the browser. If our page has duplicated styles, this may cause a "flash of style" when the server-side and client-side styles differ. In addition, having duplicated styles reduces perfomance. We use basic JavaScript's method [querySelector]
    public componentDidMount() {
      // Remove the server-side injected CSS.
      const jssStyles = document.querySelector('#jss-server-side');
      if (jssStyles && jssStyles.parentNode) {
          jssStyles.parentNode.removeChild(jssStyles);
      }
    }
    Once we finish integration, we will use Chrome Dev Tools to see what happens to styles for a server-side rendered page.

Add all three changes to your current book/2-begin/app/pages/_app.tsx. You will get:

book/2-begin/app/pages/_app.tsx:

import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from '@material-ui/styles';
import App from 'next/app';
import React from 'react';
import { isMobile } from '../lib/isMobile';
import { theme } from '../lib/theme';

class MyApp extends App {
    public componentDidMount() {
        // Remove the server-side injected CSS.
        const jssStyles = document.querySelector('#jss-server-side');
        if (jssStyles && jssStyles.parentNode) {
            jssStyles.parentNode.removeChild(jssStyles);
        }
    }
    public render() {
        const { Component, pageProps } = this.props;

        return (
            <ThemeProvider theme={theme}>
            <CssBaseline />
            <Component {...pageProps} />
            </ThemeProvider>
        );
    }
}

export default MyApp;

As mentioned earlier in this book, it is important for anyone who builds a Next.js web application to know the difference between server-side and client-side rendered pages (SSR and CSR, respectively).

When a user loads your Next.js application for the first time in his/her browser - we call this an initial load. A web page that gets loaded in this way (with initial load) is rendered on the server - we call it a server-side rendered page. That is a property of all Next.js applications.

After a user has loaded a Next.js application on his/her browser, this user may click a navigational link on the page to load another page from the application. A web page that gets loaded in this fashion is rendered on the browser - we call it a client-side rendered page.

Later in this chapter, we will inspect the loading behavior of SSR and CSR pages more closely. For now, please keep this in mind as a basic property of Next.js applications.

We mention CSR versus SSR here, because at this point in our integration, Material-UI works as expected for the client-side rendered pages. You can actually test it out. 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'>
                <a>Go to CSR page</a>
            </Link>
        </div>
    </div>
);

export default Index;

Read more about navigational links at: https://nextjs.org/docs/api-reference/next/link

Now create a new page by creating a new file book/2-begin/app/pages/csr-page.tsx with 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;

Let's call this our CSR page. Notice that we imported and added our first component from Material-UI's library - Button:
https://material-ui.com/components/buttons/

Although we called this page CSR (client-side rendered), the page will be rendered on the client only if accessed via a navigational link at the Index page. If this page is loaded via initial load (say you pasted the URL into a new tab of your browser), then the page will be rendered on the server. Sounds unbelievable? Let's test it out.

Start your app with yarn dev and navigate to http://localhost:3000:
Builder Book

Now click on the Go to CSR navigational link:
Builder Book

As you can see, the Button element already has expected styles from Material-UI's library! Wrapping the page component with ThemeProvider worked. Since you loaded the page via a navigational link, this page rendered on the client (browser). Another unrelated fact but fundamental property of a Next.js application - this page was prefetched in the background by default (search prefetch on this page about Link in Next.js documentation: 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, open a new tab, paste https://localhost:3000/csr-page, and press Enter:
Builder Book

The CSR page looks the same, but it is first rendered on the client and then second rendered on the server.

How do we know this?

Since the code inside our custom Document HOC only runs on the server (property of a Next.js app), we can add a console.log('rendered on the server'); statement like this:

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="viewport" content="width=device-width, initial-scale=1.0" />
                    <meta name="google" content="notranslate" />
                    <meta name="theme-color" content="#303030" />
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

export default MyDocument;

You've reached the end of the Chapter 2 preview. To continue reading, you will need to purchase the book.

We periodically update code and book's content.


format_list_bulleted
help_outline
lens