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, store, pages, components). Mongoose, MongoDB. Slugify. Tests. Session. Settings page. AWS S3. File upload.
  6. Store. MobX. Store HOC withStore. Authentication HOC withAuth. Login page. Index page.
  7. Google OAuth. Mailchimp.
  8. AWS SES. EmailTemplate. Welcome email.
  9. Passwordless OAuth.
  10. Team. Invitation. Invitation email.
  11. Stripe. Customer. Subscription. Invoice.
  12. Discussion. Post.
  13. Web sockets.
  14. Create Post via email. AWS Lambda. AWS API Gateway.
  15. Deploy to AWS Elastic BeanStalk.

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

Available for pre-order for $99. The price becomes $199 once published.


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-end/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-end/app/lib/, create a theme.ts file with following content:

./book/2-end/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-end/app/pages/_app.tsx. You will get:

./book/2-end/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-end/app/pages/index.tsx) that 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-end/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;

The section above is a preview of this book, which is in progress. You can pre-order the book for $99. The price after book's completion will be $199.

If you pre-order the book, you will be emailed about new chapters as they become available.

The book is to be completed by May 1, 2020.

Available for pre-order for $99. The price becomes $199 once published.


format_list_bulleted
help_outline
lens