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. Post. Web sockets.
  10. Stripe. Customer. Subscription. Invoice. Send email for new post. AWS Lambda. AWS API Gateway.
  11. Prepare APP and API for production. Deploy to Heroku. Deploy to AWS Elastic BeanStalk.

Chapter 7: Application state, App HOC, store and MobX. Data store for User. Toggle theme API. Team. Invitation.

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


The section below 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 $249.

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

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


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, action, runInAction, decorate

  • Data store for User
    - Updating withAuth HOC
    - Updating YourSettings page
    - Testing store infrastructure

  • Toggle theme API
    - static method toggleTheme
    - Express route
    - API method
    - Update Layout component and testing

  • Team
    - Model and static methods
    - Express routes
    - API methods
    - Store for Team
    - TeamSettings page

  • Invitation
    - Model and static methods
    - Invitation email template
    - Updating static methods for User model
    - Express routes
    - API methods
    - Store for Invitation
    - InviteMember component
    - Login page

  • Discussion
    - Model and static methods
    - Express routes
    - API methods
    - Store for Discussion
    - Discussion components
    - Discussion page

  • Post
    - Model and static methods
    - Express routes
    - API methods
    - Store for Post
    - Post components


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 (having extra step) and have a data store. As we discussed earlier, one benefit is to be productive as developer, on any page, we can access this.props.store.currentUser and this.props.store.currentUser.updateProfile method - no need to call getInitialProps and define user prop for every page. Second benefit, if data changes in the store and this data is displayed on UI, UI will get updated automatically and reactively:

Builder Book

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


link Updating App HOC

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

We need to:

  • Wrap page component with Provider higher-order component from mobx-react package. Provider passes store as a prop to page component and all child components: https://github.com/mobxjs/mobx-react#provider-and-inject

  • Wrap page with observer HOC from mobx-react package to subscribe wrapped components to observables 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-render 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.
    Example of observables in our case is store.currentUser (object) or store.currentUrl (parameter, string).

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

Official docs for mobx-react show the above steps cab 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 children components to pass store to them. inject and observer HOCs wrap components. In our case, we decided to modify App HOC that wraps page component. Open book/7-begin/app/pages/_app.tsx file and find 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 official docs suggest wrapping with observer HOC before wrapping with inject HOC.

Alternatively, we can pass store to 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 page components to observables inside store. We need to wrap page component with observer HOC, we can achieve it by wrapping page component with observer. Later in this section we will do that for YourSettings page:

export default withAuth(observer(YourSettings));

Make the above two changes to 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;

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 $249.

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

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

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


format_list_bulleted
help_outline
lens