Chapter 7: Application state, App HOC, store and MobX. Toggle theme API. Team API. Invitation API.
We keep our book up to date with recent libraries and packages.
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.
Application state, withStore HOC, store and MobX link
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:
Here how it will look like with store:
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:
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
Updating App HOC link
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 themobx-react
package.Provider
passesstore
as a prop to a page component and all child components:Wrap our page component with an
observer
HOC from themobx-react
package to subscribe the wrapped components to anobservable
change. This will automatically re-render wrapped components if there is change inobservable
: https://mobx.js.org/refguide/observer-component.htmlobservable
is data (object, array, parameter and etc) instore
that will change over time and trigger re-rendering of corresponding React components: https://mobx.js.org/intro/overview.htmlMobX
creates a clone ofstore
(store
contains allobservables
), and when data changes in your application, it gets compared to the cloned instance to conclude if data changed.
An example ofobservable
in our case isstore.currentUser
(object) orstore.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 theinject
HOC from themobx-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;
}
let userObj = null;
try {
const { user } = await getUserApiMethod(ctx.req);
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;
const isThemeDark = store.currentUser ? store.currentUser.darkTheme : true;
return (
<ThemeProvider
theme={isThemeDark ? 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 keep our book up to date with recent libraries and packages.