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 7: Application state, App HOC, store and MobX. Data store for User. Toggle theme API. Team. Invitation.

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 7, you will start with the codebase in the 7-begin folder of our saas repo and end up with the codebase in the 7-end folder.

We will cover the following topics in this chapter:

  • Application state, withStore HOC, store and MobX
    - Updating App HOC
    - store, observable, decorate
    - initializeStore, getStore, server-side rendering, action
    - Data store for User
    - Updating withAuth HOC
    - Updating YourSettings page
    - Testing store infrastructure

  • Toggle theme API
    - Layout
    - Store method toggleTheme
    - toggleThemeApiMethod API method
    - Express route /user/toggle-theme
    - Static method toggleTheme
    - Testing toggle theme API

  • Team API
    - Model and static methods - Team
    - Updating User model - Team
    - Express routes - Team
    - API methods - Team
    - Data store and store methods - Team
    - Updating main store - Team
    - Initial data from App.getInitialProps - Team
    - teamRequired - Team
    - CreateTeam page
    - TeamSettings page
    - Testing Team API

  • Invitation API
    - Updating TeamSettings page
    - InviteMember component
    - Invitation page
    - Updating LoginButton component
    - Invitation data store - Invitation
    - Updating Team data store - Invitation
    - API methods - Invitation
    - Express routes - Invitation
    - Model and static methods
    - Invitation email template
    - Updating Team model - Invitation
    - Testing Invitation API


In this chapter, we will learn about data stores and their purpose. We will build a data store for User and modify our APP project to work properly with MobX. We will introduce multiple new models (Team, Invitation, Discussion, and Post) and build the corresponding infrastructure for these models in both APP and API projects.

Inside the API project, we will build many "Model - Static method - Express route" infrastructures.

Inside the APP project, we will build many "API method - Store - Page - Component" infrastructures.

link Application state, withStore HOC, store and MobX

As your SaaS boilerplate grows and becomes more complicated, there is a growing need for management of your application's state. The application's state is all data associated with your application (objects, arrays, etc). For example, the user object with public parameters in APP is part of our application's state. In this chapter, we will add more data to our application's state (Team, Invitation, Discussion and Post).

Why do we need to worry about carefully managing our application's state? What happens when a project grows? Here are a few consequences:

  • Number of pages (page components), higher-order components, and regular components increases

Let's say you need to display user information (displayName and avatarUrl) on multiple pages. As the number of pages grows, you, as a web developer, have to call some API method inside the getInitialProps method on each page.

  • Number of user events increases (end user clicks on button, uploads file, creates or deletes post and etc).

An end user may update their avatarUrl on YourSettings page. You, as a web developer, need to make sure the user object gets an updated avatarUrl parameter inside your application's state so that all other pages of your application display this updated avatar. You can achieve this by calling an API method that sends a request from your APP to API server for every page load, but that is not efficient. You'd rather send a request to your API server only one time, get an updated avatarUrl, save it to application's state, and then use this saved avatarUrl for every page that needs it - without sending a request to the API server for every page.

  • For some interactions with your web application, end users expect reactivity. For example, a user updates their avatarUrl and wants to see a new avatar right away, without reloading the page. Or a user creates a new post and wants to see this new post right away, without reloading the page. In other words, the end user does not want to reload their browser tab (server-side rendered page) or click a navigational link (client-side rendered page) to see successfully updated data on the user interface.

If we do not manage our application's state properly, we may show inconsistent and/or non-reactive data throughout our web application.

We wish there was a way to define data store for User. We want such data store to persist. When updated, we want to send a request to our API server to update that data in the database and automatically re-render corresponding components to display this data reactively to the end user. Every time an end user loads a page that requires user information, the page gets data from data store for User instead of sending a request to the API server.

The mobx package allows us to define data store with the above properties. The mobx-react package allows us to automatically re-render React components if the corresponding data changes inside the MobX data store.

Let's discuss how we will implement the above infrastructure. We can create a store object that contains all MobX data stores in our web application. We can populate the page component's props with this store object, so we can easily access it on any page with this.props.store. You already know two ways to populate props of a page component. One method is to to call the getInitialProps method. The second method is to wrap the page component with a higher-order component that can add props to the page's props. For example, let's look at YourSettings page at the end of Chapter 4. Open file book/4-end/app/pages/your-settings.tsx and file book/4-end/app/pages/_app.tsx:
- getInitialProps method of YourSettings page populates user prop
- App higher-order component that wraps all pages (Next.js feature) populates isMobile and firstGridItem props

In other words, at the end of Chapter 4, we used both methods to populate YourSettings page's props.

We can create a new higher-order withStore or update an existing higher-order component, for example, App HOC. Then on any page that needs user data, we can access it simply by:

this.props.store.currentUser

In this book, we chose to do the latter, updating App HOC instead of creating a new HOC.

Here is how our typical internal (non third-party) API infrastructure looks like now:

Builder Book

Here how it will look like with store:

Builder Book

You may ask why complicate things (add an extra step) and have a data store? As we discussed earlier, one benefit is to be productive as a developer. On any page, we can access this.props.store.currentUser and this.props.store.currentUser.updateProfile methods - no need to call getInitialProps and define the user prop for every page. A second benefit is that if data displayed on the UI changes in the store, the UI will get updated automatically and reactively:

Builder Book

Before we can access this.props.store on any page, we have to build the following parts:
- Update our App HOC so it populates a page's props with store, which can be accessed as this.props.store
- Define store from the above step
- Update our withAuth HOC
- Update YourSettings page
- Test store infrastructure


link Updating App HOC

In order to automatically and reactively re-render components when the corresponding data inside the store changes, we need to do a few things:

  • Wrap our page component with a Provider higher-order component from the mobx-react package. Provider passes store as a prop to a page component and all child components:

  • Wrap our page component with an observer HOC from the mobx-react package to subscribe the wrapped components to an observable change. This will automatically re-render wrapped components if there is change in observable: https://mobx.js.org/refguide/observer-component.html
    observable is data (object, array, parameter and etc) in store that will change over time and trigger re-rendering of corresponding React components: https://mobx.js.org/intro/overview.html
    MobX creates a clone of store (store contains all observables), and when data changes in your application, it gets compared to the cloned instance to conclude if data changed.
    An example of observable in our case is store.currentUser (object) or store.currentUrl (parameter, string).

  • We need to inject store into our page component before it can render. We can do this by wrapping our page component with the inject HOC from the mobx-react package: https://github.com/mobxjs/mobx-react#provider-and-inject

Official docs for mobx-react show the above steps can be implemented:

@inject("color")
@observer
class Button extends React.Component {
    render() {
        return <button style={{ background: this.props.color }}>{this.props.children}</button>
    }
}

class Message extends React.Component {
    render() {
        return (
            <div>
                {this.props.text} <Button>Delete</Button>
            </div>
        )
    }
}

class MessageList extends React.Component {
    render() {
        const children = this.props.messages.map(message => <Message text={message.text} />)
        return (
            <Provider color="red">
                <div>{children}</div>
            </Provider>
        )
    }
}

In the above example, store has one parameter: color. Provider wraps child components to pass store to them. inject and observer HOCs wrap components. In our case, we decided to modify our App HOC that wraps our page component. Open book/7-begin/app/pages/_app.tsx and find this line:

<Component {...pageProps} />

Based on the above example and docs, we can do:

import { Provider } from 'mobx-react';

<Provider store={store}>
    <Component {...pageProps} />
</Provider>

And then, somewhere before page component renders, we need to add:

inject('store')(observer(Component))

Note that the official docs suggest wrapping with the observer HOC before wrapping with the inject HOC.

Alternatively, we can pass store to our page component like this:

<Component {...pageProps} store={store} />

If we do so, we don't need wrap page components with inject HOC. Hovewer, we still need to wrap all other components that require reactive re-rendering with inject HOC.

We still need to subscribe our page components to observables inside store. We can achieve this by wrapping our page component with the observer HOC. Later in this section, we will do the same for the YourSettings page:

export default withAuth(observer(YourSettings));

Make the above two changes to your App HOC, and you should get:

import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from '@material-ui/styles';
import { Provider } from 'mobx-react';
import App from 'next/app';
import React from 'react';

import { themeDark, themeLight } from '../lib/theme';
import { getUserApiMethod } from '../lib/api/public';
import { isMobile } from '../lib/isMobile';
import { getStore, initializeStore, Store } from '../lib/store';

class MyApp extends App<{ isMobile: boolean }> {
    public static async getInitialProps({ Component, ctx }) {
        let firstGridItem = true;

        if (ctx.pathname.includes('/login')) {
            firstGridItem = false;
        }

        const pageProps = { isMobile: isMobile({ req: ctx.req }), firstGridItem };

        if (Component.getInitialProps) {
            Object.assign(pageProps, await Component.getInitialProps(ctx));
        }

        const appProps = { pageProps };

        if (getStore()) {
            return appProps;
        }

        const { req } = ctx;

        const headers: any = {};
        if (req.headers && req.headers.cookie) {
            headers.cookie = req.headers.cookie;
        }

        let userObj = null;
        try {
            const { user } = await getUserApiMethod({ headers});
            userObj = user;
        } catch (error) {
            console.log(error);
        }

        return {
            ...appProps,
            initialState: { user: userObj, currentUrl: ctx.asPath },
        };
    }

    public componentDidMount() {
        // Remove the server-side injected CSS.
        const jssStyles = document.querySelector('#jss-server-side');
        if (jssStyles && jssStyles.parentNode) {
            jssStyles.parentNode.removeChild(jssStyles);
        }
    }

    private store: Store;

    constructor(props) {
        super(props);

        this.store = initializeStore(props.initialState);
    }

    public render() {
        const { Component, pageProps } = this.props;
        const store = this.store;

        return (
            <ThemeProvider
                theme={store.currentUser && store.currentUser.darkTheme ? themeDark : themeLight}
            >
                <CssBaseline />
                <Provider store={store}>
                    <Component {...pageProps} store={store} />
                </Provider>
            </ThemeProvider>
        );
    }
}

export default MyApp;

You've reached the end of the Chapter 8 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