Skeletons

This guide shows you how to create skeletons for your pages that act as a placeholder while content is being fetched from your API.

Overview

To get the best shimmer effect for content blocks, React Storefront provides a set of components in react-storefront/Skeleton that help you lay out the negative space between elements as first introduced in this blog by Facebook.

Configuring a Custom Skeleton

React Storefront's Pages allows you to configure a custom load mask for each page using the loadMasks prop. It takes an object whose keys are values of AppModelBase's page field and whose values are React components. Here we configure a skeleton for the subcategory view:

// src/App.js

import SubcategorySkeleton from './subcategory/SubcategorySkeleton' // we'll implement this later
import React, { Component } from 'react'
import Pages from 'react-storefront/Pages'

export default class App extends Component {

  render() {
    return (
      <Pages
        loadMasks={{
          Subcategory: SubcategorySkeleton
        }}
        components={universal => ({
          Subcategory: universal(import('./subcategory/Subcategory')),
        })}
      />
    )
  }

}

Note that SubcategorySkeleton is not lazy-loaded. This is so that the skeleton can be shown instantly on the first client-side navigation to a subcategory.

Debugging

While developing a skeleton, it can be helpful to set the state to loading in order to design the placeholders without needing to refresh the page. You can do this by running moov.state.applyState({ loading: true }) in the Chrome debug console.

Implementing a Skeleton

The react-storefront/Skeleton module provides a number of components that make it easy to implement a Skeleton.

Skeleton

The root class for your skeleton view

Row

A row containing content and space. All props are applied as inline styles.

BlankRow

Simply a row containing only space, provided for convenience.

Space

Negative space. The color will match theme.palette.background.paper, which is white by default. All props are applied as inline styles.

Content

A placeholder for some kind of content. For example, some text, an image, a button, etc... All props are applied as inline styles.

If a Content contains a child element, the placeholder is hidden and the child element is displayed instead. This is helpful when you may or may not have certain content available when displaying the skeleton based on the client state.

Tiles

A placeholder for an instance of react-storefront/ResponsiveTiles.

Example Skeleton

Here's an example of a Subcategory skeleton:

example skeleton

Here's the code:

// src/subcategory/SubcategorySkeleton.js

import React, { Component } from 'react'
import { Skeleton, BlankRow, Row, Space, Content, Tiles } from 'react-storefront/Skeleton'
import withStyles from '@material-ui/core/styles/withStyles'

@withStyles(theme => ({
  imagePlaceholder: {
    paddingTop: '100%',
    flex: 1
  }
}))
export default class SubcategorySkeleton extends Component {
  
  render() {
    const { classes } = this.props

    return (
      <Skeleton>
        <BlankRow/>
        <Row height="26px">
          <Space/>
          <Content width="200px"/>
          <Space flex="1" minWidth="15px"/>
        </Row>
        <BlankRow/>
        <Row height="20px">
          <Space/>
          <Content flex="1"/>
          <Space/>
        </Row>
        <BlankRow/>
        <Row height="36px">
          <Space/>
          <Content flex="1"/>
          <Space/>
          <Content flex="1"/>
          <Space/>
        </Row>
        <BlankRow height="10px"/>
        <Row height="16px">
          <Space flex="1"/>
          <Content width="80px"/>
          <Space/>
        </Row>
        <BlankRow/>
        {this.createTiles()}
      </Skeleton>
    )

  }

  createTiles() {
    const tiles = []
    const { classes } = this.props

    for (let i=0; i<4; i++) {
      tiles.push(
        <div key={i}>
          <Row>
            <div className={classes.imagePlaceholder}/>
          </Row>
          <BlankRow height="15px"/>
          <Row height="16px">
            <Content flex="1"/>
          </Row>
          <BlankRow height="10px"/>
          <Row height="12px">
            <Content flex="1"/>
          </Row>
          <BlankRow height="12px"/>
          <Row height="10px">
            <Content flex="1"/>
          </Row>
          <BlankRow height="55px"/>
        </div>
      )
    }

    return <Tiles>{tiles}</Tiles>
  }

}

Rendering Model Data in Skeletons

If you're loading a page and already have part of the content available on the client, you can render that data in your skeleton by first embedding it in the link to the page using the Link component's state prop:

<Link 
  to={subcategory.url} 
  state={{ 
    subcategory: subcategory.toJSON() 
  }}
>
  {subcategory.name}
</Link>

The data in the state prop will be automatically applied to the app model when the link is clicked. You can add that data to your skeleton by injecting the corresponding part of the app state into your skeleton component:

import React, { Component } from 'react'
import { inject, observer } from 'mobx-react'
import { Skeleton, BlankRow, Row, Space, Content, Tiles } from 'react-storefront/Skeleton'
import Typography from '@material-ui/core/Typography'

@inject(({ app }) => ({ subcategory: app.subcategory }))
@observer
export default class SubcategorySkeleton extends Component {

  render() {
    const { subcategory } = this.props

    return (
      <Skeleton>
        <BlankRow/>
        <Row height="26px">
          <Space/>
          <Content width="200px">
            <Typography variant="h6">{subcategory.name}</Typography>
          </Content>
          <Space flex="1" minWidth="15px"/>
        </Row>

        {/* more here ... */}

      </Skeleton>
    )
  }

}