Builder Book logo

Book: SaaS Boilerplate

  1. Introduction. Project structure.
  2. Setup. GitHub and Git. Visual Studio code editor. Node, Yarn. package.json. TypeScript. ESLint, Prettier. Next.js. Server-side rendering. Project structure. Document HOC. App HOC. Index page. Testing. Environmental variables.
  3. Material-UI. Client-side and server-side rendered pages. Dark theme, CssBaseline. Shared layout. Adding styles. Shared components. MenuWithLinks. Notifier. Confirmer. Nprogress. Mobile browser.
  4. HTTP, request, response. APP project. Fetch method. API method at Index page. Next-Express server. Express route. Asynchronous function, Promise, async/await. API server. New project API. Updating APP.
  5. Infrastructure for User. MongoDB database. MongoDB index. Jest testing for TypeScript. Your Settings page. API infrastructure for uploading file.
  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. Toggle theme API. Team API. Invitation API.
  9. Discussion API. Post API. Websockets for Discussion and Post.
  10. Stripe API - API project. Stripe API - APP project. Setup at Stripe dashboard and environmental variables. Email notification for new post API - API project. Amazon API Gateway and AWS Lambda.
  11. Environmental variables, production/development. Logger. APP server. API server. SEO - robots.txt, sitemap.xml. Server-side caching. Heroku. Testing application in production. AWS Elastic Beanstalk.

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've sold over 500 copies of SaaS Boilerplate with less than 1% refund requests. The latest update was January 2021. We regularly update the codebase with recent syntax and stable versions for libraries. The price becomes $249 on June 1st, 2021.


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'>
                <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

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:
Builder Book

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

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:
Builder Book

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:
Builder Book

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:
Builder Book

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

As you can see from Material-UI docs:
- sheets is a collection of style rules (CSS rules)
- sheets.collect() method collects all styles during server-side rendering so these styles can be sent to the client
- sheets.getStyleElement() method returns a string of all collected styles (styles collected with the sheets.collect() method)
- React.Children.toArray(initialProps.styles) method returns a flat array of styles from intitialProps (React docs)
- spread operator ... (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) creates a new array of styles from the elements of array React.Children.toArray(initialProps.styles) and sheets.getStyleElement()

So far, we understand that code collects styles during server-side rendering of a page and sends these styles to the client as the page's this.props.styles. This is because the getInitialProps method populates the page's props. So when you see that getInitialProps returns something like:

return {
    ...initialProps,
    styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
};

then the page's props will have the property styles. this.props.styles will have a string of CSS styles that were collected during server-side rendering of the page.

More on the getInitialProps method:
https://nextjs.org/docs/api-reference/data-fetching/getInitialProps

This method populates the page's props with data. This method can be used in pages and in the App and Document HOCs but cannot be used in child components (non-page components). We will use this method on many pages, whenever we need to get data for the page. It's a good place to call an API method to fetch data from the database. A page's getInitialProps runs on both the server and the browser, depending on rendering type. If you know for a fact that your page will be only accessed as client-side rendered, then you can call an API method to fetch data inside componentDidMount instead of getInitialProps. The method getInitialProps runs before render, thus we collect CSS rules on the server and send this collection of rules to the client as the page's this.props.styles inside MyDocument.getInitialProps.

Let's define the getInitialProps method for the MyDocument component (extension for Document HOC):

public static getInitialProps = async (ctx) => {
    const sheets = new ServerStyleSheets();
    console.log(sheets);
    const originalRenderPage = ctx.renderPage;

    ctx.renderPage = () =>
        originalRenderPage({
            enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
        });

    const initialProps = await Document.getInitialProps(ctx);

    console.log(initialProps);
    console.log(initialProps.styles);
    console.log(React.Children.toArray(initialProps.styles));

    console.log(sheets);
    console.log(sheets.getStyleElement());

    return {
        ...initialProps,
        styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
    };
};

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

We've sold over 500 copies of SaaS Boilerplate with less than 1% refund requests. The latest update was January 2021. We regularly update the codebase with recent syntax and stable versions for libraries. The price becomes $249 on June 1st, 2021.