The App: “Event Management”

I made (with React Native & Redux) a ticketing/event management application. You know the type, show a list of events you manage, pull up a list of people who are attending, check them in with QR code ticket scanning, etc. It’s the sort of app people running events would want to use, not the people attending them.

There are things called “Events” where “Users” can sign up to attend. When they do so they become “Guests” and are on the list to be checked in at the door.

The Components I want to Reuse

One screen of the app shows information related to someone attending an event (a “Guest”). I made this by creating a component called “GuestDetails” and routed to it using the Navigator component provided by Facebook.

Top half of the GuestDetailsScreen. In blue, the GuestHeader component. The circle within is the GuestProfilePhoto.

Code of GuestHeader and GuestProfilePhoto

As I was filling out my GuestDetails component, at some point I realized that my code was getting a bit long. It was time to split some stuff out and make some More Awesome React Components!

// Note: These components are a case study in what NOT to do. // Some things have been simplified for code clarity and I've put

// two components in the same block here for brevity. import React from ‘react’;

import { View, Text, Image } from ‘react-native’;

import { GuestShape } from ‘./entityTypes’; // Validation for Guests // GuestProfilePhoto.js function GuestProfilePhoto({ guest }) {

return (

<Image source={guest.userImage} style={...} /> // Rounded Img

);

} GuestProfilePhoto.propTypes = {

guest: GuestShape.isRequired

}; // GuestHeader.js function GuestHeader({ guest }) {

return (

<View>

<View style={{ flex:1, ... }}> // Centered container

<GuestProfilePhoto guest={guest} />

</View>

<Text style={{ fontSize: 20 }}>{guest.fullName}</Text>

<Text style={{ fontSize: 10 }}>{guest.countryOfOrigin}</Text>

</View>

);

} GuestHeader.propTypes = {

guest: GuestShape.isRequired

};

Look! I’ve got reuse in mind already! I needed to use the same PropTypes for GuestHeader and GuestProfilePhoto without repeating the validation, so I stored the PropType shape validation for `guest` centrally and imported it from any component that took the `guest` as props.

I’m sure that’ll work out well….

Let’s Play “Reuse the GuestHeader”

Update 8/3: Though the original version remains intact below, based on feedback I’ve received I wish to clarify for the reader that “rules” in articles such as this are fickle things, and should not be considered strictly. I use the word here to mean something more like “shit that’s worked for me.” Onward!

To enumerate all the ways I’m an idiot and GuestHeader, GuestProfilePhoto and all components like it suck, let’s try to reuse them.

The Naming is Wrong

What if I wanted a similar looking header on the screen for users that aren’t attending an event? They’re not Guests anymore, they’re just Users at this point. Okay, so how about something more generic like “UserHeader” and “UserProfilePhoto”?

But, what if I want to reuse these components for displaying event information?

Rule #1: Do not name your components after the part of state they connect to. Rule #2: Do not name your components for the role they play in YOUR application.

The moment a component is named for a particular use case, well then that’s the only use case it can be used for! In order to be reusable, presentational components must be named as if they can be connected to any piece of state and display any information.

So how about…

// Rename GuestHeader to:

function ScreenHeaderWithImage({ guest }) { ... } // Rename GuestProfilePhoto to:

function CircularImage({ guest }) { ... }

Ship it! Oh, wait.

The Props Are Wrong

Our component is no longer named in such a way as to couple it to a particular screen’s usage, but those props sure are. My components can only work if I pass a prop called “guest” and that’s hardly reusable.

Further, looking at the cases where `guest.something` exists in the ScreenHeaderWithImage component, `guest` must have at least the following shape:

// To satisfy ScreenHeaderWithImage, Guest's shape must include:

{

fullName: '',

countryOfOrigin: ''

}

But there’s more.

...

<CircularImage guest={guest} />

...

The entire Guest object is passed to a child component for displaying the Guest’s photo. Now the GuestHeader must be passed whatever props this thing needs in the appropriate shape or this child will break!

So, looking into that component I found that it needs a single property called `userImage` on the `guest` object. Adding that to our required props we need:

// To satisfy GuestHeader and children, Guest's shape must include:

{

fullName: '',

countryOfOrigin: '',

userImage: ''

}

What a maintenance headache! If the shape of this were to change, it breaks two different components, and it’s totally unclear from the outside what props are even used or how!

Rule #3: A component should be given ONLY the props it requires. This structure should* be flat.

* One exception is when your individual parts of data are meaningless by themselves (e.g. tuples, e.g. an XY coordinate).

So, we could break up the props then and pass each individually:

function ScreenHeaderWithImage({

fullName,

countryOfOrigin,

userImage

}) { ... }

This is undoubtedly a step in the right direction. But we can do better. What if I don’t want to display a userImage, but instead an image of my favorite cat (Jasper)?

Rule #4: Props should not be named according to the application state that gets passed in. Instead, they should be named according to their usage within the component itself.

This is just like any function you write, but is sometimes overlooked on React components. In the end, the prop is in reality any string that’s used as the source for an image, and so should be named accordingly. With that rule in mind, we get:

function ScreenHeaderWithImage({

primaryHeadline,

secondaryHeadline,

imageSourceUrl

}) { ... }

The PropType Validation is Wrong

Let’s look back at the original validations being used:

// GuestShape was imported in the previous component code snippet,

// but is shown inline here for stupidity demonstration purposes. const GuestShape = PropTypes.shape({

id: PropTypes.number,

fullName: PropTypes.string,

isPremium: PropTypes.bool,

userImage: PropTypes.string,

countryOfOrigin: PropTypes.string,

countryCodeOfOrigin: PropTypes.string,

gender: PropTypes.string

}); GuestHeader.propTypes = {

guest: GuestShape.isRequired

}; GuestProfilePhoto.propTypes = {

guest: GuestShape.isRequired

};

Nothing is `required` in GuestShape!

I was trying to reuse the same validation shape across multiple components, and different components needed different parts of it, so nothing could be required!

By attempting to reuse PropType validation on application-specific state used as props, my state shape could change, break my app, and I wouldn’t even get so much as a warning!

Now that we’ve changed up our component’s prop structure though, we can easily fix this critical point of failure:

// Rest easy, friend. I got ya. ScreenHeaderWithImage.propTypes = {

imageSourceUrl: React.PropTypes.string.isRequired,

primaryHeadline: React.PropTypes.string.isRequired,

secondaryHeadline: React.PropTypes.string // Optional

}

Glad that’s gone….

The Component Coupling is Wrong

We still have one problem, and it’s a subtle one that by itself warrants its own article. It’s that the ScreenHeaderWithImage component is coupled to the CircularImage component. What if I want a SquareImage? Or no image at all?

One solution would be to pass in a prop or twenty for this:

function ScreenHeaderWithImage({

primaryHeadline,

secondaryHeadline,

imageSourceUrl,

hasImage

}) {

if (hasImage) { ... }

else { ... }

...

}

… but this is not sustainable and is still limiting. Instead, React gives us a far more powerful tool to handle the situation where a component wishes to not care about what child component(s) are used.

Composition!

Rule #5: Composition is superior to import statements at the top and is the primary mechanism which allows your component to focus on doing ONE thing. Use it wherever, and whenever you can.

By composing, a parent can become decoupled from its children and this allows for its own usage and function to become more granular and clear.

Wherever you are taking in some props and merely passing them unmodified to a child component, that’s also a good indication of a spot to use composition instead.

function ScreenHeader({

primaryHeader,

secondaryHeader,

children

}) {

return (

<View>

<View style={{ flex:1, ... }}> // Centered container

{children}

</View>

<Text style={{ fontSize: 20 }}>{primaryHeader}</Text>

<Text style={{ fontSize: 10}}>{secondaryHeader}</Text>

</View>

);

}

The ScreenHeader component is now the epitome of reusable!!!

Assuming you’re using React Native….

The Platform Coupling is Wrong(ish)

Let’s go crazy. What if I want to display this on the web? I can’t use this component because it’s using React Native components (View & Text) as well as styling that doesn’t exist outside of React Native….

The good news is that React is just the tool for the job.

// Replace the "View" with a generic container component

import Container from './Container'; // Replace the "Text" with more semantic components

import PrimaryHeadline from './PrimaryHeadline';

import SecondaryHeadline from './SecondaryHeadline'; function ScreenHeader({

primaryHeader,

secondaryHeader,

children

}) {

return (

<Container>

<Container centered>

{children}

</Container>

<PrimaryHeadline>{primaryHeader}</PrimaryHeadline>

<SecondaryHeadline>{secondaryHeader}</SecondaryHeadline>

</Container>

);

}

The bad news is that the problem is just pushed forward. The new components look something like this:

// Container.js -- Depends on React Native :( import { View } from 'react-native'; const center = {

flex:1,

flexDirection:'row',

alignItems:'center',

justifyContent:'center'

}; function Container({ centered }) {

let style = {}; if (centered) {

Object.assign(style, center);

} return (

<View style={style} />

);

} ------ // Text.js -- Depends on React Native :( import Text from 'ReactNative';

function RNTextWrapper({ size }) {

return <Text style={{size}} />;

} ------ // PrimaryHeadline.js -- Semantic titles, requires only Text above import Text from './Text';

function PrimaryHeadline({ children }) {

return <Text size={20}>{children}</Text>;

} ------ // SecondaryHeadline.js import Text from './Text';

function SecondaryHeadline({ children }) {

return <Text size={10}>{children}</Text>;

}

But, the good news is that all you need to do now is rewrite these TWO components (Container and Text) to support multiple platforms!

In theory :)

TL;DR All the guidelines in one place

The idea: Presentational React components should not know about your company, its product, or the platform it’s built for. None of them exist.

#1: Do not name your components after the part of state they connect to.

#2: Do not name your components for the role they play in YOUR application.

#3: A component should be given ONLY the props it requires. This structure should be flat.

#4: Props should not be named according to the application state that gets passed in. Instead, they should be named according to their usage within the component itself.

#5: Composition is superior to import statements at the top and is the primary mechanism which allows your component to focus on doing ONE thing. Use it wherever, and whenever you can.

Here are some common ways presentational components “know” about a product:

By using names for a particular part of the application’s state (e.g. EventStats)

By using names based on the role the component plays in the application (e.g. RegistrationConfirmation)

And some common ways presentational components “know” about a platform: