React Storefront |

Managing State

The React Storefront framework uses mobx-state-tree for state management. It combines the ease of use of MobX's mutable, observable models, while providing structure to the app's state (similar to Redux). It supports the Redux Dev Tools Chrome Extension

Base Models

React Storefront provides an number of base model classes that implement common behavior and automatically integrate with analytics. These are found in react-storefront/model.

Accessing State

The root of the state tree in React Storefront is named app. You can access it from any component using mobx-react's @inject decorator. To make your component automatically rerender when injected state changes, add the @observer decorator.

import React, { Component } from 'react'
import { observer, inject } from 'mobx-react'
import Typography from '@material-ui/core/Typography'

@inject(({ app }) => ({ product: app.product })) // inject and observe the product branch of the state tree
@observer // `observer` should always be the last decorator!
export default class Product extends Component {

  render() {
    return (
      <Typography variant="h6">{product.name}</Typography>
    )
  }

}

Modifying State

To modify app state, call an action method on a model object. For example: to add a product to the cart, you can call the add method on react-storefront/model/CartModelBase:

import React, { Component } from 'react'
import { observer, inject } from 'mobx-react'
import Button from '@material-ui/core/Typography'

@inject(({ app }) => ({ product: app.product, cart: app.cart }))
@observer
export default class Product extends Component {

  render() {
    return (
      <Button onClick={this.addToCart}>Add to Cart</Button>
    )
  }

  addToCart = () => {
    this.props.cart.add(this.props.product)
  }

}

Only action methods can modify state. Action methods are defined on models like this:

import { types } from "mobx-state-tree"

const MyModel = types
  .model("MyModel", {
    value: types.number
  })
  .actions(self => ({
    increment() {
      self.value++
    },
    decrement() {
      self.value--
    }
  }))

const instance = MyModel.create({ value: 1 })
instance.increment() // value = 2
instance.decrement() // value = 1
instance.value = 2   // throws an error

Historical State

During client-side navigation, React Storefront saves snapshot of the application state in window.history.state. When the user navigates back or forward, React Storefront automatically restores the application's state from this snapshot. This makes backward and forward navigation very fast.

Typically, certain parts of the app state should not be restored from the snapshot when navigating back or forward. By default, React Storefront does not restore user and cart. This can be changed by overriding the retainStateOnHistoryPop action on AppModelBase.

Here is the default implementation:

// from react-storefront/AppModelBase

/**
 * Returns the part of the state tree which should not be overwritten
 * when the user goes forward or back.  You can override this action
 * to retain additional branches of the tree.
 */
retainStateOnHistoryPop() {
  return { 
    cart: self.cart.toJSON(), 
    user: self.user && self.user.toJSON() 
  }
}

withGlobalState

Your application will likely have two types of state:

  • global - state that is shared between all pages. For example, the main menu, cart, and navigation.
  • local - state that is only displayed on the current page. For example, product details.

Users can land on any page in your app. When the user first lands on the app, they will get a server-side rendered response that will include both global and local state (to render both shared and page-specific content). When a user navigates from one page to another within the PWA, the app doesn't need to update the global state (for example, the main menu never changes). In this case the server only needs to return the local state for the page being requested. React Storefront provides a function called withGlobalState(getGlobalState, localState) that makes it easy to skip the work needed to create the global state when the user navigates within the PWA.

import fetch from 'fetch'
import { withGlobalState } from 'react-storefront/router'
import Config from 'react-storefront/Config'

export default async function productHandler({ id }) {
  const apiHost = Config.get('apiHost')

  // a function to fetch navigation items, which will only be called during server-side rendering
  const getNavigation = () => {
    return fetch(`https://${apiHost}/navigation`).then(res => res.json())
  }

  // fetch the page-specific data
  const product = await fetch(`https://${apiHost}/products/${id}`).then(res => res.json())

  // Calls getNavigation during SSR and merges the result with product, otherwise skips it and just returns product.
  return withGlobalState(getNavigation, product)
}

Ideally your API would have the ability to return both global and local state in the same request. In this case, we can still use withGlobalState to omit the global state from JSON responses, saving bandwidth and execution time. For example:

import fetch from 'fetch'
import { withGlobalState } from 'react-storefront/router'
import Config from 'react-storefront/Config'
import createNavigationModel from './createNavigationModel'

export default async function productHandler({ id }) {

  // fetch the page-specific data
  const { product, navigation } = await fetch(`https://${apiHost}/products/${id}`).then(res => res.json())

  // Calls getNavigation during SSR and merges the result with product, otherwise skips it and just returns product.
  return withGlobalState(
    () => createNavigationModel(navigation), 
    createProductModel(product)
  )

}

function createProductModel(data) {
  // convert the API response to your app's ProductModel...
}