Make Your React Views Explicit!

Introduction

A. Sharif
JavaScript Inside
6 min readAug 11, 2018

--

When building React components or components in general we sometimes need to consider that a certain property should or should not be displayed. We mostly model our component data to reflect this situation by using a boolean flag f.e.

Sometimes we also use a defined set of values and sometimes we make our input data optional. All these approaches should enable the component consumer to be able to define if or what should be displayed.

This all sounds sensible at first look, but we will try to understand where problems might arise and how we can model our data differently to handle these type of situations.

Note: We are going to use PropTypes to define the data shape, but this can
also be achieved via
Flow or TypeScript type definitions.

Limitations

Before we try to better understand where problems might arise, let’s summarise possible ways to enable to define if and what should be rendered to the screen.

  • Boolean flag, i.e. <BlogPost showTitle />
  • Enums, i.e. <BlogPost status="draft" />
  • Optional, i.e. <div>{title && <Title content={title}/>}</div>

This all looks sensible when we look at it from an isolated perspective, we can indicate what we want to be displayed.

But what happens when our component needs more than a boolean flag or the boolean flag needs to be refactored to an enum?
What if we have two inputs like status and id, but an id shouldn’t be displayed when the status is new? Now we need to keep track of multiple inputs.

The best thing we can do, is try to build a more than trivial example, to fully
understand the implications. For example we might be writing a BlogPostInfo component that, as the name implies, should display any relevant blog post information like status, creation and published date or the number of views.

So there is some information we need to keep in sync when rendering the information. First off, let’s take a look at all the possible states:

const PostStatus = {
New: “New”,
Draft: “Draft”,
Review: “Review”,
Published: “Published”,
Removed: “Removed”
}

We can see five possible state representations and each containing state specific information . For example, a blog post can have a creation but no published date. This implies that we need to consider these constraints when rendering the information.

Let’ assume have a basic understanding of what our BlogPostInfo should be able to display.


BlogPostInfo.propTypes = {
title: propTypes.string.isRequired,
status: propTypes.oneOf(Object.keys(PostStatus)).isRequired,
id: propTypes.number,
createdDate: propTypes.object,
publishedDate: propTypes.object,
reviewedDate: propTypes.object,
removedDate: propTypes.object,
views: propTypes.number
}

Every post has a title and status, which means we can make the title and status property required. These two properties are shared by all posts but everything else depends on the status.

This means we need to cover all cases via JSX and check what is needed to ensure that everything is in sync. We do this to avoid making incompatible representations, like f.e. rendering a published date when in draft status.

It’s time to see how we might our BlogPostInfo component.

const BlogPostInfo = ({
title,
status,
id,
createdDate,
publishedDate,
reviewedDate,
removedDate,
views
}) => (
<div>
<h2>{title}</h2>
<div>status: {status}</div>
<div>
{status === “New”
? “Save this post.”
: `Update post with id ${id}.`
}
</div>
{createdDate && <div>created at: {formatDate(createdDate)}</div>}
{reviewedDate &&
<div>reviewed at: {formatDate(reviewedDate)}</div>}
{publishedDate &&
<div>published at: {formatDate(publishedDate)}</div>}
{removedDate && <div>removed at: {formatDate(removedDate)}</div>}
{views !== undefined && <div>view count: {views}</div>}
</div>
)

It might seem more confusing that it actually is, but we need to do multiple checks here, as we can’t rely on too much from what is being passed into the component. There are a couple of possible scenarios here, were we might easily render an incorrect representation. The props don’t give us enough information as they are mostly optional.

How can a component consumer represent incorrect information?

Let’s take a look at a couple of possible scenarios:

The post is in status draft, but we don’t pass in any id.

<BlogPostInfo
title=”This post is in a draft status! (but it’s missing an id!)”
status={PostStatus.Draft}
createdDate={new Date(“2018–07–01 12:45”)}
/>

The post might be in review status but we only provide a createdDate but no reviewDate.

<BlogPostInfo
title=”This post is in a review status! (but no review date!)”
status={PostStatus.Review}
createdDate={new Date(“2018–07–01 12:45”)}
id={123}
/>

Another possible scenario is that we don’t provide the view count for a published post, as view count is optional.

<BlogPostInfo
title=”This post is in a published status! (view is missing!)”
status={PostStatus.Published}
createdDate={new Date(“2018–07–01 12:45”)}
reviewedDate={new Date(“2018–07–02 17:02”)}
publishedDate={new Date(“2018–07–03 10:20”)}
id={123}
/>

One final example.

<BlogPostInfo
title=”This post has removed status (but wrong status no views!)”
status={PostStatus.Published}
createdDate={new Date(“2018–07–01 12:45”)}
reviewedDate={new Date(“2018–07–02 17:02”)}
publishedDate={new Date(“2018–07–03 10:20”)}
removedDate={new Date(“2018–07–09 19:17”)}
id={123}
/>

As we can see from the above examples, we can quickly come up with a number of possible scenarios where we have incorrect representation of our actual data.

Being Explicit

In the previous section we were able to demonstrate why using optional props, boolean flags or enums can lead to incorrect UI representations.
Now that we have a solid understanding of the scenario, let’s refactor our original components to multiple components, each representing an explicit state.

Let’s choose two possible scenarios an implement explicit representations.
First, let’s choose the initial state, the new state.

const BlogPostNew = ({ title }) => (
<div>
<h2>{title}</h2>
<div>status: New</div>
<div>Save this post.</div>
</div>
)
BlogPostNew.propTypes = {
title: propTypes.string.isRequired
}

This looks very minimal, but interestingly this is all we need to represent our
initial state. We don’t even need the status property, because we already know exactly in which state our data is. We delegate the decision when the New state is rendered to a top level component. All we need is a title property, which is required and has to be provided.

To clarify our explicit approach, let’s also build a component that represents a
published state.

const BlogPostPublished = ({ title, id, …props }) => (
<div>
<h2>{title}</h2>
<div>status: Published</div>
<div>Update post with id {id}.</div>
<div>created at: {formatDate(createdDate)}</div>
<div>reviewed at: {formatDate(reviewedDate)}</div>
<div>published at: {formatDate(publishedDate)}</div>
<div>view count: {views}</div>
</div>
)
BlogPostPublished.propTypes = {
title: propTypes.string.isRequired,
id: propTypes.number.isRequired,
createdDate: propTypes.object.isRequired,
reviewedDate: propTypes.object.isRequired,
publishedDate: propTypes.object.isRequired,
views: propTypes.number.isRequired
}

There is definitely more going on as compared to the previous example, but again we can observe that every property is required. There are no optional props, also no state enum. We exactly know, that we are in the context of a published post.

The component consumer has to provide all necessary data, which means we can always represent the correct state information.

If you recall our previous examples, we can make these states explicit now.

<BlogPostReview
title=”Explicit Review Post! (No missing reviewDate)”
id={123}
createdDate={new Date(“2018–07–01 12:45”)}
reviewedDate={new Date(“2018–07–02 17:02”)}
/>

The removed state that had an incorrect state and missing view count can now be explicitly defined.

<BlogPostRemoved
title=”Explicit removed state!(no incorrect state has view count)”
createdDate={new Date(“2018–07–01 12:45”)}
reviewedDate={new Date(“2018–07–02 17:02”)}
publishedDate={new Date(“2018–07–03 10:20”)}
removedDate={new Date(“2018–07–09 19:17”)}
id={123}
views={4567}
/>

Explicit State Transitions

One positive effect we gain from taking the explicit route is that we can also
be explicit about our state transitions now. For example we can define a publish function that only accepts a post in draft or review status and otherwise throws an error.

Taking this route means we need to model our data to represent the possible states. The following approach is a simplified approach, but should suffice for better understanding what benefits we can derive. Also we can use a library like daggy or a full fledged approach like xstate to leverage the benefits.

function publish(post) {
if (post.type === 'Draft') {
return {
…post.data,
reviewedDate: new Date(),
publishedDate: new Date(),
views: 0
}
}
if (post.type === 'Review') {
return {
…post.data,
publishedDate: new Date(),
views: 0
}
}
// throw an error...
}

Finally, here is a demo with showing the explicit blog post state.

Hopefully this post was useful. It’s also important to note, that making your views explicit should be well thought through. First check, if this will solve the actual problem and then focus on how to model these explicit states.

If you have any questions or feedback please connect via Twitter: A. Sharif

--

--

A. Sharif
JavaScript Inside

Focusing on quality. Software Development. Product Management.