Skip to content

Instantly share code, notes, and snippets.

@busypeoples
Last active June 22, 2019 18:02
Show Gist options
  • Save busypeoples/5cfeea36f900f426fd08af6cfb0d2e99 to your computer and use it in GitHub Desktop.
Save busypeoples/5cfeea36f900f426fd08af6cfb0d2e99 to your computer and use it in GitHub Desktop.
Making unrepresentable UI representations unrepresentable!

Making Unrepresentable UI Representations Unrepresentable!

When building components, we mostly start out with a minimal API, as we mostly have a clear initial idea of what the Component should do. But as requirements start to change, our API might start to evolve a long the way too. We start adding more props to cover conditional or special cases etc. Sometimes we use optional props, as in not required, or we might start using flags, as in boolean props or enums, to handle variants. Let's take a closer look at optional props and what effects these can have on our UI representation.

Optional props

Sometimes we need optional props to calculate specific UI states, i.e. depending on the ìd, status and reviewer properties we might want to render something specific to the screen. But how can we guarantee that the provided props are actually a valid combination? What if an id can never be provided when the status is New or that a reviewer property can't exist when the status is Published? Because all props are conditional, we can't guarantee that the provided props can actually be represented. We can quickly run into representing impossible UI states. Another problem is that the more props we need to provide, the more complicated the component can get, to actually be used. What we want is that any developer can look at the possible props and always provide the right representation.

Avoiding illegal representations

How can we avoid illegal representations in our UI?

Let's first look at an example, to better understand what problems we might be actually dealing with. To get back to the id, status and reviewer props example from earlier on, we could be building a component, that depending on the current status displays something specific, we might also want to render some reviewer information or display other specific information.

// flow type representation

type OriginalType = {
  type: "New" | "Draft" | "Published",
  title: string,
  reviewer?: string,
  id?: number
};

// prop-types representation

Original.propTypes = {
  data: PropTypes.shape({
    type: PropTypes.oneOf(["New", "Draft", "Published"]).isRequired,
    title: PropTypes.string.isRequired,
    reviewer: PropTypes.string,
    id: PropTypes.number
  }).isRequired
};

Now what happens when we have two or more optional props, but we only want to allow a specific combination to be rendered? One example could be that we're dealing with a data set that is in "Draft" status and we might want to display a reviewer name, if there is any. But we do not want to represent a reviewer if the status is set to "Published".

The more optional props we need to consider that more complicated that handling becomes.

It also makes the component difficult to use, as we have to always ensure we're not randomly passing in props. Now either the developers using our component have to ensure that we're passing the right combination of props when working with the component or we have to handle this inside the component, needing to consider all possible cases. The more optional props, the more possible outcomes we need to consider.

But what if developers don't have to provide a specific combination of props for the state they want to see rendered?

At first glance it might appear that we're only shifting the problem from defining props to defining a shape. But there is something to gain from taking the latter approach. By defining possible data shapes our component expects, we can make all the props explicit. In this specific case we only have one prop and that is data now. What could data be?

<Example data={data} />

data is a variant prop that expects a data to have a certain shape to be seen as valid.

Let's take a closer look at the requirements:

A blog post can either be in New, Draft or Published status.

A New status only contains a title no other information.

A Draft status also requires a reviewer information besides a title.

A published status requires a title and an id, but no reviewer information.

We might have defined our status as variants, but what we actually want to do is define shapes for the different states our provided data can be. This can be achieved by defining a "New" state as the following with flow or typescript:

// flow-type representation
{
  type: "New",
  title: string
}

or as a prop-type definition:

const NewPostPropType = PropTypes.shape({
  title: PropTypes.string.isRequired,
  type: PropTypes.oneOf(["New"]).isRequired
});

Our top level component might compose other components depending on the type, but this just an implementation detail, i.e. loading a NewPost component when the type is New. The more interesting aspect is that all possible UI representations can now be represented explicitly.

Finally, here is an example of how these types might be defined using flow or prop-types, but can also be achieved with typescript.

// flow type representation

type ExplicitType =
  | {
      type: "New",
      title: string
    }
  | {
      type: "Draft",
      reviewer: string,
      title: string
    }
  | {
      type: "Published",
      id: number,
      title: string
    };
    
// prop-types representation    

Explicit.propTypes = {
  data: PropTypes.oneOfType([
    PropTypes.shape({
      title: PropTypes.string.isRequired,
      type: PropTypes.oneOf(["New"]).isRequired
    }),
    PropTypes.shape({
      title: PropTypes.string.isRequired,
      reviewer: PropTypes.string.isRequired,
      type: PropTypes.oneOf(["Draft"]).isRequired
    }),
    PropTypes.shape({
      title: PropTypes.string.isRequired,
      id: PropTypes.number.isRequired,
      type: PropTypes.oneOf(["Published"]).isRequired
    })
  ]).isRequired
};

If you have any questions or feedback please leave a comment here or connect via Twitter: A. Sharif

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment