One of the advantages of building a Single Page Application (SPA) is the way navigating between pages is extremely fast. Unfortunately, the data of our components is sometimes only available after we have navigated to a specific part of our application. We can level up the user’s perceived performance by breaking the component into two pieces: the container (which displays a skeleton view when it’s empty) and the content. If we delay the rendering of the content component until we have actually received the content required, then we can leverage the skeleton view of the container thus boosting the perceived load time!
Let’s get started in creating our components.
What we’re making
This is a great article that outlines how you can create a skeleton component, and the use of the :empty
selector allows us to cleverly use {this.props.children}
inside of our components so that the skeleton card is rendered whenever the content is unavailable.
Creating our components
We’re going to create a couple of components to help get us started.
- The outside container (
CardContainer
) - The inside content (
CardContent
)
First, let’s create our CardContainer
. This container component will leveraging the :empty
pseudo selector so it will render the skeleton view whenever this component doesn’t receive a child.
class CardContainer extends React.Component {
render() {
return (
<div className="card">
{this.props.children}
</div>
);
}
}
Next, let’s create our CardContent
component, which will be nested inside of our CardContainer
component.
class CardContent extends React.Component {
render() {
return (
<div className="card--content">
<div className="card-content--top">
<div className="card-avatar">
<img
className="card-avatar--image"
src={this.props.avatarImage}
alt="" />
<span>{this.props.avatarName}</span>
</div>
</div>
<div className="card-content--bottom">
<div className="card-copy">
<h1 className="card-copy--title">{this.props.cardTitle}</h1>
<p className="card-copy--description">{this.props.cardDescription}</p>
</div>
<div className="card--info">
<span className="card-icon">
<span className="sr-only">Total views: </span>
{this.props.countViews}
</span>
<span className="card-icon">
<span className="sr-only">Total comments: </span>
{this.props.countComments}
</span>
</div>
</div>
</div>
);
}
}
As you can see, there’s a couple of spaces for properties that can be accepted, such as an avatar image and name and the content of the card that is visible.
Putting the components together allows us to create a full card component.
<CardContainer>
<CardContent
avatarImage='path/to/avatar.jpg'
avatarName='FirstName LastName'
cardTitle='Title of card'
cardDescription='Description of card'
countComments='XX'
countViews='XX'
/>
</CardContainer>
Using a ternary operator to reveal contents when the state has been loaded
Now that we have both a CardContainer
and CardContent
component, we have split our card into the necessary pieces to create a skeleton component. But how do we swap between the two when content has been loaded?
This is where a clever use of state and ternary operators comes to the rescue!
We’re going to do three things in this section:
- Create a state object that is initially set to
false
- Update our component to use a ternary operator so that the
cardContent
component will not be rendered when the state isfalse
- Set the state to be the content of our object once we receive that information
We want to set the default state of our content to be set to false
. This hides the card content and allows the CSS :empty
selector to do it’s magic.
this.state = {
cardContent: false
};
Now we’re got to update our CardContainer
children to include a ternary operator. In our case, it looks at this.state.cardContent
to see whether or not it resolves to true or false. If it’s true
, it does everything on the left side of the colon (:
). Conversely, if it’s false
, it does everything on the right hand of the colon. This is pretty useful because objects will resolve to true
and if we set the initial state to false
, then our component has all the conditions it needs to implement a skeleton component!
Let’s combine everything together inside of our main application. We wont worry about the state inside CardContent
quite yet. We’ll bind that to a button to mimic the process of fetching content from an API.
<CardContainer>
{this.state.cardContent
?
<CardContent
avatarImage={this.state.cardContent.card.avatarImage}
avatarName={this.state.cardContent.card.avatarName}
cardTitle={this.state.cardContent.card.cardTitle}
cardDescription={this.state.cardContent.card.cardDescription}
countComments={this.state.cardContent.card.countComments}
countViews={this.state.cardContent.card.countViews}/>
:
null
}
</CardContainer>
Boom! As you can see, the card is rendering as the skeleton component since the state of cardContent
is set to false
. Next, we’re going to create a function that sets the state of cardContent
to a mock Card Data Object (dummyCardData
):
populateCardContent = (event) => {
const dummyCardData = {
card: {
avatarImage: "https://gravatar.com/avatar/f382340e55fa164f1e3aef2739919078?s=80&d=https://codepen.io/assets/avatars/user-avatar-80x80-bdcd44a3bfb9a5fd01eb8b86f9e033fa1a9897c3a15b33adfc2649a002dab1b6.png",
avatarName: "Mathias Rechtzigel",
cardTitle: "Minneapolis",
cardDescription:"Winter is coming, and it will never leave",
countComments:"52",
countViews:"32"
}
}
const cardContent = dummyCardData
this.setState({
cardContent
})
}
In this example, we’re setting the state inside of a function. We could also leverage React’s lifecycle methods to populate the component’s state. We would have to take a look at the appropriate method to use, depending on our requirements. For example, if I’m loading an individual component and want to get the content from the API, then we would use the ComponentDidMount lifecycle method. As the documentation states, we have to be careful of using this lifecycle method in this way as it could cause an additional render — but setting the initial state to false
should prevent that from happening.
The second card in the list is hooked up to the click event that sets the cardContent
state. Once the state is set to the content’s object, the skeleton version of the card disappears and the content is shown, ensuring the that the user doesn’t see a flash of UI (FLU season is coming so we don’t want to give the users the F.L.U.!).
Let’s review
We covered quite a bit, so let’s recap what we did.
- We created a
CardContainer
. The container component is leveraging the:empty
pseudo selector so that it renders the skeleton view of the component when it is empty. - We created the
CardContent
component that is nested withinCardContainer
that we pass our state to. - We set the default state of the
cardContent
tofalse
- We use a ternary operator to render the inner content component only when we receive the content and put it in our
cardContent
state object.
And there we have it! A perceived boost in performance by creating an interstitial state between the UI being rendered and it receiving the data to populate content.
Source: CSS-tricks.com